Revert "fix: address mobile dashboard review findings"
This reverts commit 1039bb18b3.
This commit is contained in:
1 parent
43e95ff185
commit
38a710efe3
21 files changed
+45
-175
No files matched your search
@@ -40,7 +40,7 @@ The column count defines how many horizontal columns are available to drag and d
|
||||
### Public
|
||||
|
||||
This will enable public access for everyone - regardless of whether they are logged in or not.
|
||||
This can be really useful if you want to have public boards in your company or for family and friends.
|
||||
This can be really useful if you want to have public boards in your company or for family and fiends.
|
||||
Modifications for public boards must be allowed explicitly using user groups and their permissions.
|
||||
|
||||
## Board settings
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ export const beszelRouter = createTRPCRouter({
|
||||
};
|
||||
},
|
||||
{
|
||||
throwOnAllFailure: true,
|
||||
fallback: (integration, error) => ({
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
@@ -120,7 +119,6 @@ export const beszelRouter = createTRPCRouter({
|
||||
};
|
||||
},
|
||||
{
|
||||
throwOnAllFailure: true,
|
||||
fallback: (integration, error) => ({
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
|
||||
@@ -23,22 +23,6 @@ describe("settleIntegrationQueries", () => {
|
||||
expect(results).toEqual(["Second"]);
|
||||
});
|
||||
|
||||
it("retains fallback and successful results for a partial failure", async () => {
|
||||
const results = await settleIntegrationQueries(
|
||||
integrations,
|
||||
vi.fn(async (integration) => {
|
||||
if (integration.id === "first") throw new Error("offline");
|
||||
return integration.name;
|
||||
}),
|
||||
{
|
||||
throwOnAllFailure: true,
|
||||
fallback: (integration) => `${integration.name} unavailable`,
|
||||
},
|
||||
);
|
||||
|
||||
expect(results).toEqual(["First unavailable", "Second"]);
|
||||
});
|
||||
|
||||
it("throws when every integration fails and throwOnAllFailure is enabled", async () => {
|
||||
await expect(
|
||||
settleIntegrationQueries(
|
||||
@@ -51,21 +35,6 @@ describe("settleIntegrationQueries", () => {
|
||||
).rejects.toThrow("offline");
|
||||
});
|
||||
|
||||
it("throws on total failure even when fallbacks are configured", async () => {
|
||||
await expect(
|
||||
settleIntegrationQueries(
|
||||
integrations,
|
||||
vi.fn(async () => {
|
||||
throw new Error("offline");
|
||||
}),
|
||||
{
|
||||
throwOnAllFailure: true,
|
||||
fallback: (integration) => `${integration.name} unavailable`,
|
||||
},
|
||||
),
|
||||
).rejects.toThrow("offline");
|
||||
});
|
||||
|
||||
it("does not treat an empty integration list as a failure", async () => {
|
||||
await expect(
|
||||
settleIntegrationQueries(
|
||||
|
||||
@@ -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;
|
||||
@@ -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`
|
||||
: "?";
|
||||
Reference in new issue
Block a user