Revert "fix: address mobile dashboard review findings"

This reverts commit 1039bb18b3.
This commit is contained in:
ajnart committed 2026-07-29 20:06:59 +02:00
1 parent 43e95ff185
commit 38a710efe3
21 files changed
+45 -175

No files matched your search

@@ -13,7 +13,6 @@ import { useScopedI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../definition";
import { BeszelIntegrationErrorIndicator } from "../beszel/_shared/error-indicator";
import { hasStaleIntegrationData } from "../beszel/_shared/query-status";
import { WidgetMobileLoading, WidgetMobileSummary } from "../common/mobile-summary";
const alertIconMap: Record<string, LucideIcon> = {
@@ -44,13 +43,7 @@ export default function BeszelAlertsWidget({
() => ({ integrationIds, includeHistory: options.showHistory, maxHistoryItems: options.maxHistoryItems }),
[integrationIds, options.showHistory, options.maxHistoryItems],
);
const {
data: results = [],
error: alertsError,
isPending,
isLoadingError: alertsLoadingError,
isRefetchError: alertsRefetchError,
} = clientApi.widget.beszel.getAlerts.useQuery(alertsInput);
const { data: results = [], error: alertsError, isPending } = clientApi.widget.beszel.getAlerts.useQuery(alertsInput);
const systemNameMap = useMemo(() => {
const map: Record<string, string> = {};
@@ -79,7 +72,7 @@ export default function BeszelAlertsWidget({
const failedResults = results.filter((result) => "error" in result);
if (alertsLoadingError) throw alertsError;
if (alertsError) throw alertsError;
if (results.length > 0 && failedResults.length === results.length) {
throw new Error(String(failedResults[0]?.error ?? "Unable to connect to Beszel"));
}
@@ -100,7 +93,7 @@ export default function BeszelAlertsWidget({
value={triggeredAlerts.length}
label={t("status.triggered")}
description={`${alerts.length} ${t("name")}`}
isStale={hasStaleIntegrationData(alertsRefetchError, failedResults)}
isStale={failedResults.length > 0}
/>
);
}
@@ -108,7 +101,7 @@ export default function BeszelAlertsWidget({
return (
<Box h="100%" pos="relative">
<Box pos="absolute" top={4} right={8} style={{ zIndex: 1 }}>
<BeszelIntegrationErrorIndicator results={results} isStale={alertsRefetchError} />
<BeszelIntegrationErrorIndicator results={results} />
</Box>
<ScrollArea h="100%" style={{ pointerEvents: isEditMode ? "none" : undefined }}>
<Stack gap="sm" p="sm">
@@ -398,13 +398,11 @@ function BeszelSystemGridContent({
data: results = [],
error: systemsError,
isPending,
isLoadingError: systemsLoadingError,
isRefetchError: systemsRefetchError,
} = clientApi.widget.beszel.getSystems.useQuery({ integrationIds });
const filteredSystems = useBeszelFilteredSystems(results, options.statusFilter);
if (systemsLoadingError) throw systemsError;
if (systemsError) throw systemsError;
if (isPending) {
return (
@@ -417,7 +415,7 @@ function BeszelSystemGridContent({
if (filteredSystems.length === 0) {
return (
<Box h="100%" pos="relative" style={{ pointerEvents: isEditMode ? "none" : undefined }}>
<BeszelIntegrationErrorIndicator results={results} isStale={systemsRefetchError} />
<BeszelIntegrationErrorIndicator results={results} />
<Center h="100%">
<Stack align="center" gap="xs">
<IconServerOff size={28} opacity={0.5} />
@@ -441,7 +439,7 @@ function BeszelSystemGridContent({
return (
<Box h="100%" pos="relative" style={{ pointerEvents: isEditMode ? "none" : undefined }}>
<BeszelIntegrationErrorIndicator results={results} isStale={systemsRefetchError} />
<BeszelIntegrationErrorIndicator results={results} />
<Box
h="100%"
style={{
@@ -47,8 +47,6 @@ function BeszelSystemStatsContent({
data: systemsResult = [],
isPending: systemsPending,
error: systemsError,
isLoadingError: systemsLoadingError,
isRefetchError: systemsRefetchError,
} = clientApi.widget.beszel.getSystems.useQuery({ integrationIds });
const systems = useMemo(
@@ -80,7 +78,7 @@ function BeszelSystemStatsContent({
[setOptions, isReadOnly, hasChangeAccess, boardId, itemId, saveItemOptions],
);
if (systemsLoadingError) throw systemsError;
if (systemsError) throw systemsError;
if (systemsPending)
return (
<Center h="100%">
@@ -91,7 +89,7 @@ function BeszelSystemStatsContent({
if (systems.length === 0) {
return (
<Box h="100%" pos="relative">
<BeszelIntegrationErrorIndicator results={systemsResult} isStale={systemsRefetchError} />
<BeszelIntegrationErrorIndicator results={systemsResult} />
<Center h="100%">
<Stack align="center" gap="xs">
<IconServerOff size={28} opacity={0.5} />
@@ -127,7 +125,7 @@ function BeszelSystemStatsContent({
return (
<Box h="100%" pos="relative">
<Box pos="absolute" top={4} right={8} style={{ zIndex: 1 }}>
<BeszelIntegrationErrorIndicator results={systemsResult} isStale={systemsRefetchError} />
<BeszelIntegrationErrorIndicator results={systemsResult} />
</Box>
<ScrollArea
h="100%"
@@ -99,8 +99,6 @@ function BeszelSystemTableContent({
data: results = [],
error: systemsError,
isPending,
isLoadingError: systemsLoadingError,
isRefetchError: systemsRefetchError,
} = clientApi.widget.beszel.getSystems.useQuery({ integrationIds });
const size = getSizeConfig(width);
@@ -304,7 +302,7 @@ function BeszelSystemTableContent({
openModal({ integrationId, systemId: record.id }, { title: record.name });
};
if (systemsLoadingError) throw systemsError;
if (systemsError) throw systemsError;
if (isPending) {
return (
@@ -317,7 +315,7 @@ function BeszelSystemTableContent({
return (
<div style={{ position: "relative", height: "100%" }}>
<div style={{ position: "absolute", top: 4, right: 8, zIndex: 1 }}>
<BeszelIntegrationErrorIndicator results={results} isStale={systemsRefetchError} />
<BeszelIntegrationErrorIndicator results={results} />
</div>
<DataTable
style={{ pointerEvents: isEditMode ? "none" : undefined }}
@@ -3,8 +3,6 @@
import { Tooltip } from "@mantine/core";
import { IconAlertTriangle } from "@tabler/icons-react";
import { useI18n } from "@homarr/translation/client";
interface IntegrationResult {
integrationId: string;
integrationName?: string;
@@ -13,25 +11,15 @@ interface IntegrationResult {
interface BeszelIntegrationErrorIndicatorProps {
results: IntegrationResult[];
isStale?: boolean;
}
export function BeszelIntegrationErrorIndicator({ results, isStale = false }: BeszelIntegrationErrorIndicatorProps) {
const t = useI18n();
export function BeszelIntegrationErrorIndicator({ results }: BeszelIntegrationErrorIndicatorProps) {
const failed = results.filter((r) => "error" in r && r.error);
if (failed.length === 0 && !isStale) return null;
const label =
failed.length > 0
? failed.map((r) => r.integrationName ?? r.integrationId).join(", ")
: t("board.mobile.dataWarning");
if (failed.length === 0) return null;
const label = failed.map((r) => r.integrationName ?? r.integrationId).join(", ");
return (
<Tooltip label={label} position="left" withArrow>
<IconAlertTriangle
size={14}
color="var(--mantine-color-orange-6)"
style={{ cursor: "help", flexShrink: 0 }}
aria-label={label}
/>
<IconAlertTriangle size={14} color="var(--mantine-color-orange-6)" style={{ cursor: "help", flexShrink: 0 }} />
</Tooltip>
);
}
@@ -5,7 +5,6 @@ import { useI18n } from "@homarr/translation/client";
import { WidgetMobileLoading, WidgetMobileSummary } from "../../common/mobile-summary";
import { useBeszelFilteredSystems } from "./hooks";
import { hasStaleIntegrationData } from "./query-status";
interface BeszelSystemsMobileSummaryProps {
integrationIds: string[];
@@ -19,18 +18,12 @@ export const BeszelSystemsMobileSummary = ({
statusFilter,
}: BeszelSystemsMobileSummaryProps) => {
const t = useI18n();
const {
data: results = [],
error,
isPending,
isLoadingError,
isRefetchError,
} = clientApi.widget.beszel.getSystems.useQuery({ integrationIds });
const { data: results = [], error, isPending } = clientApi.widget.beszel.getSystems.useQuery({ integrationIds });
const systems = useBeszelFilteredSystems(results, statusFilter);
const failedResults = results.filter((result) => "error" in result);
if (isPending) return <WidgetMobileLoading />;
if (isLoadingError) throw error;
if (error) throw error;
if (results.length > 0 && failedResults.length === results.length) {
throw new Error(String(failedResults[0]?.error ?? "Unable to connect to Beszel"));
}
@@ -45,7 +38,7 @@ export const BeszelSystemsMobileSummary = ({
up,
attention: systems.length - up,
})}
isStale={hasStaleIntegrationData(isRefetchError, failedResults)}
isStale={failedResults.length > 0}
/>
);
};
@@ -1,11 +0,0 @@
import { describe, expect, test } from "vitest";
import { hasStaleIntegrationData } from "./query-status";
describe("Beszel query status", () => {
test("marks cached data stale after outer or per-integration failures", () => {
expect(hasStaleIntegrationData(true, [])).toBe(true);
expect(hasStaleIntegrationData(false, [{ error: "offline" }])).toBe(true);
expect(hasStaleIntegrationData(false, [])).toBe(false);
});
});
@@ -1,2 +0,0 @@
export const hasStaleIntegrationData = (isRefetchError: boolean, failedResults: readonly unknown[]) =>
isRefetchError || failedResults.length > 0;
@@ -1,7 +1,7 @@
"use client";
import { useMemo } from "react";
import { Box, Button, Center, Group, SimpleGrid, Skeleton, Stack, Text } from "@mantine/core";
import { Button, Center, Group, SimpleGrid, Skeleton, Stack, Text } from "@mantine/core";
import { IconPlugConnectedX, IconServerOff } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
@@ -19,7 +19,6 @@ import {
useSystemChartData,
} from "./chart";
import { chartAxisFormatters, formatByteRate, formatGB, formatPercent, formatStorageBytes } from "./format";
import { BeszelIntegrationErrorIndicator } from "./error-indicator";
import { makeTooltipProps } from "./tooltip";
import { useLiveStats } from "./use-live-stats";
@@ -75,12 +74,7 @@ export function BeszelStatsView({
const showDocker = visibility.dockerCpu || visibility.dockerMemory || visibility.dockerNetwork;
const isLive = timePeriod === "1m";
const {
data: historicalData,
error: historicalError,
isLoadingError: historicalLoadingError,
isRefetchError: historicalRefetchError,
} = clientApi.widget.beszel.getSystemStats.useQuery(
const { data: historicalData, error: historicalError } = clientApi.widget.beszel.getSystemStats.useQuery(
{ integrationIds, systemId, timePeriod, includeDocker: showDocker },
{ refetchInterval: isLive ? false : 5_000, enabled: !isLive && systemId !== "" },
);
@@ -181,7 +175,7 @@ export function BeszelStatsView({
timePeriod,
);
if (!isLive && historicalLoadingError) throw historicalError;
if (historicalError) throw historicalError;
if (isLive && liveError) {
return (
@@ -226,10 +220,7 @@ export function BeszelStatsView({
}
return (
<SimpleGrid cols={columns} spacing="md" pos="relative">
<Box pos="absolute" top={0} right={0} style={{ zIndex: 1 }}>
<BeszelIntegrationErrorIndicator results={[]} isStale={historicalRefetchError} />
</Box>
<SimpleGrid cols={columns} spacing="md">
{visibility.cpu && cpuData.length > 0 && (
<BeszelChartPanel
title={t("chart.cpu.title")}
@@ -3,7 +3,6 @@ import { describe, expect, test } from "vitest";
import {
filterStorageVolumes,
normalizeStorageDeviceName,
storageDeviceNamesMatch,
toScopedStorageVolumeValue,
} from "./filter-storage-volumes";
import { matchFileSystemAndSmart } from "./health-monitoring/system-health";
@@ -31,14 +30,6 @@ describe("normalizeStorageDeviceName", () => {
});
});
describe("storageDeviceNamesMatch", () => {
test("matches partitions to their base SMART device without merging unrelated devices", () => {
expect(storageDeviceNamesMatch("/dev/sda1", "/dev/sda")).toBe(true);
expect(storageDeviceNamesMatch("/dev/nvme0n1p2", "/dev/nvme0n1")).toBe(true);
expect(storageDeviceNamesMatch("/dev/md0", "/dev/md1")).toBe(false);
});
});
describe("toScopedStorageVolumeValue", () => {
test("scopes unscoped volume names to the query integration", () => {
expect(toScopedStorageVolumeValue("nas-a", "volume_1")).toBe("nas-a:volume_1");
@@ -28,14 +28,18 @@ export const toScopedStorageVolumeValue = (integrationId: string, value: string)
return `${integrationId}:${volumeName}`;
};
export const storageDeviceNamesMatch = (leftDeviceName: string, rightDeviceName: string): boolean => {
const storageDeviceNamesMatch = (leftDeviceName: string, rightDeviceName: string): boolean => {
return (
leftDeviceName === rightDeviceName ||
normalizeStorageDeviceName(leftDeviceName) === normalizeStorageDeviceName(rightDeviceName)
);
};
const matchesVisibleStorageVolume = (visibleVolume: string, integrationId: string, deviceName: string): boolean => {
const matchesVisibleStorageVolume = (
visibleVolume: string,
integrationId: string,
deviceName: string,
): boolean => {
const separatorIndex = visibleVolume.indexOf(":");
if (separatorIndex === -1) {
return storageDeviceNamesMatch(visibleVolume, deviceName);
@@ -10,10 +10,9 @@ import { useI18n } from "@homarr/translation/client";
import { WidgetEmptyState } from "../common/empty-state";
import { WidgetMobileLoading, WidgetMobileSummary } from "../common/mobile-summary";
import { NoIntegrationDataError } from "../errors/no-data-integration";
import { filterStorageVolumes, storageDeviceNamesMatch } from "../filter-storage-volumes";
import type { WidgetComponentProps } from "../definition";
import { formatDiskTemperature } from "./mobile-summary";
import { filterStorageVolumes } from "../filter-storage-volumes";
import { NoIntegrationDataError } from "../errors/no-data-integration";
type DisplayMode = WidgetComponentProps<"systemDisks">["options"]["displayMode"];
@@ -82,7 +81,6 @@ const SystemDiskCard = ({
}, [displayText, healthy]);
const unhealthyLabel = t("widget.systemDisks.status.unhealthy");
const temperatureLabel = formatDiskTemperature(temperature, true);
return (
<Tooltip
@@ -107,7 +105,7 @@ const SystemDiskCard = ({
{!healthy && <span style={{ marginLeft: 5 }}>{unhealthyLabel}</span>}
</p>
</div>
<div>{temperatureLabel ? <p style={{ margin: 0 }}>{temperatureLabel}</p> : null}</div>
<div>{temperature ? <p style={{ margin: 0 }}>{temperature}°C</p> : null}</div>
</Group>
<Box
bg={healthy ? "green" : "red"}
@@ -157,9 +155,10 @@ export default function SystemResources({
const disk = fileSystem.toSorted((diskA, diskB) => diskB.percentage - diskA.percentage)[0];
if (!disk) return <WidgetEmptyState />;
const smartItem = smart.find((item) => storageDeviceNamesMatch(item.deviceName, disk.deviceName));
const smartItem = smart.find((item) => item.deviceName === disk.deviceName);
const isHealthy = smartItem?.healthy ?? true;
const temperature = formatDiskTemperature(smartItem?.temperature, options.showTemperatureIfAvailable);
const temperature =
options.showTemperatureIfAvailable && smartItem?.temperature ? `${smartItem.temperature}°C` : undefined;
return (
<WidgetMobileSummary
@@ -174,7 +173,7 @@ export default function SystemResources({
return (
<Stack gap="xs" p="xs" h="100%">
{fileSystem.map((item) => {
const smartItem = smart.find((smartEntry) => storageDeviceNamesMatch(smartEntry.deviceName, item.deviceName));
const smartItem = smart.find((smartEntry) => smartEntry.deviceName === item.deviceName);
return (
<SystemDiskCard
@@ -1,15 +0,0 @@
import { describe, expect, test } from "vitest";
import { formatDiskTemperature } from "./mobile-summary";
describe("formatDiskTemperature", () => {
test("preserves a valid zero-degree reading", () => {
expect(formatDiskTemperature(0, true)).toBe("0°C");
});
test("omits unavailable or disabled readings", () => {
expect(formatDiskTemperature(null, true)).toBeUndefined();
expect(formatDiskTemperature(undefined, true)).toBeUndefined();
expect(formatDiskTemperature(20, false)).toBeUndefined();
});
});
@@ -1,2 +0,0 @@
export const formatDiskTemperature = (temperature: number | null | undefined, shouldShow: boolean) =>
shouldShow && temperature != null ? `${temperature}°C` : undefined;
+7 -1
View File
@@ -14,7 +14,6 @@ import { WidgetEmptyState } from "../common/empty-state";
import type { WidgetComponentProps } from "../definition";
import { AnimatedWeatherIcon } from "./animated-icon";
import { WeatherDescription } from "./icon";
import { getPreferredUnit } from "./temperature";
export default function WeatherWidget({ isEditMode, options }: WidgetComponentProps<"weather">) {
const input = {
@@ -248,3 +247,10 @@ function Forecast({ weather, options }: WeatherProps) {
</Group>
);
}
const getPreferredUnit = (value?: number, isFahrenheit = false, disableTemperatureDecimals = false): string =>
value
? isFahrenheit
? `${(value * (9 / 5) + 32).toFixed(disableTemperatureDecimals ? 0 : 1)}°F`
: `${value.toFixed(disableTemperatureDecimals ? 0 : 1)}°C`
: "?";
@@ -1,20 +0,0 @@
import { describe, expect, test } from "vitest";
import { getPreferredUnit } from "./temperature";
describe("getPreferredUnit", () => {
test("preserves zero in Celsius and Fahrenheit", () => {
expect(getPreferredUnit(0)).toBe("0.0°C");
expect(getPreferredUnit(0, true)).toBe("32.0°F");
});
test("only uses the unavailable placeholder for missing values", () => {
expect(getPreferredUnit(undefined)).toBe("?");
});
test("respects the decimal preference", () => {
expect(getPreferredUnit(0, false, true)).toBe("0°C");
expect(getPreferredUnit(0, true, true)).toBe("32°F");
expect(getPreferredUnit(12.6, false, true)).toBe("13°C");
});
});
@@ -1,6 +0,0 @@
export const getPreferredUnit = (value?: number, isFahrenheit = false, disableTemperatureDecimals = false): string =>
value !== undefined
? isFahrenheit
? `${(value * (9 / 5) + 32).toFixed(disableTemperatureDecimals ? 0 : 1)}°F`
: `${value.toFixed(disableTemperatureDecimals ? 0 : 1)}°C`
: "?";