* Calculate and display total CPU and memory usage
Added total CPU and memory usage calculations for containers.
* Refactor Docker component layout
Refactor component layout and remove redundant Group elements.
* Create test.yaml
* Add files via upload
* Delete packages/icons/icons directory
Accidental
* Add total CPU and memory strings to English translations
* Add English translations and size refactor
* Fix typo in search label in en.json
* chore(docs): update OpenAPI spec
* Remove color styling from CPU and memory usage text
* Enhance Docker component with footer and toolbar updates
Updated createColumns function to include totals and relativeTime parameters for footer display. Adjusted table configuration to enable bottom toolbar based on isTiny flag.
* Fix formatting and add missing newline at end of file
* Enable sticky header and footer in component
Enable sticky header and add footer row properties for sticky positioning.
* Refactor table component properties
* Refactor table columns and fix footers
* Revert "chore(docs): update OpenAPI spec"
This reverts commit 26f3c809d9.
---------
Co-authored-by: Bergasha <Bergasha@users.noreply.github.com>
Co-authored-by: Manuel <30572287+manuel-rw@users.noreply.github.com>
300 lines
9.1 KiB
TypeScript
300 lines
9.1 KiB
TypeScript
"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, cpuUsageColor, memoryUsageColor, safeValue } 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 (
|
|
<Badge size="xs" radius="sm" variant="light" color={containerStateColorMap[state]}>
|
|
{t(state)}
|
|
</Badge>
|
|
);
|
|
};
|
|
|
|
const actionIconIconStyle: IconProps["style"] = {
|
|
height: "var(--ai-icon-size)",
|
|
width: "var(--ai-icon-size)",
|
|
};
|
|
|
|
const createColumns = (
|
|
t: ReturnType<typeof useScopedI18n<"docker">>,
|
|
): MRT_ColumnDef<RouterOutputs["docker"]["getContainers"]["containers"][number]>[] => [
|
|
{
|
|
id: "name",
|
|
accessorKey: "name",
|
|
header: t("field.name.label"),
|
|
Cell({ renderedCellValue, row }) {
|
|
return (
|
|
<Group gap="xs" wrap="nowrap">
|
|
<Avatar variant="outline" size={20} src={row.original.iconUrl} />
|
|
<Text p="0.5" size="sm" style={{ overflow: "hidden", textOverflow: "ellipsis" }}>
|
|
{renderedCellValue}
|
|
</Text>
|
|
</Group>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: "state",
|
|
accessorKey: "state",
|
|
size: 100,
|
|
header: t("field.state.label"),
|
|
Cell({ row }) {
|
|
return <ContainerStateBadge state={row.original.state} />;
|
|
},
|
|
},
|
|
{
|
|
id: "host",
|
|
accessorKey: "host",
|
|
size: 100,
|
|
header: t("field.host.label"),
|
|
Cell({ row }) {
|
|
return (
|
|
<Text size="xs" truncate="end">
|
|
{row.original.host}
|
|
</Text>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
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 (
|
|
<Text size="xs" c={cpuUsageColor(cpuUsage, row.original.state)}>
|
|
{cpuUsage.toFixed(2)}%
|
|
</Text>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
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 (
|
|
<Text size="xs" c={memoryUsageColor(bytesUsage, row.original.state)}>
|
|
{humanFileSize(bytesUsage)}
|
|
</Text>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
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 (
|
|
<Group wrap="nowrap" gap="xs">
|
|
<Tooltip label={row.original.state === "running" ? t("action.stop.label") : t("action.start.label")}>
|
|
<ActionIcon
|
|
variant="subtle"
|
|
size="xs"
|
|
radius="100%"
|
|
onClick={() => handleActionAsync(row.original.state === "running" ? "stop" : "start")}
|
|
>
|
|
{row.original.state === "running" ? (
|
|
<IconPlayerStop style={actionIconIconStyle} />
|
|
) : (
|
|
<IconPlayerPlay style={actionIconIconStyle} />
|
|
)}
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
<Tooltip label={t("action.restart.label")}>
|
|
<ActionIcon variant="subtle" size="xs" radius="100%" onClick={() => handleActionAsync("restart")}>
|
|
<IconRotateClockwise style={actionIconIconStyle} />
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
</Group>
|
|
);
|
|
},
|
|
},
|
|
];
|
|
|
|
export default function DockerWidget({ options, width, isEditMode }: WidgetComponentProps<"dockerContainers">) {
|
|
const t = useScopedI18n("docker");
|
|
const isTiny = width <= 256;
|
|
|
|
const utils = clientApi.useUtils();
|
|
const { data } = clientApi.docker.getContainers.useQuery(undefined, {
|
|
staleTime: 20 * 1000,
|
|
});
|
|
const containers = data?.containers ?? [];
|
|
const timestamp = useMemo(() => data?.timestamp ?? new Date(), [data?.timestamp]);
|
|
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 totals = useMemo(() => {
|
|
return containers.reduce(
|
|
(acc, container) => {
|
|
acc.cpu += safeValue(container.cpuUsage);
|
|
acc.memory += safeValue(container.memoryUsage);
|
|
return acc;
|
|
},
|
|
{ cpu: 0, memory: 0 }
|
|
);
|
|
}, [containers]);
|
|
|
|
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 (
|
|
<Stack gap={0} h="100%" display="flex">
|
|
<MantineReactTable table={table} />
|
|
|
|
{!isTiny && (
|
|
<Group
|
|
justify="space-between"
|
|
style={{
|
|
borderTop: "0.0625rem solid var(--border-color)",
|
|
}}
|
|
p={4}
|
|
>
|
|
<Group gap={4} wrap="nowrap">
|
|
<IconBrandDocker size={20} style={{ flexShrink: 0 }} />
|
|
<Text size="sm" truncate="end">
|
|
{t("table.footer", { count: totalContainers.toString() })}
|
|
</Text>
|
|
</Group>
|
|
|
|
<Group gap="sm" wrap="wrap" justify="flex-end" style={{ flex: 1, minWidth: 0 }}>
|
|
<Text size="sm" style={{ whiteSpace: "nowrap" }}>
|
|
{t("table.totalCpu", { cpu: totals.cpu.toFixed(2) })}
|
|
</Text>
|
|
|
|
<Text size="sm" style={{ whiteSpace: "nowrap" }}>
|
|
{t("table.totalMemory", { memory: humanFileSize(totals.memory) })}
|
|
</Text>
|
|
|
|
<Text size="sm" style={{ whiteSpace: "nowrap" }}>
|
|
{t("table.updated", { when: relativeTime })}
|
|
</Text>
|
|
</Group>
|
|
</Group>
|
|
)}
|
|
</Stack>
|
|
);
|
|
}
|