mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-27 08:50:56 +01:00
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com> Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com> Co-authored-by: homarr-crowdin[bot] <190541745+homarr-crowdin[bot]@users.noreply.github.com> Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
257 lines
7.8 KiB
TypeScript
257 lines
7.8 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 } 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 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<typeof useScopedI18n<"docker">>,
|
|
): MRT_ColumnDef<RouterOutputs["docker"]["getContainers"]["containers"][number]>[] => [
|
|
{
|
|
accessorKey: "name",
|
|
header: t("field.name.label"),
|
|
Cell({ renderedCellValue, row }) {
|
|
return (
|
|
<Group gap="xs" wrap="nowrap">
|
|
<Avatar variant="outline" radius="md" size={20} src={row.original.iconUrl} />
|
|
<Text p="0.5" size="sm" style={{ overflow: "hidden", textOverflow: "ellipsis" }}>
|
|
{renderedCellValue}
|
|
</Text>
|
|
</Group>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "state",
|
|
size: 100,
|
|
header: t("field.state.label"),
|
|
Cell({ row }) {
|
|
return <ContainerStateBadge state={row.original.state} />;
|
|
},
|
|
},
|
|
{
|
|
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>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
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>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "actions",
|
|
size: 80,
|
|
header: t("action.title"),
|
|
Cell({ row }) {
|
|
const utils = clientApi.useUtils();
|
|
const { mutateAsync: startContainer } = clientApi.docker.startAll.useMutation();
|
|
const { mutateAsync: stopContainer } = clientApi.docker.stopAll.useMutation();
|
|
const { mutateAsync: restartContainer } = clientApi.docker.restartAll.useMutation();
|
|
|
|
const handleActionAsync = async (action: "start" | "stop" | "restart") => {
|
|
const mutation = action === "start" ? startContainer : action === "stop" ? stopContainer : restartContainer;
|
|
|
|
await mutation(
|
|
{ ids: [row.original.id] },
|
|
{
|
|
async onSettled() {
|
|
await utils.docker.getContainers.invalidate();
|
|
},
|
|
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({ width }: 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,
|
|
enableSorting: false,
|
|
enableColumnActions: false,
|
|
enableStickyHeader: false,
|
|
enableColumnOrdering: false,
|
|
enableRowSelection: false,
|
|
enableFullScreenToggle: false,
|
|
enableGlobalFilter: false,
|
|
enableDensityToggle: false,
|
|
enableFilters: false,
|
|
enableHiding: false,
|
|
initialState: {
|
|
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}>
|
|
<IconBrandDocker size={20} />
|
|
<Text size="sm">{t("table.footer", { count: totalContainers.toString() })}</Text>
|
|
</Group>
|
|
|
|
<Text size="sm">{t("table.updated", { when: relativeTime })}</Text>
|
|
</Group>
|
|
)}
|
|
</Stack>
|
|
);
|
|
}
|