diff --git a/e2e/custom-widgets-custom-jsx-v2.spec.ts b/e2e/custom-widgets-custom-jsx-v2.spec.ts index a88ca1944..fbfdcc7d2 100644 --- a/e2e/custom-widgets-custom-jsx-v2.spec.ts +++ b/e2e/custom-widgets-custom-jsx-v2.spec.ts @@ -36,7 +36,10 @@ describe("Custom JSX v2 workbench", () => { await page.waitForLoadState("networkidle"); await page.getByLabel("Username").fill(adminCredentials.username); await page.locator("#password").fill(adminCredentials.password); - const signedIn = page.waitForURL(baseUrl, { waitUntil: "commit", timeout: 15_000 }); + const signedIn = page.waitForURL((url) => url.origin === baseUrl && url.pathname !== "/auth/login", { + waitUntil: "commit", + timeout: 60_000, + }); await page.locator("button[type='submit']").click(); await signedIn; diff --git a/packages/integrations/src/synology/synology-client.ts b/packages/integrations/src/synology/synology-client.ts index cc853e2c9..87450cc2f 100644 --- a/packages/integrations/src/synology/synology-client.ts +++ b/packages/integrations/src/synology/synology-client.ts @@ -85,6 +85,11 @@ export class SynologyClient { if (volumesFromV2.length > 0) { return volumesFromV2; } + + const legacyVolumesFromV2 = this.mapLegacyVolumes(storageV2.vol_info); + if (legacyVolumesFromV2.length > 0) { + return legacyVolumesFromV2; + } } catch (error) { logger.debug("storage_v2 unavailable, falling back to legacy storage info", { integrationId: this.integrationId, diff --git a/packages/integrations/src/synology/synology-types.ts b/packages/integrations/src/synology/synology-types.ts index c1f73b52e..0d3a13163 100644 --- a/packages/integrations/src/synology/synology-types.ts +++ b/packages/integrations/src/synology/synology-types.ts @@ -78,6 +78,7 @@ export const synologyStorageV2VolumeSchema = z export const synologyStorageV2DataSchema = z.object({ volumes: z.array(synologyStorageV2VolumeSchema).optional(), + vol_info: z.array(synologyVolumeInfoSchema).optional(), }); export const synologyUtilizationCpuSchema = z.object({ diff --git a/packages/integrations/src/synology/test/synology-integration.spec.ts b/packages/integrations/src/synology/test/synology-integration.spec.ts index e8a13f015..60e334fe4 100644 --- a/packages/integrations/src/synology/test/synology-integration.spec.ts +++ b/packages/integrations/src/synology/test/synology-integration.spec.ts @@ -426,6 +426,60 @@ describe("SynologyIntegration.getSystemInfoAsync", () => { expect(result.fileSystem[0]?.deviceName).toBe("volume_1"); }); + test("uses vol_info from storage_v2 without falling back to legacy storage", async () => { + mockSuccessfulFlow({ + storageV2: { + success: true, + data: { + vol_info: [ + { + name: "volume_1", + used_size: "4000000000", + total_size: "10000000000", + status: "normal", + }, + { + name: "volume_2", + used_size: "5000000000", + total_size: "20000000000", + status: "attention", + }, + ], + }, + }, + }); + + const integration = createIntegration(); + const result = await integration.getSystemInfoAsync(); + + expect(result.fileSystem).toEqual([ + { + deviceName: "volume_1", + used: "4000000000", + available: "6000000000", + percentage: 40, + }, + { + deviceName: "volume_2", + used: "5000000000", + available: "15000000000", + percentage: 25, + }, + ]); + + const legacyStorageRequests = mockFetch.mock.calls.filter(([url]) => { + const urlString = toUrlString(url); + + return ( + getApiFromUrl(urlString) === "SYNO.Core.System" && + getMethodFromUrl(urlString) === "info" && + getTypeFromUrl(urlString) === "storage" + ); + }); + + expect(legacyStorageRequests).toHaveLength(0); + }); + test("continues when optional tier-2 calls fail", async () => { mockSuccessfulFlow({ skipOptional: true }); const integration = createIntegration(); diff --git a/packages/widgets/src/beszel/_shared/tooltip.tsx b/packages/widgets/src/beszel/_shared/tooltip.tsx index b43ea1765..b72403fe5 100644 --- a/packages/widgets/src/beszel/_shared/tooltip.tsx +++ b/packages/widgets/src/beszel/_shared/tooltip.tsx @@ -1,8 +1,6 @@ "use client"; import type { CSSProperties } from "react"; -import { createPortal } from "react-dom"; -import { useCallback, useEffect, useRef, useState } from "react"; import dayjs from "dayjs"; import localizedFormat from "dayjs/plugin/localizedFormat"; @@ -47,7 +45,7 @@ export interface TooltipPayloadItem { payload?: Record; } -interface PortalTooltipProps { +interface BeszelTooltipProps { active?: boolean; label?: string; payload?: TooltipPayloadItem[]; @@ -55,53 +53,8 @@ interface PortalTooltipProps { showTotal?: boolean; } -const MARGIN = 14; - -const PortalTooltipContent = ({ active, label, payload, formatter, showTotal }: PortalTooltipProps) => { - const mouseRef = useRef({ x: 0, y: 0 }); - const [pos, setPos] = useState({ x: 0, y: 0 }); - const rafRef = useRef(0); - const tooltipRef = useRef(null); - - const handleMouse = useCallback((e: MouseEvent) => { - mouseRef.current = { x: e.clientX, y: e.clientY }; - cancelAnimationFrame(rafRef.current); - rafRef.current = requestAnimationFrame(() => { - const el = tooltipRef.current; - const mx = mouseRef.current.x; - const my = mouseRef.current.y; - - if (!el) { - setPos({ x: mx + MARGIN, y: my }); - return; - } - - const { width, height } = el.getBoundingClientRect(); - const vw = window.innerWidth; - const vh = window.innerHeight; - - let x = mx + MARGIN; - let y = my - height / 2; - - if (x + width > vw - MARGIN) x = mx - width - MARGIN; - if (y < MARGIN) y = MARGIN; - if (y + height > vh - MARGIN) y = vh - MARGIN - height; - - setPos({ x, y }); - }); - }, []); - +const BeszelTooltipContent = ({ active, label, payload, formatter, showTotal }: BeszelTooltipProps) => { const isActive = active && (payload?.length ?? 0) > 0; - - useEffect(() => { - if (!isActive) return; - document.addEventListener("mousemove", handleMouse); - return () => { - document.removeEventListener("mousemove", handleMouse); - cancelAnimationFrame(rafRef.current); - }; - }, [isActive, handleMouse]); - if (!isActive) return null; const deduped = new Map(); @@ -116,41 +69,30 @@ const PortalTooltipContent = ({ active, label, payload, formatter, showTotal }: const tooltipLabel = rawTime ? dayjs(rawTime).format("MMM D, LT") : label; const total = sorted.reduce((sum, p) => sum + p.value, 0); - const portalStyle: CSSProperties = { - position: "fixed", - left: pos.x, - top: pos.y, - zIndex: 10000, - pointerEvents: "none", - }; - - return createPortal( -
-
-
{tooltipLabel}
- {sorted.map((item) => ( -
-
- {item.name} - {formatter(item.value)} -
- ))} - {showTotal && sorted.length > 1 && ( -
- Total - {formatter(total)} -
- )} -
-
, - document.body, + return ( +
+
{tooltipLabel}
+ {sorted.map((item) => ( +
+
+ {item.name} + {formatter(item.value)} +
+ ))} + {showTotal && sorted.length > 1 && ( +
+ Total + {formatter(total)} +
+ )} +
); }; // eslint-disable-next-line @typescript-eslint/no-explicit-any export const makeTooltipProps = (formatter: (v: number) => string, showTotal = false) => ({ content: (props: any) => ( - string, showTotal = f showTotal={showTotal} /> ), - wrapperStyle: { visibility: "hidden" as const, position: "absolute" as const, width: 0, height: 0 }, + wrapperStyle: { zIndex: 10000, pointerEvents: "none" as const }, isAnimationActive: false, });