fix(widgets): address table layout review

This commit is contained in:
Thomas Camlong committed 2026-07-31 13:29:10 +02:00
1 parent e63618d00d
commit 0dad7ca67d
5 files changed
+67 -32

No files matched your search

@@ -52,3 +52,7 @@ Each container row has an action menu, accessible via the three-dot icon or by r
### Adding the widget
<AddingWidget />
### Configuration
<WidgetConfig configuration={dockerContainersWidget.configuration} />
@@ -26,7 +26,7 @@ import { useScopedI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../definition";
import { HomarrDataTable } from "../common/homarr-data-table";
import { usePersistedTableLayout } from "../common/use-persisted-table-layout";
import { usePersistedTableLayout, useTableLayoutPersistence } from "../common/use-persisted-table-layout";
import type { BeszelSystemRow } from "../beszel/_shared/types";
import { loadAvgColor, statusColorMap, thresholdColor } from "../beszel/_shared/colors";
import {
@@ -116,13 +116,13 @@ export default function BeszelSystemTableWidget({
message: t("error.layoutSaveMessage"),
}),
});
const persistLayout = useCallback(
(newOptions: Partial<Pick<typeof options, "columnOrder" | "columnWidths">>) => {
setOptions({ newOptions });
if (hasChangeAccess && boardId && itemId) saveItemOptions({ boardId, itemId, newOptions });
},
[boardId, hasChangeAccess, itemId, saveItemOptions, setOptions],
);
const persistLayout = useTableLayoutPersistence({
boardId,
hasChangeAccess,
itemId,
saveItemOptions,
setOptions,
});
const filteredSystems = useBeszelFilteredSystems(results, options.statusFilter);
@@ -1,14 +1,22 @@
"use client";
import { useEffect, useMemo, useRef } from "react";
import { useCallback, useEffect, useMemo, useRef } from "react";
import type { DataTableColumn } from "mantine-datatable";
import { useDataTableColumns } from "mantine-datatable";
interface TableLayoutOptions {
interface TableLayoutOptions extends Record<string, unknown> {
columnOrder?: string;
columnWidths?: string;
}
interface UseTableLayoutPersistenceProps {
boardId: string | undefined;
hasChangeAccess: boolean;
itemId: string | undefined;
saveItemOptions: (input: { boardId: string; itemId: string; newOptions: TableLayoutOptions }) => void;
setOptions: (input: { newOptions: TableLayoutOptions }) => void;
}
interface UsePersistedTableLayoutProps<T> {
columns: DataTableColumn<T>[];
columnAccessors: readonly string[];
@@ -21,6 +29,21 @@ interface UsePersistedTableLayoutProps<T> {
const layoutSaveDelay = 350;
export const useTableLayoutPersistence = ({
boardId,
hasChangeAccess,
itemId,
saveItemOptions,
setOptions,
}: UseTableLayoutPersistenceProps) =>
useCallback(
(newOptions: TableLayoutOptions) => {
setOptions({ newOptions });
if (hasChangeAccess && boardId && itemId) saveItemOptions({ boardId, itemId, newOptions });
},
[boardId, hasChangeAccess, itemId, saveItemOptions, setOptions],
);
const toPixelWidth = (width: string | number): number | undefined => {
if (typeof width === "number" && Number.isFinite(width) && width > 0) return width;
if (typeof width !== "string" || !width.endsWith("px")) return undefined;
@@ -147,11 +170,22 @@ export const usePersistedTableLayout = <T>({
const pendingLayout = useRef<TableLayoutOptions>({});
const saveTimeout = useRef<number | undefined>(undefined);
const onLayoutChangeRef = useRef(onLayoutChange);
useEffect(() => {
onLayoutChangeRef.current = onLayoutChange;
}, [onLayoutChange]);
const flushPendingLayout = useCallback(() => {
const layout = pendingLayout.current;
pendingLayout.current = {};
saveTimeout.current = undefined;
if (Object.keys(layout).length > 0) onLayoutChangeRef.current(layout);
}, []);
useEffect(
() => () => {
window.clearTimeout(saveTimeout.current);
flushPendingLayout();
},
[],
[flushPendingLayout],
);
useEffect(() => {
if (!hydrated.current) return;
@@ -179,11 +213,8 @@ export const usePersistedTableLayout = <T>({
pendingLayout.current = { ...pendingLayout.current, ...newLayout };
window.clearTimeout(saveTimeout.current);
saveTimeout.current = window.setTimeout(() => {
onLayoutChange(pendingLayout.current);
pendingLayout.current = {};
}, layoutSaveDelay);
}, [columnAccessors, columnsOrder, columnsWidth, onLayoutChange, savedOrder, savedWidths, visibleAccessorSet]);
saveTimeout.current = window.setTimeout(flushPendingLayout, layoutSaveDelay);
}, [columnAccessors, columnsOrder, columnsWidth, flushPendingLayout, savedOrder, savedWidths, visibleAccessorSet]);
return { effectiveColumns, storeKey };
};
+8 -8
View File
@@ -30,7 +30,7 @@ import { useScopedI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../definition";
import { HomarrDataTable } from "../common/homarr-data-table";
import { usePersistedTableLayout } from "../common/use-persisted-table-layout";
import { usePersistedTableLayout, useTableLayoutPersistence } from "../common/use-persisted-table-layout";
type DockerContainer = RouterOutputs["docker"]["getContainers"]["containers"][number];
type ContainerAction = "start" | "stop" | "restart" | "remove";
@@ -242,13 +242,13 @@ export default function DockerWidget({
message: tWidget("error.layoutSaveMessage"),
}),
});
const persistLayout = useCallback(
(newOptions: Partial<Pick<typeof options, "columnOrder" | "columnWidths">>) => {
setOptions({ newOptions });
if (hasChangeAccess && boardId && itemId) saveItemOptions({ boardId, itemId, newOptions });
},
[boardId, hasChangeAccess, itemId, saveItemOptions, setOptions],
);
const persistLayout = useTableLayoutPersistence({
boardId,
hasChangeAccess,
itemId,
saveItemOptions,
setOptions,
});
const defaultDirection = options.descendingDefaultSort ? "desc" : "asc";
const [sortStatus, setSortStatus] = useState<DataTableSortStatus<DockerContainer>>({
+8 -8
View File
@@ -54,7 +54,7 @@ import { useScopedI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../definition";
import { HomarrDataTable } from "../common/homarr-data-table";
import { usePersistedTableLayout } from "../common/use-persisted-table-layout";
import { usePersistedTableLayout, useTableLayoutPersistence } from "../common/use-persisted-table-layout";
dayjs.extend(relativeTime);
@@ -338,13 +338,13 @@ export default function DownloadClientsWidget({
const { mutate: saveItemOptions } = clientApi.widget.options.saveItemOptions.useMutation({
onError: () => showErrorNotification({ title: t("errors.actionFailed"), message: t("errors.actionFailedMessage") }),
});
const persistOption = useCallback(
(newOptions: Partial<Pick<typeof options, "columnOrder" | "columnWidths">>) => {
setOptions({ newOptions });
if (hasChangeAccess && boardId && itemId) saveItemOptions({ boardId, itemId, newOptions });
},
[setOptions, hasChangeAccess, boardId, itemId, saveItemOptions],
);
const persistOption = useTableLayoutPersistence({
boardId,
hasChangeAccess,
itemId,
saveItemOptions,
setOptions,
});
let defaultSortDirection: DataTableSortStatus<ExtendedDownloadClientItem>["direction"] = "asc";
if (options.descendingDefaultSort) defaultSortDirection = "desc";