feat: refine mobile dashboard experience
This commit is contained in:
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"
|
||||
|
||||
Reference in new issue
Block a user