chore: reconcile widget layer after stack rebase
This commit is contained in:
1 parent
09b82ba171
commit
bd4bbcaca2
23 files changed
+101
-814
No files matched your search
@@ -3,6 +3,7 @@
|
||||
import { useCallback, useMemo, useRef } from "react";
|
||||
import { Box, Button, Center, Group, Loader, Menu, ScrollArea, Select, Stack, Text } from "@mantine/core";
|
||||
import { IconQuestionMark, IconServer, IconServerOff } from "@tabler/icons-react";
|
||||
import { getQueryKey } from "@trpc/react-query";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
@@ -68,15 +69,16 @@ export default function BeszelSystemStatsWidget({
|
||||
widgetRuntimeRef,
|
||||
selectedSystem && options.timePeriod !== "1m"
|
||||
? [
|
||||
{
|
||||
path: ["widget", "beszel", "getSystemStats"],
|
||||
input: {
|
||||
getQueryKey(
|
||||
clientApi.widget.beszel.getSystemStats,
|
||||
{
|
||||
integrationIds: [selectedSystem.integrationId],
|
||||
systemId: selectedSystem.systemId,
|
||||
timePeriod: options.timePeriod,
|
||||
timePeriod: options.timePeriod as BeszelTimePeriod,
|
||||
includeDocker,
|
||||
},
|
||||
},
|
||||
"query",
|
||||
),
|
||||
]
|
||||
: [],
|
||||
);
|
||||
|
||||
@@ -20,22 +20,17 @@ import {
|
||||
useSystemChartData,
|
||||
} from "./chart";
|
||||
import { chartAxisFormatters, formatByteRate, formatGB, formatPercent, formatStorageBytes } from "./format";
|
||||
import type { BeszelTooltipLayer } from "./tooltip";
|
||||
import { makeTooltipProps } from "./tooltip";
|
||||
import { useLiveStats } from "./use-live-stats";
|
||||
|
||||
const CHART_HEIGHT = 180;
|
||||
const MB = 1024 * 1024;
|
||||
const makeTooltipSet = (layer: BeszelTooltipLayer) => ({
|
||||
percent: makeTooltipProps(formatPercent, false, layer),
|
||||
gigabytes: makeTooltipProps(formatGB, true, layer),
|
||||
rate: makeTooltipProps(formatByteRate, true, layer),
|
||||
percentTotal: makeTooltipProps(formatPercent, true, layer),
|
||||
bytesTotal: makeTooltipProps(formatStorageBytes, true, layer),
|
||||
});
|
||||
const tooltipSets = {
|
||||
background: makeTooltipSet("background"),
|
||||
modal: makeTooltipSet("modal"),
|
||||
const tooltips = {
|
||||
percent: makeTooltipProps(formatPercent),
|
||||
gigabytes: makeTooltipProps(formatGB, true),
|
||||
rate: makeTooltipProps(formatByteRate, true),
|
||||
percentTotal: makeTooltipProps(formatPercent, true),
|
||||
bytesTotal: makeTooltipProps(formatStorageBytes, true),
|
||||
};
|
||||
|
||||
const ChartSkeleton = () => (
|
||||
@@ -66,7 +61,6 @@ interface BeszelStatsViewProps {
|
||||
visibility: BeszelStatsVisibility;
|
||||
columns: 1 | 2;
|
||||
onSwitchToHistorical?: () => void;
|
||||
tooltipLayer?: BeszelTooltipLayer;
|
||||
}
|
||||
|
||||
const whenVisible = <T,>(visible: boolean, data: T | undefined) => (visible ? data : undefined);
|
||||
@@ -78,14 +72,11 @@ export function BeszelStatsView({
|
||||
visibility,
|
||||
columns,
|
||||
onSwitchToHistorical,
|
||||
tooltipLayer = "background",
|
||||
}: BeszelStatsViewProps) {
|
||||
const t = useScopedI18n("widget.beszelSystemStats");
|
||||
const locale = useCurrentIntlLocale();
|
||||
const showDocker = visibility.dockerCpu || visibility.dockerMemory || visibility.dockerNetwork;
|
||||
const isLive = timePeriod === "1m";
|
||||
const tooltips = tooltipSets[tooltipLayer];
|
||||
|
||||
const historicalQuery = clientApi.widget.beszel.getSystemStats.useQuery(
|
||||
{ integrationIds, systemId, timePeriod, includeDocker: showDocker },
|
||||
{ refetchInterval: isLive ? false : 5_000, enabled: !isLive && systemId !== "" },
|
||||
|
||||
@@ -50,7 +50,6 @@ export const BeszelSystemStatsModal = createModal<BeszelSystemStatsModalProps>((
|
||||
timePeriod={timePeriod}
|
||||
visibility={allCharts}
|
||||
columns={2}
|
||||
tooltipLayer="modal"
|
||||
onSwitchToHistorical={() => setTimePeriod("1h")}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
@@ -40,15 +40,3 @@ export function filterDownloadItemsByStatus<T extends { state: DownloadState }>(
|
||||
export function getAvailableDownloadStates(items: readonly { state: DownloadState }[]): DownloadState[] {
|
||||
return [...new Set(items.map(({ state }) => state))];
|
||||
}
|
||||
|
||||
export function columnWidthsToRecord(columnsWidth: readonly Record<string, string | number>[]): Record<string, number> {
|
||||
const result: Record<string, number> = {};
|
||||
for (const entry of columnsWidth) {
|
||||
const key = Object.keys(entry)[0];
|
||||
if (!key) continue;
|
||||
const width = entry[key];
|
||||
if (typeof width === "number") result[key] = width;
|
||||
else if (typeof width === "string" && width.endsWith("px")) result[key] = parseInt(width, 10);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -46,7 +46,6 @@ describe("WidgetError", () => {
|
||||
MantineProvider,
|
||||
null,
|
||||
createElement(WidgetError, {
|
||||
kind: "app",
|
||||
error: new Error(secret),
|
||||
resetErrorBoundary: vi.fn(),
|
||||
}),
|
||||
|
||||
@@ -37,7 +37,7 @@ interface FirewallQueryState {
|
||||
isPending: boolean;
|
||||
isFetching: boolean;
|
||||
isError: boolean;
|
||||
data?: readonly { error?: string }[];
|
||||
data?: readonly { error?: string; integration?: { id: string } }[];
|
||||
}
|
||||
|
||||
export const hasTotalFirewallFailure = (queries: readonly FirewallQueryState[]) =>
|
||||
@@ -48,6 +48,13 @@ export const hasTotalFirewallFailure = (queries: readonly FirewallQueryState[])
|
||||
(query.data !== undefined && query.data.length > 0 && query.data.every(({ error }) => error)),
|
||||
);
|
||||
|
||||
export const hasFirewallPartialFailure = (firewallId: string, queries: readonly FirewallQueryState[]) =>
|
||||
queries.some(
|
||||
(query) =>
|
||||
query.isError ||
|
||||
query.data?.some(({ error, integration }) => integration?.id === firewallId && Boolean(error)) === true,
|
||||
);
|
||||
|
||||
export default function FirewallWidget({
|
||||
integrationIds,
|
||||
width,
|
||||
@@ -101,6 +108,7 @@ export default function FirewallWidget({
|
||||
const initialSelectedFirewall = firewallIds[0] ?? "";
|
||||
const activeFirewall = firewallMetadata.has(selectedFirewall) ? selectedFirewall : initialSelectedFirewall;
|
||||
const displayedFirewallIds = isAdvanced ? firewallIds : activeFirewall ? [activeFirewall] : [];
|
||||
const firewallHasError = (firewallId: string) => hasFirewallPartialFailure(firewallId, queries);
|
||||
const dropdownItems = firewallIds.map((firewallId) => ({
|
||||
label: firewallMetadata.get(firewallId)?.name ?? firewallId,
|
||||
value: firewallId,
|
||||
@@ -128,11 +136,20 @@ export default function FirewallWidget({
|
||||
dropdownItems={dropdownItems}
|
||||
isTiny={isTiny}
|
||||
/>
|
||||
<FirewallVersion
|
||||
firewallsVersionData={firewallsVersionData.filter(({ error }) => !error)}
|
||||
selectedFirewall={activeFirewall}
|
||||
isTiny={isTiny}
|
||||
/>
|
||||
<Group gap={4} wrap="nowrap">
|
||||
{activeFirewall && firewallHasError(activeFirewall) && (
|
||||
<Tooltip label={t("widget.firewall.error.internalServerError")}>
|
||||
<Badge color="red" variant="light" size="xs">
|
||||
{t("common.error")}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
<FirewallVersion
|
||||
firewallsVersionData={firewallsVersionData.filter(({ error }) => !error)}
|
||||
selectedFirewall={activeFirewall}
|
||||
isTiny={isTiny}
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
@@ -142,9 +159,7 @@ export default function FirewallWidget({
|
||||
const memory = firewallsMemoryData.find(({ integration }) => integration.id === firewallId);
|
||||
const version = firewallsVersionData.find(({ integration }) => integration.id === firewallId);
|
||||
const interfaces = firewallsInterfacesData.find(({ integration }) => integration.id === firewallId);
|
||||
const hasError = Boolean(
|
||||
cpu?.error || memory?.error || version?.error || interfacesQuery.isError || interfaces?.error,
|
||||
);
|
||||
const hasError = firewallHasError(firewallId);
|
||||
const metadata = firewallMetadata.get(firewallId);
|
||||
|
||||
return (
|
||||
|
||||
@@ -13,12 +13,19 @@ const mocks = vi.hoisted(() => ({
|
||||
getAlbums: vi.fn(() => ({ data: [], isPending: false, isError: false })),
|
||||
}));
|
||||
|
||||
vi.mock("@trpc/react-query", () => ({
|
||||
getQueryKey: (procedure: { path: string[] }, input: unknown) => [procedure.path, { input, type: "query" }],
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/api/client", () => ({
|
||||
clientApi: {
|
||||
widget: {
|
||||
immich: {
|
||||
getAlbums: { useQuery: mocks.getAlbums },
|
||||
getServerStats: { useQuery: () => ({ data: undefined }) },
|
||||
getAlbums: { path: ["widget", "immich", "getAlbums"], useQuery: mocks.getAlbums },
|
||||
getServerStats: {
|
||||
path: ["widget", "immich", "getServerStats"],
|
||||
useQuery: () => ({ data: undefined }),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from "./manifest";
|
||||
import { widgetQueryRefetchIntervals } from "./refetch-intervals";
|
||||
import { widgetCatalogIcons } from "./catalog";
|
||||
import { getWidgetQueryKeys } from "./definition";
|
||||
|
||||
describe("widget manifest promise stability", () => {
|
||||
it("returns the same module promise for every render", () => {
|
||||
@@ -111,11 +112,11 @@ describe("widget manifest promise stability", () => {
|
||||
const definitions = await loadAllWidgetDefinitions();
|
||||
const expectedQueryKeys = new Map([
|
||||
["anchorNote", [["widget", "anchorNotes"]]],
|
||||
["beszelAlerts", [["widget", "beszel"]]],
|
||||
["clock", [["widget", "weather"]]],
|
||||
["mediaMissing", [["widget", "mediaOrganizer"]]],
|
||||
["mediaRequests-requestList", [["widget", "mediaRequests"]]],
|
||||
["mediaRequests-requestStats", [["widget", "mediaRequests"]]],
|
||||
["beszelAlerts", [["widget", "beszel", "getAlerts"]]],
|
||||
["clock", [["widget", "weather", "atLocation"]]],
|
||||
["mediaMissing", [["widget", "mediaOrganizer", "getData"]]],
|
||||
["mediaRequests-requestList", [["widget", "mediaRequests", "getLatestRequests"]]],
|
||||
["mediaRequests-requestStats", [["widget", "mediaRequests", "getStats"]]],
|
||||
["smartHome-entityState", [["widget", "smartHome"]]],
|
||||
] as const);
|
||||
|
||||
@@ -133,14 +134,15 @@ describe("widget manifest promise stability", () => {
|
||||
const definition = definitions.get(kind);
|
||||
if (definition?.refetchInterval === undefined) continue;
|
||||
|
||||
const entry = {
|
||||
queryKey: definition.queryKey ?? [["widget", kind]],
|
||||
intervalSeconds: definition.refetchInterval,
|
||||
} as const;
|
||||
const serializedQueryKey = JSON.stringify(entry.queryKey);
|
||||
const existing = expectedByQueryKey.get(serializedQueryKey);
|
||||
expect(existing?.intervalSeconds ?? entry.intervalSeconds).toBe(entry.intervalSeconds);
|
||||
expectedByQueryKey.set(serializedQueryKey, entry);
|
||||
for (const queryKey of getWidgetQueryKeys(definition, kind)) {
|
||||
const path = queryKey[0];
|
||||
if (!Array.isArray(path) || (path[0] !== "widget" && path[0] !== "docker")) continue;
|
||||
const entry = { queryKey, intervalSeconds: definition.refetchInterval } as const;
|
||||
const serializedQueryKey = JSON.stringify(entry.queryKey);
|
||||
const existing = expectedByQueryKey.get(serializedQueryKey);
|
||||
expect(existing?.intervalSeconds ?? entry.intervalSeconds).toBe(entry.intervalSeconds);
|
||||
expectedByQueryKey.set(serializedQueryKey, entry);
|
||||
}
|
||||
}
|
||||
|
||||
const serializePolicy = (entry: PollingPolicy) => JSON.stringify(entry);
|
||||
|
||||
@@ -8,23 +8,29 @@
|
||||
export const widgetQueryRefetchIntervals = [
|
||||
{ queryKey: [["docker", "getContainers"]], intervalSeconds: 30 },
|
||||
{ queryKey: [["widget", "dnsHole"]], intervalSeconds: 5 },
|
||||
{ queryKey: [["widget", "downloads"]], intervalSeconds: 5 },
|
||||
{ queryKey: [["widget", "downloads", "getJobsAndStatuses"]], intervalSeconds: 5 },
|
||||
{ queryKey: [["widget", "firewall"]], intervalSeconds: 5 },
|
||||
{ queryKey: [["widget", "healthMonitoring"]], intervalSeconds: 5 },
|
||||
{ queryKey: [["widget", "mediaServer"]], intervalSeconds: 5 },
|
||||
{ queryKey: [["widget", "mediaServer", "getCurrentStreams"]], intervalSeconds: 5 },
|
||||
{ queryKey: [["widget", "tracearr"]], intervalSeconds: 5 },
|
||||
{ queryKey: [["widget", "immich"]], intervalSeconds: null },
|
||||
{ queryKey: [["widget", "immich", "getAlbum"]], intervalSeconds: null },
|
||||
{ queryKey: [["widget", "immich", "getServerStats"]], intervalSeconds: null },
|
||||
{ queryKey: [["widget", "immich", "getAlbums"]], intervalSeconds: null },
|
||||
{ queryKey: [["widget", "indexerManager"]], intervalSeconds: null },
|
||||
{ queryKey: [["widget", "mediaRelease"]], intervalSeconds: null },
|
||||
{ queryKey: [["widget", "mediaTranscoding"]], intervalSeconds: null },
|
||||
{ queryKey: [["widget", "minecraft"]], intervalSeconds: null },
|
||||
{ queryKey: [["widget", "mediaTranscoding", "getDataAsync"]], intervalSeconds: null },
|
||||
{ queryKey: [["widget", "minecraft", "getServerStatus"]], intervalSeconds: null },
|
||||
{ queryKey: [["widget", "networkController"]], intervalSeconds: null },
|
||||
{ queryKey: [["widget", "paperlessNgx"]], intervalSeconds: null },
|
||||
{ queryKey: [["widget", "releases"]], intervalSeconds: null },
|
||||
{ queryKey: [["widget", "rssFeed"]], intervalSeconds: null },
|
||||
{ queryKey: [["widget", "releases", "getLatest"]], intervalSeconds: null },
|
||||
{ queryKey: [["widget", "rssFeed", "getFeeds"]], intervalSeconds: null },
|
||||
{ queryKey: [["widget", "speedtestTracker"]], intervalSeconds: null },
|
||||
{ queryKey: [["widget", "stockPrice"]], intervalSeconds: null },
|
||||
{ queryKey: [["widget", "umami"]], intervalSeconds: null },
|
||||
{ queryKey: [["widget", "stockPrice", "getPriceHistory"]], intervalSeconds: null },
|
||||
{ queryKey: [["widget", "umami", "getVisitorStats"]], intervalSeconds: null },
|
||||
{ queryKey: [["widget", "umami", "getActiveVisitors"]], intervalSeconds: null },
|
||||
{ queryKey: [["widget", "umami", "getMultiEventTimeSeries"]], intervalSeconds: null },
|
||||
{ queryKey: [["widget", "umami", "getTopPages"]], intervalSeconds: null },
|
||||
{ queryKey: [["widget", "umami", "getTopReferrers"]], intervalSeconds: null },
|
||||
{ queryKey: [["widget", "vpn"]], intervalSeconds: null },
|
||||
{ queryKey: [["widget", "wud"]], intervalSeconds: null },
|
||||
] as const;
|
||||
Reference in new issue
Block a user