feat: refine mobile dashboard experience

This commit is contained in:
ajnart committed 2026-08-13 19:55:11 +02:00
1 parent 2e3dca3e87
commit 84db2f9d49
68 files changed
+1425 -1311

No files matched your search

@@ -20,7 +20,6 @@ import { clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client";
import { constructBoardPermissions } from "@homarr/auth/shared";
import { useOptionalBoard } from "@homarr/boards/context";
import { formatBytes } from "@homarr/common";
import { useModalAction } from "@homarr/modals";
import { showErrorNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
@@ -41,23 +40,15 @@ import {
} from "../beszel/_shared/format";
import { useBeszelFilteredSystems } from "../beszel/_shared/hooks";
import { IntegrationErrorIndicator } from "../common/integration-error-indicator";
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
import { BeszelSystemStatsModal } from "../beszel/_shared/system-stats-modal";
import { DiskUsage } from "../beszel/_shared/disk-usage";
import { getBeszelTableVisibleMetricKeys } from "./display";
const directionMultiplier: Record<string, number> = { asc: 1, desc: -1 };
type SystemRowWithKey = BeszelSystemRow & { rowKey: string; integrationName: string };
type SystemRowWithKey = BeszelSystemRow & { _key: string };
const columnAccessors = [
"name",
"integrationName",
"hostname",
"osName",
"cpuModel",
"cores",
"memoryTotal",
"cpu",
"memory",
"disk",
@@ -69,7 +60,7 @@ const columnAccessors = [
"services",
"uptime",
"agentVersion",
] as const satisfies readonly (keyof SystemRowWithKey)[];
] as const satisfies readonly (keyof BeszelSystemRow)[];
interface SizeConfig {
iconSize: number;
@@ -106,7 +97,6 @@ export default function BeszelSystemTableWidget({
boardId,
itemId,
setOptions,
displayMode,
}: WidgetComponentProps<"beszelSystemTable">) {
const t = useScopedI18n("widget.beszelSystemTable");
const { openModal } = useModalAction(BeszelSystemStatsModal);
@@ -114,15 +104,26 @@ export default function BeszelSystemTableWidget({
const { data: session } = useSession();
const hasChangeAccess = board ? constructBoardPermissions(board, session).hasChangeAccess : false;
const systemsQuery = clientApi.widget.beszel.getSystems.useQuery({ integrationIds });
const systemsData = getUsableWidgetQueryData(systemsQuery);
const results = useMemo(() => systemsData ?? [], [systemsData]);
const results = getUsableWidgetQueryData(systemsQuery) ?? [];
const { isPending } = systemsQuery;
const isAdvanced = displayMode === "advanced";
const size = useMemo(() => getSizeConfig(width), [width]);
const visibleMetricKeys = useMemo(
() => getBeszelTableVisibleMetricKeys(options, width, isAdvanced),
[options, width, isAdvanced],
);
const visibleMetricKeys = useMemo(() => {
const enabled = [
"showCpu",
"showMemory",
"showDisk",
"showGpu",
"showLoadAvg",
"showNet",
"showTemp",
"showBattery",
"showServices",
"showUptime",
"showAgent",
].filter((key) => options[key as keyof typeof options]) as string[];
const budget = width < 360 ? 1 : width < 560 ? 2 : width < 760 ? 4 : enabled.length;
return new Set(enabled.slice(0, budget));
}, [options, width]);
const { mutate: saveItemOptions } = clientApi.widget.options.saveItemOptions.useMutation({
onError: () =>
@@ -140,18 +141,6 @@ export default function BeszelSystemTableWidget({
});
const filteredSystems = useBeszelFilteredSystems(results, options.statusFilter);
const integrationNames = useMemo(
() => new Map(results.map((result) => [result.integrationId, result.integrationName])),
[results],
);
const systemsWithSource = useMemo(
() =>
filteredSystems.map((system) => ({
...system,
integrationName: integrationNames.get(system.rowKey.split(":")[0] ?? "") ?? "—",
})),
[filteredSystems, integrationNames],
);
const [sortStatus, setSortStatus] = useState<DataTableSortStatus<SystemRowWithKey>>({
columnAccessor: options.sortBy,
@@ -165,16 +154,16 @@ export default function BeszelSystemTableWidget({
}, [options.sortBy, options.sortDirection]);
const sortedSystems = useMemo(() => {
const accessor = sortStatus.columnAccessor as keyof SystemRowWithKey;
const accessor = sortStatus.columnAccessor as keyof BeszelSystemRow;
const dir = directionMultiplier[sortStatus.direction] ?? 1;
return [...systemsWithSource].toSorted((a, b) => {
return [...filteredSystems].toSorted((a, b) => {
const aVal = a[accessor] ?? 0;
const bVal = b[accessor] ?? 0;
if (typeof aVal === "string" && typeof bVal === "string") return aVal.localeCompare(bVal) * dir;
if (typeof aVal === "number" && typeof bVal === "number") return (aVal - bVal) * dir;
return 0;
});
}, [systemsWithSource, sortStatus]);
}, [filteredSystems, sortStatus]);
const PercentCell = ({ value }: { value: number }) => (
<Group gap={8} wrap="nowrap" style={{ flex: 1 }}>
@@ -212,54 +201,6 @@ export default function BeszelSystemTableWidget({
</Group>
),
},
isAdvanced && {
accessor: "integrationName",
width: 140,
ellipsis: true,
title: t("column.source"),
sortable: true,
render: (record) => <Text size={size.fontSize}>{record.integrationName}</Text>,
},
isAdvanced && {
accessor: "hostname",
width: 150,
ellipsis: true,
title: t("column.hostname"),
sortable: true,
render: (record) => <Text size={size.fontSize}>{record.hostname || "—"}</Text>,
},
isAdvanced && {
accessor: "osName",
width: 160,
ellipsis: true,
title: t("column.os"),
sortable: true,
render: (record) => <Text size={size.fontSize}>{record.osName || "—"}</Text>,
},
isAdvanced && {
accessor: "cpuModel",
width: 220,
ellipsis: true,
title: t("column.cpuModel"),
sortable: true,
render: (record) => <Text size={size.fontSize}>{record.cpuModel || "—"}</Text>,
},
isAdvanced && {
accessor: "cores",
width: 80,
title: t("column.cores"),
sortable: true,
render: (record) => <Text size={size.fontSize}>{record.cores}</Text>,
},
isAdvanced && {
accessor: "memoryTotal",
width: 120,
title: t("column.memoryTotal"),
sortable: true,
render: (record) => (
<Text size={size.fontSize}>{record.memoryTotal > 0 ? formatBytes(record.memoryTotal) : "—"}</Text>
),
},
visibleMetricKeys.has("showCpu") && {
accessor: "cpu",
width: 140,
@@ -416,7 +357,7 @@ export default function BeszelSystemTableWidget({
];
return cols.filter(Boolean) as DataTableColumn<SystemRowWithKey>[];
}, [t, size, visibleMetricKeys, isAdvanced]);
}, [t, size, visibleMetricKeys]);
const { effectiveColumns, storeKey } = usePersistedTableLayout({
columns,
@@ -430,7 +371,7 @@ export default function BeszelSystemTableWidget({
const handleRowClick = useCallback(
({ record }: { record: SystemRowWithKey }) => {
const integrationId = record.rowKey.split(":")[0] ?? "";
const integrationId = record._key.split(":")[0] ?? "";
openModal({ integrationId, systemId: record.id }, { title: record.name });
},
[openModal],
@@ -447,10 +388,7 @@ export default function BeszelSystemTableWidget({
const table = (
<div style={{ position: "relative", height: "100%" }}>
<div style={{ position: "absolute", top: 4, right: 8, zIndex: 1 }}>
<Group gap={0}>
<WidgetQueryErrorIndicator error={systemsQuery.error} label={t("name")} />
<IntegrationErrorIndicator results={results} />
</Group>
<IntegrationErrorIndicator results={results} />
</div>
<HomarrDataTable
isEditMode={isEditMode}
@@ -461,7 +399,7 @@ export default function BeszelSystemTableWidget({
sortStatus={sortStatus}
onSortStatusChange={setSortStatus}
noRecordsText={t("noRecords")}
idAccessor="rowKey"
idAccessor="_key"
storeColumnsKey={storeKey}
onRowClick={isEditMode ? undefined : handleRowClick}
/>
@@ -25,6 +25,7 @@ const sortDirectionOptions = [
export const { definition, componentLoader } = createWidgetDefinition("beszelSystemTable", {
icon: IconTable,
supportsAdvancedFocus: false,
queryKeys: [[["widget", "beszel", "getSystems"]], [["widget", "beszel", "getSystemStats"]]],
queryMatcher: (query, scope) =>
query.path.at(-1) === "getSystems"