"use client"; import { useMemo } from "react"; import { ActionIcon, Avatar, Badge, Group, Stack, Text, Tooltip } from "@mantine/core"; import type { IconProps } from "@tabler/icons-react"; import { IconBrandDocker, IconPlayerPlay, IconPlayerStop, IconRotateClockwise } from "@tabler/icons-react"; import type { MRT_ColumnDef } from "mantine-react-table"; import { MantineReactTable } from "mantine-react-table"; import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; import { humanFileSize, useTimeAgo } from "@homarr/common"; import type { ContainerState } from "@homarr/docker"; import { containerStateColorMap } from "@homarr/docker/shared"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useScopedI18n } from "@homarr/translation/client"; import { useTranslatedMantineReactTable } from "@homarr/ui/hooks"; import type { WidgetComponentProps } from "../definition"; const ContainerStateBadge = ({ state }: { state: ContainerState }) => { const t = useScopedI18n("docker.field.state.option"); return ( {t(state)} ); }; const memoryUsageColor = (number: number, state: string) => { const mbUsage = number / 1024 / 1024; if (mbUsage === 0 && state !== "running") return "red"; if (mbUsage < 128) return "green"; if (mbUsage < 256) return "yellow"; if (mbUsage < 512) return "orange"; return "red"; }; const cpuUsageColor = (number: number, state: string) => { if (number === 0 && state !== "running") return "red"; if (number < 40) return "green"; if (number < 60) return "yellow"; if (number < 90) return "orange"; return "red"; }; const safeValue = (value?: number, fallback = 0) => (value !== undefined && !isNaN(value) ? value : fallback); const actionIconIconStyle: IconProps["style"] = { height: "var(--ai-icon-size)", width: "var(--ai-icon-size)", }; const createColumns = ( t: ReturnType>, ): MRT_ColumnDef[] => [ { id: "name", accessorKey: "name", header: t("field.name.label"), Cell({ renderedCellValue, row }) { return ( {renderedCellValue} ); }, }, { id: "state", accessorKey: "state", size: 100, header: t("field.state.label"), Cell({ row }) { return ; }, }, { id: "cpuUsage", sortingFn: (rowA, rowB) => { const cpuUsageA = safeValue(rowA.original.cpuUsage); const cpuUsageB = safeValue(rowB.original.cpuUsage); return cpuUsageA - cpuUsageB; }, accessorKey: "cpuUsage", size: 80, header: t("field.stats.cpu.label"), Cell({ row }) { const cpuUsage = safeValue(row.original.cpuUsage); return ( {cpuUsage.toFixed(2)}% ); }, }, { id: "memoryUsage", sortingFn: (rowA, rowB) => { const memoryUsageA = safeValue(rowA.original.memoryUsage); const memoryUsageB = safeValue(rowB.original.memoryUsage); return memoryUsageA - memoryUsageB; }, accessorKey: "memoryUsage", size: 80, header: t("field.stats.memory.label"), Cell({ row }) { const bytesUsage = safeValue(row.original.memoryUsage); return ( {humanFileSize(bytesUsage)} ); }, }, { id: "actions", accessorKey: "actions", size: 80, header: t("action.title"), enableSorting: false, Cell({ row }) { const utils = clientApi.useUtils(); // eslint-disable-next-line no-restricted-syntax const onSettled = async () => { await utils.docker.getContainers.invalidate(); }; const { mutateAsync: startContainer } = clientApi.docker.startAll.useMutation({ onSettled }); const { mutateAsync: stopContainer } = clientApi.docker.stopAll.useMutation({ onSettled }); const { mutateAsync: restartContainer } = clientApi.docker.restartAll.useMutation({ onSettled }); const handleActionAsync = async (action: "start" | "stop" | "restart") => { const mutation = action === "start" ? startContainer : action === "stop" ? stopContainer : restartContainer; await mutation( { ids: [row.original.id] }, { onSuccess() { showSuccessNotification({ title: t(`action.${action}.notification.success.title`), message: t(`action.${action}.notification.success.message`), }); }, onError() { showErrorNotification({ title: t(`action.${action}.notification.error.title`), message: t(`action.${action}.notification.error.message`), }); }, }, ); }; return ( handleActionAsync(row.original.state === "running" ? "stop" : "start")} > {row.original.state === "running" ? ( ) : ( )} handleActionAsync("restart")}> ); }, }, ]; export default function DockerWidget({ options, width, isEditMode }: WidgetComponentProps<"dockerContainers">) { const t = useScopedI18n("docker"); const isTiny = width <= 256; const utils = clientApi.useUtils(); const [{ containers, timestamp }] = clientApi.docker.getContainers.useSuspenseQuery(); const relativeTime = useTimeAgo(timestamp); clientApi.docker.subscribeContainers.useSubscription(undefined, { onData(data) { utils.docker.getContainers.setData(undefined, { containers: data, timestamp: new Date() }); }, }); const totalContainers = containers.length; const columns = useMemo(() => createColumns(t), [t]); const table = useTranslatedMantineReactTable({ columns, data: containers, enablePagination: false, enableTopToolbar: false, enableBottomToolbar: false, enableColumnActions: false, enableSorting: options.enableRowSorting && !isEditMode, enableStickyHeader: false, enableColumnOrdering: false, enableRowSelection: false, enableFullScreenToggle: false, enableGlobalFilter: false, enableDensityToggle: false, enableFilters: false, enableHiding: false, initialState: { sorting: [{ id: options.defaultSort, desc: options.descendingDefaultSort }], density: "xs", }, mantinePaperProps: { flex: 1, withBorder: false, shadow: undefined, }, mantineTableProps: { className: "docker-widget-table", style: { tableLayout: "fixed", }, }, mantineTableHeadProps: { fz: "xs", }, mantineTableHeadCellProps: { p: 4, }, mantineTableBodyCellProps: { p: 4, }, mantineTableContainerProps: { style: { height: "100%", }, }, }); return ( {!isTiny && ( {t("table.footer", { count: totalContainers.toString() })} {t("table.updated", { when: relativeTime })} )} ); }