|
| 1 | +"use client"; |
| 2 | + |
| 3 | +import type { ButtonProps, MantineColor } from "@mantine/core"; |
| 4 | +import { Avatar, Badge, Box, Button, Group, Text } from "@mantine/core"; |
| 5 | +import { IconPlayerPlay, IconPlayerStop, IconRotateClockwise, IconTrash } from "@tabler/icons-react"; |
| 6 | +import type { MRT_ColumnDef } from "mantine-react-table"; |
| 7 | +import { MantineReactTable, useMantineReactTable } from "mantine-react-table"; |
| 8 | + |
| 9 | +import type { RouterOutputs } from "@homarr/api"; |
| 10 | +import { useTimeAgo } from "@homarr/common"; |
| 11 | +import type { DockerContainerState } from "@homarr/definitions"; |
| 12 | +import type { TranslationFunction } from "@homarr/translation"; |
| 13 | +import { useI18n, useScopedI18n } from "@homarr/translation/client"; |
| 14 | +import { OverflowBadge } from "@homarr/ui"; |
| 15 | + |
| 16 | +const createColumns = ( |
| 17 | + t: TranslationFunction, |
| 18 | +): MRT_ColumnDef<RouterOutputs["docker"]["getContainers"]["containers"][number]>[] => [ |
| 19 | + { |
| 20 | + accessorKey: "name", |
| 21 | + header: t("docker.field.name.label"), |
| 22 | + Cell({ renderedCellValue, row }) { |
| 23 | + return ( |
| 24 | + <Group gap="xs"> |
| 25 | + <Avatar variant="outline" radius="lg" size="md" src={row.original.iconUrl}> |
| 26 | + {row.original.name.at(0)?.toUpperCase()} |
| 27 | + </Avatar> |
| 28 | + <Text>{renderedCellValue}</Text> |
| 29 | + </Group> |
| 30 | + ); |
| 31 | + }, |
| 32 | + }, |
| 33 | + { |
| 34 | + accessorKey: "state", |
| 35 | + header: t("docker.field.state.label"), |
| 36 | + size: 120, |
| 37 | + Cell({ cell }) { |
| 38 | + return <ContainerStateBadge state={cell.row.original.state} />; |
| 39 | + }, |
| 40 | + }, |
| 41 | + { |
| 42 | + accessorKey: "image", |
| 43 | + header: t("docker.field.containerImage.label"), |
| 44 | + maxSize: 200, |
| 45 | + Cell({ renderedCellValue }) { |
| 46 | + return ( |
| 47 | + <Box maw={200}> |
| 48 | + <Text truncate="end">{renderedCellValue}</Text> |
| 49 | + </Box> |
| 50 | + ); |
| 51 | + }, |
| 52 | + }, |
| 53 | + { |
| 54 | + accessorKey: "ports", |
| 55 | + header: t("docker.field.ports.label"), |
| 56 | + Cell({ cell }) { |
| 57 | + return ( |
| 58 | + <OverflowBadge overflowCount={1} data={cell.row.original.ports.map((port) => port.PrivatePort.toString())} /> |
| 59 | + ); |
| 60 | + }, |
| 61 | + }, |
| 62 | +]; |
| 63 | + |
| 64 | +export function DockerTable({ containers, timestamp }: RouterOutputs["docker"]["getContainers"]) { |
| 65 | + const t = useI18n(); |
| 66 | + const tDocker = useScopedI18n("docker"); |
| 67 | + const relativeTime = useTimeAgo(timestamp); |
| 68 | + const table = useMantineReactTable({ |
| 69 | + data: containers, |
| 70 | + enableDensityToggle: false, |
| 71 | + enableColumnActions: false, |
| 72 | + enableColumnFilters: false, |
| 73 | + enablePagination: false, |
| 74 | + enableRowSelection: true, |
| 75 | + positionToolbarAlertBanner: "top", |
| 76 | + enableTableFooter: false, |
| 77 | + enableBottomToolbar: false, |
| 78 | + positionGlobalFilter: "right", |
| 79 | + mantineSearchTextInputProps: { |
| 80 | + placeholder: tDocker("table.search", { count: containers.length }), |
| 81 | + style: { minWidth: 300 }, |
| 82 | + autoFocus: true, |
| 83 | + }, |
| 84 | + |
| 85 | + initialState: { density: "xs", showGlobalFilter: true }, |
| 86 | + renderToolbarAlertBannerContent: ({ groupedAlert, table }) => { |
| 87 | + return ( |
| 88 | + <Group gap={"sm"}> |
| 89 | + {groupedAlert} |
| 90 | + <Text fw={500}> |
| 91 | + {tDocker("table.selected", { |
| 92 | + selectCount: table.getSelectedRowModel().rows.length, |
| 93 | + totalCount: table.getRowCount(), |
| 94 | + })} |
| 95 | + </Text> |
| 96 | + <ContainerActionBar /> |
| 97 | + </Group> |
| 98 | + ); |
| 99 | + }, |
| 100 | + |
| 101 | + columns: createColumns(t), |
| 102 | + }); |
| 103 | + return ( |
| 104 | + <> |
| 105 | + <Text>{tDocker("table.updated", { when: relativeTime })}</Text> |
| 106 | + <MantineReactTable table={table} /> |
| 107 | + </> |
| 108 | + ); |
| 109 | +} |
| 110 | + |
| 111 | +const ContainerActionBar = () => { |
| 112 | + const t = useScopedI18n("docker.action"); |
| 113 | + const sharedButtonProps = { |
| 114 | + variant: "light", |
| 115 | + radius: "md", |
| 116 | + } satisfies Partial<ButtonProps>; |
| 117 | + |
| 118 | + return ( |
| 119 | + <Group gap="xs"> |
| 120 | + <Button leftSection={<IconPlayerPlay />} color="green" {...sharedButtonProps}> |
| 121 | + {t("start")} |
| 122 | + </Button> |
| 123 | + <Button leftSection={<IconPlayerStop />} color="red" {...sharedButtonProps}> |
| 124 | + {t("stop")} |
| 125 | + </Button> |
| 126 | + <Button leftSection={<IconRotateClockwise />} color="orange" {...sharedButtonProps}> |
| 127 | + {t("restart")} |
| 128 | + </Button> |
| 129 | + <Button leftSection={<IconTrash />} color="red" {...sharedButtonProps}> |
| 130 | + {t("remove")} |
| 131 | + </Button> |
| 132 | + </Group> |
| 133 | + ); |
| 134 | +}; |
| 135 | + |
| 136 | +const containerStates = { |
| 137 | + created: "cyan", |
| 138 | + running: "green", |
| 139 | + paused: "yellow", |
| 140 | + restarting: "orange", |
| 141 | + exited: "red", |
| 142 | + removing: "pink", |
| 143 | + dead: "dark", |
| 144 | +} satisfies Record<DockerContainerState, MantineColor>; |
| 145 | + |
| 146 | +const ContainerStateBadge = ({ state }: { state: DockerContainerState }) => { |
| 147 | + const t = useScopedI18n("docker.field.state.option"); |
| 148 | + |
| 149 | + return ( |
| 150 | + <Badge size="lg" radius="sm" variant="light" w={120} color={containerStates[state]}> |
| 151 | + {t(state)} |
| 152 | + </Badge> |
| 153 | + ); |
| 154 | +}; |
0 commit comments