feat: modernize dashboard widgets

This commit is contained in:
Thomas Camlong committed 2026-08-01 21:14:33 +02:00
1 parent fcc5130068
commit d421046d15
286 files changed
+9664 -3282

No files matched your search

+31 -22
View File
@@ -17,6 +17,7 @@ import { useScopedI18n } from "@homarr/translation/client";
import { useTranslatedMantineReactTable } from "@homarr/ui/hooks";
import type { WidgetComponentProps } from "../definition";
import { getDockerColumnVisibility } from "./layout";
const ContainerStateBadge = ({ state }: { state: ContainerState }) => {
const t = useScopedI18n("docker.field.state.option");
@@ -35,6 +36,7 @@ const actionIconIconStyle: IconProps["style"] = {
const createColumns = (
t: ReturnType<typeof useScopedI18n<"docker">>,
isEditMode: boolean,
): MRT_ColumnDef<RouterOutputs["docker"]["getContainers"]["containers"][number]>[] => [
{
id: "name",
@@ -160,6 +162,7 @@ const createColumns = (
variant="subtle"
size="xs"
radius="100%"
disabled={isEditMode}
onClick={() => handleActionAsync(row.original.state === "running" ? "stop" : "start")}
>
{row.original.state === "running" ? (
@@ -170,7 +173,13 @@ const createColumns = (
</ActionIcon>
</Tooltip>
<Tooltip label={t("action.restart.label")}>
<ActionIcon variant="subtle" size="xs" radius="100%" onClick={() => handleActionAsync("restart")}>
<ActionIcon
variant="subtle"
size="xs"
radius="100%"
disabled={isEditMode}
onClick={() => handleActionAsync("restart")}
>
<IconRotateClockwise style={actionIconIconStyle} />
</ActionIcon>
</Tooltip>
@@ -180,12 +189,18 @@ const createColumns = (
},
];
export default function DockerWidget({ options, width, isEditMode }: WidgetComponentProps<"dockerContainers">) {
export default function DockerWidget({
options,
width,
isEditMode,
displayMode,
}: WidgetComponentProps<"dockerContainers">) {
const t = useScopedI18n("docker");
const isTiny = width <= 256;
const isAdvanced = displayMode === "advanced";
const isTiny = !isAdvanced && width <= 256;
const { data, refetch, isFetching } = clientApi.docker.getContainers.useQuery();
const containers = data?.containers ?? [];
const containers = useMemo(() => data?.containers ?? [], [data?.containers]);
const timestamp = useMemo(() => data?.timestamp ?? new Date(), [data?.timestamp]);
const relativeTime = useTimeAgo(timestamp);
@@ -202,33 +217,26 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo
);
}, [containers]);
const columns = useMemo(() => createColumns(t), [t]);
const columns = useMemo(() => createColumns(t, isEditMode), [isEditMode, t]);
const columnVisibility: MRT_VisibilityState = {
name: options.columns.includes("name"),
state: options.columns.includes("state"),
host: options.columns.includes("host"),
cpuUsage: options.columns.includes("cpuUsage"),
memoryUsage: options.columns.includes("memoryUsage"),
actions: options.columns.includes("actions"),
};
const columnVisibility: MRT_VisibilityState = getDockerColumnVisibility(options.columns, width, isAdvanced);
const table = useTranslatedMantineReactTable({
columns,
data: containers,
enablePagination: false,
enableTopToolbar: false,
enableTopToolbar: isAdvanced,
enableBottomToolbar: false,
enableColumnActions: false,
enableSorting: options.enableRowSorting && !isEditMode,
enableStickyHeader: false,
enableColumnOrdering: false,
enableColumnActions: isAdvanced,
enableSorting: (isAdvanced || options.enableRowSorting) && !isEditMode,
enableStickyHeader: isAdvanced,
enableColumnOrdering: isAdvanced && !isEditMode,
enableRowSelection: false,
enableFullScreenToggle: false,
enableGlobalFilter: false,
enableGlobalFilter: isAdvanced,
enableDensityToggle: false,
enableFilters: false,
enableHiding: false,
enableHiding: isAdvanced,
initialState: {
sorting: [{ id: options.defaultSort, desc: options.descendingDefaultSort }],
density: "xs",
@@ -259,11 +267,12 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo
mantineTableContainerProps: {
style: {
height: "100%",
pointerEvents: isEditMode ? "none" : undefined,
},
},
});
if (options.columns.length === 0)
if (!isAdvanced && options.columns.length === 0)
return (
<Center h="100%">
<Text>{t("error.noColumns")}</Text>
@@ -274,7 +283,7 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo
<Stack gap={0} h="100%" display="flex">
<MantineReactTable table={table} />
{!isTiny && (
{(isAdvanced || !isTiny) && (
<Group
justify="space-between"
style={{
@@ -0,0 +1,37 @@
import { describe, expect, test } from "vitest";
import { getDockerColumnVisibility } from "./layout";
const columns = ["name", "state", "host", "cpuUsage", "memoryUsage", "actions"] as const;
describe("getDockerColumnVisibility", () => {
test("keeps only essential columns in a narrow compact widget", () => {
expect(getDockerColumnVisibility(columns, 240, false)).toEqual({
name: true,
state: true,
host: false,
cpuUsage: false,
memoryUsage: false,
actions: true,
});
});
test("reveals compact metrics as width becomes available", () => {
expect(getDockerColumnVisibility(columns, 380, false)).toMatchObject({
host: false,
cpuUsage: true,
memoryUsage: true,
});
});
test("uses every expert column in advanced mode", () => {
expect(getDockerColumnVisibility([], 240, true)).toEqual({
name: true,
state: true,
host: true,
cpuUsage: true,
memoryUsage: true,
actions: true,
});
});
});
+19
View File
@@ -0,0 +1,19 @@
export type DockerColumn = "name" | "state" | "host" | "cpuUsage" | "memoryUsage" | "actions";
export const getDockerColumnVisibility = (
configuredColumns: readonly DockerColumn[],
width: number,
isAdvanced: boolean,
): Record<DockerColumn, boolean> => {
const configured = new Set(configuredColumns);
const isVisible = (column: DockerColumn) => isAdvanced || configured.has(column);
return {
name: isVisible("name"),
state: isVisible("state"),
host: isVisible("host") && (isAdvanced || width >= 440),
cpuUsage: isVisible("cpuUsage") && (isAdvanced || width >= 280),
memoryUsage: isVisible("memoryUsage") && (isAdvanced || width >= 360),
actions: isVisible("actions"),
};
};