From 411e75e7993545c987c38bc76cb5a0f4bbd8ba71 Mon Sep 17 00:00:00 2001 From: ajnart Date: Thu, 27 Aug 2026 22:41:23 +0200 Subject: [PATCH 1/6] fix(cli): make demo mode writable --- tools/homarr-dev/internal/run/run.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/homarr-dev/internal/run/run.go b/tools/homarr-dev/internal/run/run.go index a144ee3df..7afde7cd8 100644 --- a/tools/homarr-dev/internal/run/run.go +++ b/tools/homarr-dev/internal/run/run.go @@ -70,7 +70,7 @@ func BuildPlan(opts Options) (*Plan, error) { } p.Env = append(p.Env, opts.Env...) if opts.Demo { - p.Env = append(p.Env, "DEMO_MODE=true", "UNSAFE_ENABLE_MOCK_INTEGRATION=true") + p.Env = append(p.Env, "DEMO_MODE=true", "DEMO_READ_ONLY=false", "UNSAFE_ENABLE_MOCK_INTEGRATION=true") p.Label += " (demo)" } return p, nil From a7aa89fafefbb349f3b483a1ce761a49eb30f5e8 Mon Sep 17 00:00:00 2001 From: Bergasha Date: Fri, 28 Aug 2026 10:22:48 +1000 Subject: [PATCH 2/6] feat(board+clock): fix item radius zoom, add clock day/night weather colour (#6726) --- .../layout/scaled-board-canvas.module.css | 9 +++++ packages/translation/src/lang/en.json | 10 +++++ packages/widgets/src/clock/advanced-view.tsx | 3 ++ packages/widgets/src/clock/component.tsx | 3 ++ packages/widgets/src/clock/index.ts | 38 +++++++++++++++++++ .../widgets/src/clock/weather-summary.tsx | 18 +++++++-- .../widgets/src/weather/animated-icon.tsx | 2 +- 7 files changed, 78 insertions(+), 5 deletions(-) diff --git a/apps/nextjs/src/components/board/layout/scaled-board-canvas.module.css b/apps/nextjs/src/components/board/layout/scaled-board-canvas.module.css index 91dfef8ca..a072d8c74 100644 --- a/apps/nextjs/src/components/board/layout/scaled-board-canvas.module.css +++ b/apps/nextjs/src/components/board/layout/scaled-board-canvas.module.css @@ -45,4 +45,13 @@ --mantine-h4-font-size: calc(1.125rem * var(--board-canvas-ui-scale, 1)); --mantine-h5-font-size: calc(1rem * var(--board-canvas-ui-scale, 1)); --mantine-h6-font-size: calc(0.875rem * var(--board-canvas-ui-scale, 1)); + /* Without these, border-radius tokens (e.g. the board's "Item radius" setting) stay at their + * flat rem value while the canvas zoom shrinks the rendered card, so the chosen radius becomes + * imperceptibly small on busy/zoomed-out boards - same class of bug as the spacing/font tokens + * above, just never ported over to radius. */ + --mantine-radius-xs: calc(0.25rem * var(--board-canvas-ui-scale, 1)); + --mantine-radius-sm: calc(0.25rem * var(--board-canvas-ui-scale, 1)); + --mantine-radius-md: calc(0.5rem * var(--board-canvas-ui-scale, 1)); + --mantine-radius-lg: calc(0.75rem * var(--board-canvas-ui-scale, 1)); + --mantine-radius-xl: calc(1rem * var(--board-canvas-ui-scale, 1)); } diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 8ebdd3511..a52328c0d 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -2430,6 +2430,16 @@ "label": "Animate weather icon", "description": "Animate the weather icon. Disabled by default and always disabled when reduced motion is requested." }, + "colorWeatherByDayNight": { + "label": "Color weather by day/night", + "description": "Tint the weather icon and temperature with a day color or a night color, based on whether it's currently day or night at the chosen location." + }, + "dayWeatherColor": { + "label": "Day color" + }, + "nightWeatherColor": { + "label": "Night color" + }, "worldClockCities": { "label": "World clock cities", "description": "Add and order up to six cities for advanced view." diff --git a/packages/widgets/src/clock/advanced-view.tsx b/packages/widgets/src/clock/advanced-view.tsx index bf9479f63..f438a5fba 100644 --- a/packages/widgets/src/clock/advanced-view.tsx +++ b/packages/widgets/src/clock/advanced-view.tsx @@ -132,6 +132,9 @@ const PrimaryClock = ({ now, options, primary }: Pick diff --git a/packages/widgets/src/clock/component.tsx b/packages/widgets/src/clock/component.tsx index bd656b558..426480297 100644 --- a/packages/widgets/src/clock/component.tsx +++ b/packages/widgets/src/clock/component.tsx @@ -54,6 +54,9 @@ export default function ClockWidget({ options, width, height, displayMode }: Wid locationName={options.weatherLocation.name} isFahrenheit={options.isWeatherFormatFahrenheit} animateIcon={options.animateWeatherIcon} + colorByDayNight={options.colorWeatherByDayNight} + dayColor={options.dayWeatherColor} + nightColor={options.nightWeatherColor} detailed={false} /> diff --git a/packages/widgets/src/clock/index.ts b/packages/widgets/src/clock/index.ts index 8f3fdd06f..e76fa9f3c 100644 --- a/packages/widgets/src/clock/index.ts +++ b/packages/widgets/src/clock/index.ts @@ -12,6 +12,23 @@ import { const timeZoneOptions = getTimeZoneOptions(); +// Mantine theme color names, used to let users pick their own day/night weather colors. +const weatherColorOptions: string[] = [ + "red", + "pink", + "grape", + "violet", + "indigo", + "blue", + "cyan", + "teal", + "green", + "lime", + "yellow", + "orange", + "gray", +]; + export const { definition, componentLoader } = createWidgetDefinition("clock", { icon: IconClock, supportsAdvancedFocus: true, @@ -101,6 +118,18 @@ export const { definition, componentLoader } = createWidgetDefinition("clock", { defaultValue: false, withDescription: true, }), + colorWeatherByDayNight: factory.switch({ + defaultValue: false, + withDescription: true, + }), + dayWeatherColor: factory.select({ + options: weatherColorOptions, + defaultValue: "orange", + }), + nightWeatherColor: factory.select({ + options: weatherColorOptions, + defaultValue: "blue", + }), worldClockCities: factory.timezoneList({ defaultValue: defaultWorldClockCities.map((city) => ({ ...city })), maxValues: maximumWorldClockCities, @@ -131,6 +160,15 @@ export const { definition, componentLoader } = createWidgetDefinition("clock", { animateWeatherIcon: { shouldHide: (options) => !options.showWeather, }, + colorWeatherByDayNight: { + shouldHide: (options) => !options.showWeather, + }, + dayWeatherColor: { + shouldHide: (options) => !options.showWeather || !options.colorWeatherByDayNight, + }, + nightWeatherColor: { + shouldHide: (options) => !options.showWeather || !options.colorWeatherByDayNight, + }, }, ); }, diff --git a/packages/widgets/src/clock/weather-summary.tsx b/packages/widgets/src/clock/weather-summary.tsx index 2dda870bf..89941a6f7 100644 --- a/packages/widgets/src/clock/weather-summary.tsx +++ b/packages/widgets/src/clock/weather-summary.tsx @@ -14,6 +14,9 @@ interface ClockWeatherSummaryProps { locationName: string; isFahrenheit: boolean; animateIcon: boolean; + colorByDayNight: boolean; + dayColor: string; + nightColor: string; detailed: boolean; } @@ -23,6 +26,9 @@ export const ClockWeatherSummary = ({ locationName, isFahrenheit, animateIcon, + colorByDayNight, + dayColor, + nightColor, detailed, }: ClockWeatherSummaryProps) => { const tCommon = useI18n("common"); @@ -46,6 +52,10 @@ export const ClockWeatherSummary = ({ return `${Math.round(preferred)}${unit}`; }; + const weatherColor = colorByDayNight + ? `var(--mantine-color-${weather.current.isDay ? dayColor : nightColor}-5)` + : undefined; + return ( {detailed ? ( @@ -54,9 +64,9 @@ export const ClockWeatherSummary = ({ animated={animateIcon} code={weather.current.weatherCode} isDay={weather.current.isDay} - style={zoomCompensatedSize(28)} + style={{ ...zoomCompensatedSize(28), ...(weatherColor ? { color: weatherColor } : undefined) }} /> - + {Math.round(temperature)} {unit} @@ -68,9 +78,9 @@ export const ClockWeatherSummary = ({ animated={animateIcon} code={weather.current.weatherCode} isDay={weather.current.isDay} - style={zoomCompensatedSize(26)} + style={{ ...zoomCompensatedSize(26), ...(weatherColor ? { color: weatherColor } : undefined) }} /> - + {Math.round(temperature)} {unit} diff --git a/packages/widgets/src/weather/animated-icon.tsx b/packages/widgets/src/weather/animated-icon.tsx index f25720ff5..587e486e7 100644 --- a/packages/widgets/src/weather/animated-icon.tsx +++ b/packages/widgets/src/weather/animated-icon.tsx @@ -26,7 +26,7 @@ export const AnimatedWeatherIcon = ({ animated = false, code, isDay, size = 26, let animationClass = ""; if (animated && (code !== 0 || isDay !== false)) animationClass = getAnimationClass(code); return ( - + ); From 5f572f1b92c74a2430fbfe42f9fc2604a44e2a6f Mon Sep 17 00:00:00 2001 From: Bergasha Date: Fri, 28 Aug 2026 10:59:48 +1000 Subject: [PATCH 3/6] fix(board): reserve and scale space for collapsible container header (#6727) --- .../sections/grid/section-grid.module.css | 35 +++++++++++++------ .../board/sections/grid/section-grid.tsx | 23 +++++++++++- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/apps/nextjs/src/components/board/sections/grid/section-grid.module.css b/apps/nextjs/src/components/board/sections/grid/section-grid.module.css index d24f5d776..df8a2c3ea 100644 --- a/apps/nextjs/src/components/board/sections/grid/section-grid.module.css +++ b/apps/nextjs/src/components/board/sections/grid/section-grid.module.css @@ -3,6 +3,22 @@ overflow: visible; } +/* --board-grid-drag-height inherits down the DOM tree like any custom property. A capped + section's own GridPreviewLayer effect correctly avoids setting it on itself (see + grid-preview-layer.tsx), but that does nothing to stop it inheriting the value an + *uncapped* ancestor section (e.g. the board's root section) legitimately sets on itself + while growing during a drag happening anywhere else on the board - any container (not just + a scrollable one - a non-scrollable, e.g. collapsible, container is just as capped) sits + inside that section's DOM tree, so the stray value cascades straight through and silently + defeats the `var(--board-grid-drag-height, ...px)` fallback in section-grid.tsx (including + its collapsibleHeaderInset adjustment), which only kicks in when the property is entirely + unset, not merely inherited. Reset it here so every container can never inherit a stray + value from anywhere above it, no matter what any ancestor's own drag state is doing. + https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascading_variables#custom_properties_and_inheritance */ +.viewport[data-section-kind="container"] { + --board-grid-drag-height: initial; +} + /* Chained with .viewport (rather than a standalone .scrollableViewport rule) so this always wins on specificity, not source order. GridEditor loads as a separate async chunk in edit mode, and a board mutation can trigger its stylesheet to re-register - at equal specificity, @@ -11,17 +27,14 @@ .viewport.scrollableViewport { overflow-x: hidden; overflow-y: auto; - /* --board-grid-drag-height inherits down the DOM tree like any custom property. A capped - section's own GridPreviewLayer effect correctly avoids setting it on itself (see - grid-preview-layer.tsx), but that does nothing to stop it inheriting the value an - *uncapped* ancestor section (e.g. the board's root section) legitimately sets on itself - while growing during a drag happening anywhere else on the board - a nested scrollable - container sits inside that section's DOM tree, so the stray value cascades straight - through and silently defeats the `var(--board-grid-drag-height, ...px)` fallback in - section-grid.tsx, which only kicks in when the property is entirely unset, not merely - inherited. Reset it here so this element can never inherit a stray value from anywhere - above it, no matter what any ancestor's own drag state is doing. - https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascading_variables#custom_properties_and_inheritance */ + /* The grid content inside has a fixed pixel width matching this viewport's full + outer width (needed for the drag/resize math), so there's no room to reserve + for a scrollbar without it eating into - or overlapping - the content on + whichever side it renders. Hiding the native scrollbar track entirely (wheel/ + touch/drag scrolling still works) avoids that instead of trying to reserve + space for it. */ + scrollbar-width: none; + -ms-overflow-style: none; --board-grid-drag-height: initial; } diff --git a/apps/nextjs/src/components/board/sections/grid/section-grid.tsx b/apps/nextjs/src/components/board/sections/grid/section-grid.tsx index a7401f0e9..23a584d6b 100644 --- a/apps/nextjs/src/components/board/sections/grid/section-grid.tsx +++ b/apps/nextjs/src/components/board/sections/grid/section-grid.tsx @@ -148,8 +148,25 @@ export const SectionGrid = ({ // card and visually spills past its right/bottom edges. 10 must match that CSS rule's inset. const effectiveCanvasScale = Number.isFinite(canvasScale) && canvasScale > 0 ? canvasScale : 1; const outerCardInset = section.kind === "container" ? (2 * 10) / effectiveCanvasScale : 0; + // A collapsible container's toggle bar (see the `containerToggle` Button in + // container-section.tsx) is an absolutely positioned overlay sitting on top of this grid. + // Its height comes from a Mantine size prop, which - like spacing/font-size - is compensated + // by --mantine-scale to render at a constant physical size regardless of board zoom (see + // scaled-board-canvas.module.css). That makes it behave like the outerCardInset gap above, + // not like the grid's own un-compensated logical-pixel math, so it needs the same + // effectiveCanvasScale conversion - a plain, unconverted subtraction would under-reserve on + // a zoomed-out board and leave the header overlapping the content. Without reserving it at + // all, the header bar covers the first row of the container's content instead of sitting + // above it. Only the vertical axis is affected - the toggle spans the full width already. + const collapsibleHeaderInset = + section.kind === "container" && section.options.collapsible + ? CONTAINER_HEADER_HEIGHT / effectiveCanvasScale + : 0; const logicalWidth = getLogicalGridSize(columnCount) - outerCardInset; - const viewportHeight = getLogicalGridSize(viewportRowCount) - outerCardInset; + const viewportHeight = Math.max( + 1, + getLogicalGridSize(viewportRowCount) - outerCardInset - collapsibleHeaderInset, + ); // Items are positioned in a coordinate space sized to the *un-inset* column/row count // (fullGridWidth/Height below - see getLogicalItemStyle), but logicalWidth/Height above are // deliberately smaller by outerCardInset to match the container's actual visible card size. @@ -283,6 +300,7 @@ export const SectionGrid = ({ { width: logicalWidth, height: `var(--board-grid-drag-height, ${viewportHeight}px)`, + marginTop: collapsibleHeaderInset || undefined, "--board-item-radius": `var(--mantine-radius-${board.itemRadius})`, } as CSSProperties } @@ -317,6 +335,9 @@ export const SectionGrid = ({ ); }; +// Matches the collapsible container toggle's h={24} in container-section.tsx. +const CONTAINER_HEADER_HEIGHT = 24; + const INTERACTIVE_GRID_SELECTOR = 'a,button,input,textarea,select,option,[contenteditable="true"],[role="button"],[data-grid-no-drag]'; const EDIT_ACTIVATION_KEYS = new Set(["Enter", " ", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"]); From c18fa8cf7794d9a4d1646aff55bed63d36f958a3 Mon Sep 17 00:00:00 2001 From: Bergasha Date: Fri, 28 Aug 2026 15:08:22 +1000 Subject: [PATCH 4/6] fix(widgets): replace system-resources chart tooltip with Mantine (#6729) * fix(widgets): replace system-resources chart tooltip with Mantine Tooltip.Floating --- packages/translation/src/lang/en.json | 3 +- .../chart/combined-network-traffic.tsx | 61 +++-- .../system-resources/chart/common-chart.tsx | 244 ++++++++++++------ .../src/system-resources/chart/cpu-chart.tsx | 20 +- .../src/system-resources/chart/gpu-chart.tsx | 20 +- .../system-resources/chart/memory-chart.tsx | 44 ++-- .../chart/network-traffic.tsx | 16 +- 7 files changed, 238 insertions(+), 170 deletions(-) diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index a52328c0d..4dc3b1783 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -4851,7 +4851,8 @@ "memory": "MEM", "network": "NET", "up": "UP", - "down": "DOWN" + "down": "DOWN", + "memoryTooltip": "{used} / {available} ({percent}%)" } }, "systemDisks": { diff --git a/packages/widgets/src/system-resources/chart/combined-network-traffic.tsx b/packages/widgets/src/system-resources/chart/combined-network-traffic.tsx index 756c7e65b..194a87fe9 100644 --- a/packages/widgets/src/system-resources/chart/combined-network-traffic.tsx +++ b/packages/widgets/src/system-resources/chart/combined-network-traffic.tsx @@ -1,4 +1,4 @@ -import { Box, Group, Paper, Stack, Text } from "@mantine/core"; +import { Box, Group, Stack, Text } from "@mantine/core"; import { IconNetwork } from "@tabler/icons-react"; import { formatByteRate } from "@homarr/common"; @@ -21,9 +21,43 @@ export const CombinedNetworkTrafficChart = ({ labelDisplayMode: LabelDisplayModeOption; advanced?: boolean; }) => { - const chartData = usageOverTime.map((usage, index) => ({ index, up: usage.up, down: usage.down })); + const chartData = usageOverTime.map((usage, index) => ({ + index, + up: usage.up, + down: usage.down, + })); const t = useI18n("widget.systemResources.card"); + const tooltipLabel = (index: number) => { + const point = usageOverTime[index]; + return ( + + + + + {t("up")}: {formatByteRate(Math.round(point?.up ?? 0))} + + + + + + {t("down")}: {formatByteRate(Math.round(point?.down ?? 0))} + + + + ); + }; + return ( { - return ( - - - {payload.map((payloadData) => ( - - - - {payloadData.value === undefined ? ( - <>N/A - ) : ( - formatByteRate(Math.round(Number(payloadData.value))) - )} - - - ))} - - - ); - }, - }} + tooltipLabel={tooltipLabel} /> ); }; diff --git a/packages/widgets/src/system-resources/chart/common-chart.tsx b/packages/widgets/src/system-resources/chart/common-chart.tsx index 7902b8705..42535611a 100644 --- a/packages/widgets/src/system-resources/chart/common-chart.tsx +++ b/packages/widgets/src/system-resources/chart/common-chart.tsx @@ -1,10 +1,24 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { CSSProperties, ReactNode } from "react"; +import type { + CSSProperties, + PointerEvent as ReactPointerEvent, + ReactNode, +} from "react"; +import { useState } from "react"; import type { AreaChartSeries } from "@mantine/charts"; import { AreaChart, LineChart } from "@mantine/charts"; -import { Card, Center, Group, Stack, Text, useComputedColorScheme, useMantineTheme } from "@mantine/core"; +import { + Card, + Center, + Group, + Stack, + Text, + Tooltip, + useComputedColorScheme, + useMantineTheme, +} from "@mantine/core"; import { useElementSize, useHover, useMergedRef } from "@mantine/hooks"; -import type { TooltipProps, YAxisProps } from "recharts"; +import type { YAxisProps } from "recharts"; import { useRequiredBoard } from "@homarr/boards/context"; import { zoomCompensatedSize } from "@homarr/ui"; @@ -19,7 +33,7 @@ export const CommonChart = ({ title, icon: Icon, labelDisplayMode, - tooltipProps, + tooltipLabel, yAxisProps, lastValue, chartType = "line", @@ -31,7 +45,10 @@ export const CommonChart = ({ title: ReactNode; icon: TablerIcon; labelDisplayMode: LabelDisplayModeOption; - tooltipProps?: TooltipProps; + // What to show on hover. Defaults to lastValue when omitted. Pass a function to reflect + // the datum under the cursor as it moves across multi-point data (e.g. memory's + // used/available at that point), instead of always showing the latest value. + tooltipLabel?: ReactNode | ((index: number) => ReactNode); yAxisProps?: Omit; lastValue?: string; chartType?: "line" | "area"; @@ -45,63 +62,80 @@ export const CommonChart = ({ const ref = useMergedRef(elementSizeRef, hoverRef); const opacity = board.opacity / 100; - let backgroundColor = colorScheme === "dark" ? `rgba(57, 57, 57, ${opacity})` : `rgba(246, 247, 248, ${opacity})`; + let backgroundColor = + colorScheme === "dark" + ? `rgba(57, 57, 57, ${opacity})` + : `rgba(246, 247, 248, ${opacity})`; const cardStyle: CSSProperties = { overflow: "hidden" }; - const chartRootStyle: CSSProperties = { padding: 5, borderRadius: theme.radius[board.itemRadius] }; + const chartRootStyle: CSSProperties = { + padding: 5, + borderRadius: theme.radius[board.itemRadius], + }; if (advanced) { - backgroundColor = "rgb(from var(--mantine-color-primaryColor-filled) r g b / calc(var(--opacity, 1) * 0.12))"; + backgroundColor = + "rgb(from var(--mantine-color-primaryColor-filled) r g b / calc(var(--opacity, 1) * 0.12))"; cardStyle.border = "1px solid rgb(from var(--mantine-color-secondaryColor-filled) r g b / calc(var(--opacity, 1) * 0.45))"; chartRootStyle.backgroundColor = backgroundColor; } const ChartComponent = chartType === "line" ? LineChart : AreaChart; - const showIcon = labelDisplayMode === "icon" || labelDisplayMode === "textWithIcon"; - const showText = labelDisplayMode === "text" || labelDisplayMode === "textWithIcon"; + const showIcon = + labelDisplayMode === "icon" || labelDisplayMode === "textWithIcon"; + const showText = + labelDisplayMode === "text" || labelDisplayMode === "textWithIcon"; + + // Track which data point the cursor is over so the tooltip can reflect that point + // instead of always showing the latest value while scrubbing across the chart. + const [hoveredIndex, setHoveredIndex] = useState(data.length - 1); + const handlePointerMove = (event: ReactPointerEvent) => { + if (data.length <= 1) return; + const rect = event.currentTarget.getBoundingClientRect(); + if (rect.width === 0) return; + const ratio = (event.clientX - rect.left) / rect.width; + const index = Math.round(ratio * (data.length - 1)); + setHoveredIndex(Math.min(data.length - 1, Math.max(0, index))); + }; + + // Clamp on read rather than syncing via an effect - handles data growing/shrinking + // between renders (e.g. history resetting on integration change) without ever + // handing a stale or out-of-range index to tooltipLabel. + const clampedHoveredIndex = Math.max( + 0, + Math.min(hoveredIndex, data.length - 1), + ); + const resolvedTooltipLabel = + typeof tooltipLabel === "function" + ? tooltipLabel(clampedHoveredIndex) + : (tooltipLabel ?? lastValue); return ( - - {data.length > 1 && height > 40 && !hovered && ( - - {showIcon && ( - 100 ? 20 : 14)} - stroke={1.5} - /> - )} - {showText && ( - 100 ? "md" : "xs"} fw={"bold"}> - {title} - - )} - {lastValue && ( - 100 ? "md" : "xs"} lineClamp={1}> - {lastValue} - - )} - - )} - {advanced && data.length > 1 && height > 0 && height <= 40 && lastValue && !hovered && ( -
- - {lastValue} - -
- )} - {data.length <= 1 ? ( -
- + + + {data.length > 1 && height > 40 && !hovered && ( + {showIcon && ( )} {showText && ( - 100 ? "md" : "xs"} fw={"bold"} ta="center"> + 100 ? "md" : "xs"} fw={"bold"}> {title} )} - {advanced && lastValue && ( - 100 ? "md" : "xs"} fw={600} ta="center"> + {lastValue && ( + 100 ? "md" : "xs"} + lineClamp={1} + > {lastValue} )} - -
- ) : ( - = 64} - yAxisProps={yAxisProps} - fillOpacity={chartType === "area" ? 0.3 : undefined} - /> - )} -
+ + )} + {advanced && + data.length > 1 && + height > 0 && + height <= 40 && + lastValue && + !hovered && ( +
+ + {lastValue} + +
+ )} + {data.length <= 1 ? ( +
+ + {showIcon && ( + 100 ? 20 : 14)} + stroke={1.5} + /> + )} + {showText && ( + 100 ? "md" : "xs"} + fw={"bold"} + ta="center" + > + {title} + + )} + {advanced && lastValue && ( + 100 ? "md" : "xs"} fw={600} ta="center"> + {lastValue} + + )} + +
+ ) : ( + + )} + + ); }; diff --git a/packages/widgets/src/system-resources/chart/cpu-chart.tsx b/packages/widgets/src/system-resources/chart/cpu-chart.tsx index 2922b737a..33ff0cc86 100644 --- a/packages/widgets/src/system-resources/chart/cpu-chart.tsx +++ b/packages/widgets/src/system-resources/chart/cpu-chart.tsx @@ -1,4 +1,3 @@ -import { Paper, Text } from "@mantine/core"; import { IconCpu } from "@tabler/icons-react"; import { invariantTechnicalLabels } from "@homarr/definitions"; @@ -27,25 +26,16 @@ export const SystemResourceCPUChart = ({ title={invariantTechnicalLabels.cpu} icon={IconCpu} lastValue={ - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - cpuUsageOverTime.length > 0 ? `${Math.round(cpuUsageOverTime[cpuUsageOverTime.length - 1]!)}%` : undefined + cpuUsageOverTime.length > 0 + ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + `${Math.round(cpuUsageOverTime[cpuUsageOverTime.length - 1]!)}%` + : undefined } chartType={hasShadow ? "area" : "line"} yAxisProps={{ domain: [0, 100] }} labelDisplayMode={labelDisplayMode} advanced={advanced} - tooltipProps={{ - content: ({ payload }) => { - const value = payload[0] ? Number(payload[0].value) : 0; - return ( - - - {value.toFixed(0)}% - - - ); - }, - }} + tooltipLabel={(index) => `${Math.round(cpuUsageOverTime[index] ?? 0)}%`} /> ); }; diff --git a/packages/widgets/src/system-resources/chart/gpu-chart.tsx b/packages/widgets/src/system-resources/chart/gpu-chart.tsx index 51d7ea9e5..14d5ad59c 100644 --- a/packages/widgets/src/system-resources/chart/gpu-chart.tsx +++ b/packages/widgets/src/system-resources/chart/gpu-chart.tsx @@ -1,4 +1,3 @@ -import { Paper, Text } from "@mantine/core"; import { IconDeviceDesktop } from "@tabler/icons-react"; import { invariantTechnicalLabels } from "@homarr/definitions"; @@ -27,25 +26,16 @@ export const SystemResourceGPUChart = ({ title={invariantTechnicalLabels.gpu} icon={IconDeviceDesktop} lastValue={ - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - gpuUsageOverTime.length > 0 ? `${Math.round(gpuUsageOverTime[gpuUsageOverTime.length - 1]!)}%` : undefined + gpuUsageOverTime.length > 0 + ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + `${Math.round(gpuUsageOverTime[gpuUsageOverTime.length - 1]!)}%` + : undefined } chartType={hasShadow ? "area" : "line"} yAxisProps={{ domain: [0, 100] }} labelDisplayMode={labelDisplayMode} advanced={advanced} - tooltipProps={{ - content: ({ payload }) => { - const value = payload[0] ? Number(payload[0].value) : 0; - return ( - - - {value.toFixed(0)}% - - - ); - }, - }} + tooltipLabel={(index) => `${Math.round(gpuUsageOverTime[index] ?? 0)}%`} /> ); }; diff --git a/packages/widgets/src/system-resources/chart/memory-chart.tsx b/packages/widgets/src/system-resources/chart/memory-chart.tsx index 187ecb670..4b7c040c2 100644 --- a/packages/widgets/src/system-resources/chart/memory-chart.tsx +++ b/packages/widgets/src/system-resources/chart/memory-chart.tsx @@ -1,4 +1,3 @@ -import { Paper, Text } from "@mantine/core"; import { IconBrain } from "@tabler/icons-react"; import { formatBytesPair } from "@homarr/common"; @@ -20,15 +19,33 @@ export const SystemResourceMemoryChart = ({ labelDisplayMode: LabelDisplayModeOption; advanced?: boolean; }) => { - const chartData = memoryUsageOverTime.map((usage, index) => ({ index, usage })); + const chartData = memoryUsageOverTime.map((usage, index) => ({ + index, + usage, + })); const t = useI18n("widget.systemResources.card"); const percentageUsed = memoryUsageOverTime.length > 0 && totalCapacityInBytes > 0 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - memoryUsageOverTime[memoryUsageOverTime.length - 1]! / totalCapacityInBytes + memoryUsageOverTime[memoryUsageOverTime.length - 1]! / + totalCapacityInBytes : undefined; + const tooltipLabel = (index: number) => { + const used = memoryUsageOverTime[index] ?? 0; + const memory = formatBytesPair(Math.round(used), totalCapacityInBytes); + const percent = + totalCapacityInBytes > 0 + ? Math.round((used / totalCapacityInBytes) * 100) + : 0; + return t("memoryTooltip", { + used: memory.used, + available: memory.available, + percent, + }); + }; + return ( { - const value = payload[0] ? Number(payload[0].value) : 0; - const memory = formatBytesPair(Math.round(value), totalCapacityInBytes); - return ( - - - {memory.used} / {memory.available} ( - {totalCapacityInBytes > 0 ? Math.round((value / totalCapacityInBytes) * 100) : 0}%) - - - ); - }, - }} + tooltipLabel={tooltipLabel} /> ); }; diff --git a/packages/widgets/src/system-resources/chart/network-traffic.tsx b/packages/widgets/src/system-resources/chart/network-traffic.tsx index 44d81b17e..a8513c7d9 100644 --- a/packages/widgets/src/system-resources/chart/network-traffic.tsx +++ b/packages/widgets/src/system-resources/chart/network-traffic.tsx @@ -1,4 +1,3 @@ -import { Paper, Text } from "@mantine/core"; import { IconArrowDown, IconArrowUp } from "@tabler/icons-react"; import { formatByteRate } from "@homarr/common"; @@ -39,18 +38,9 @@ export const NetworkTrafficChart = ({ chartType={hasShadow ? "area" : "line"} labelDisplayMode={labelDisplayMode} advanced={advanced} - tooltipProps={{ - content: ({ payload }) => { - const value = payload[0] ? Number(payload[0].value) : 0; - return ( - - - {formatByteRate(Math.round(value))} - - - ); - }, - }} + tooltipLabel={(index) => + formatByteRate(Math.round(usageOverTime[index] ?? 0)) + } /> ); }; From f221a5b4d3bbdcc69ce1b6fe66ec3bb3593677bc Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 28 Aug 2026 08:31:41 +0200 Subject: [PATCH 5/6] fix: respect mobile header and widget interactions (#6730) --- .../components/board/items/item-content.tsx | 1 + .../board/items/widget-context-menu.tsx | 6 +- .../header/configurable-header.module.css | 46 +++++------ .../layout/header/configurable-header.tsx | 79 +++++++++---------- apps/nextjs/src/components/layout/shell.tsx | 2 +- .../src/components/user-avatar-menu.tsx | 14 ++++ .../src/board-create/board-create-form.tsx | 3 +- packages/modals/src/modal-form-footer.tsx | 29 +++++-- packages/validation/src/header-preferences.ts | 2 +- .../src/modals/widget-edit-modal.module.css | 1 + .../widgets/src/modals/widget-edit-modal.tsx | 13 +-- 11 files changed, 104 insertions(+), 92 deletions(-) diff --git a/apps/nextjs/src/components/board/items/item-content.tsx b/apps/nextjs/src/components/board/items/item-content.tsx index e2ef8c88d..7964923c5 100644 --- a/apps/nextjs/src/components/board/items/item-content.tsx +++ b/apps/nextjs/src/components/board/items/item-content.tsx @@ -361,6 +361,7 @@ const LoadedBoardItemContent = ({ widgetStateRef={widgetStateRef} widgetRuntimeRef={widgetRuntimeRef} sourceRef={sourceRef} + disabled={isAdvanced} > | null>; widgetRuntimeRef: WidgetRuntimeRef; sourceRef: RefObject; + disabled?: boolean; children: ReactNode; } @@ -53,6 +54,7 @@ export const WidgetContextMenu = ({ previewDimensions, widgetRuntimeRef, sourceRef, + disabled = false, children, }: WidgetContextMenuProps) => { const { data: session } = useSession(); @@ -162,6 +164,7 @@ export const WidgetContextMenu = ({ }) ?? []; const openEditModal = useCallback(() => { + setMenuOpened(false); openModal( { kind: item.kind, @@ -230,6 +233,7 @@ export const WidgetContextMenu = ({ const handleOpenAdvancedFocus = useCallback(() => { if (!sourceRef.current) return; + setMenuOpened(false); openAdvancedFocus(item.id, sourceRef.current, { restoreFocusTarget: sourceRef.current.querySelector("[data-advanced-focus-trigger]") ?? sourceRef.current, @@ -256,7 +260,7 @@ export const WidgetContextMenu = ({ opened={menuOpened} onChange={setMenuOpened} > - {children} + {children} {canOpenAdvancedFocus && ( <> diff --git a/apps/nextjs/src/components/layout/header/configurable-header.module.css b/apps/nextjs/src/components/layout/header/configurable-header.module.css index 698943cb6..27e2c8402 100644 --- a/apps/nextjs/src/components/layout/header/configurable-header.module.css +++ b/apps/nextjs/src/components/layout/header/configurable-header.module.css @@ -2,7 +2,7 @@ overflow: hidden; } -.header[data-desktop-visible="false"] { +.header[data-visible="false"] { display: none; } @@ -73,39 +73,38 @@ flex: 0 0 auto; } +.userMenu { + display: flex; + padding-inline: rem(6px); +} + .mobileContent { display: none; + grid-template-columns: minmax(0, 1fr) auto; align-items: center; - justify-content: space-between; gap: var(--mantine-spacing-xs); width: 100%; height: 100%; padding-inline: var(--mantine-spacing-xs); } -.mobileIdentity, -.mobileActions { +.mobileZone { min-width: 0; + overflow-x: auto; + scrollbar-width: none; } -.mobileIdentity { - flex: 1 1 auto; - overflow: hidden; +.mobileZone::-webkit-scrollbar { + display: none; } -.mobileIdentity > :last-child { - flex: 1 1 auto; - min-width: 0; - overflow: hidden; +.mobileZone[data-zone="left"] { + justify-content: flex-start; } -.mobileIdentity h2 { - overflow: hidden; - text-overflow: ellipsis; -} - -.mobileActions { - flex: 0 0 auto; +.mobileZone[data-zone="right"] { + justify-content: flex-end; + max-width: 72vw; } .floatingControls { @@ -155,21 +154,12 @@ } @media (max-width: $mantine-breakpoint-sm) { - .header[data-desktop-visible="false"] { - display: block; - } - .desktopContent { display: none; } .mobileContent { - display: flex; - } - - .floatingControls, - .floatingControlsCorner { - display: none; + display: grid; } } diff --git a/apps/nextjs/src/components/layout/header/configurable-header.tsx b/apps/nextjs/src/components/layout/header/configurable-header.tsx index cdbd10f3f..6584cc17f 100644 --- a/apps/nextjs/src/components/layout/header/configurable-header.tsx +++ b/apps/nextjs/src/components/layout/header/configurable-header.tsx @@ -3,15 +3,7 @@ import type { ReactNode } from "react"; import { useEffect, useMemo, useRef, useState } from "react"; import { AppShellHeader, Avatar, Group, Indicator, Loader, Tooltip, useMantineColorScheme } from "@mantine/core"; -import { - IconBrandDocker, - IconHome, - IconLayoutDashboard, - IconReplace, - IconRobot, - IconSettings, - IconSunMoon, -} from "@tabler/icons-react"; +import { IconBrandDocker, IconHome, IconReplace, IconRobot, IconSettings, IconSunMoon } from "@tabler/icons-react"; import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; @@ -81,7 +73,10 @@ export const ConfigurableHeader = ({ return ( {(boardSwitcher) => { - const renderItems = (items: HeaderItem[]) => + const renderItems = ( + items: HeaderItem[], + display: { search: "input" | "icon"; logo: "logo" | "logoAndText" }, + ) => items.map((item) => ( )); + const desktopDisplay = { + search: headerPreferences.searchDisplay, + logo: headerPreferences.logoDisplay, + }; + const mobileDisplay = { search: "icon", logo: "logo" } as const; + return ( <> @@ -120,37 +121,26 @@ export const ConfigurableHeader = ({
{hasNavigation ? : null} - {renderItems(headerPreferences.zones.left)} + {renderItems(headerPreferences.zones.left, desktopDisplay)}
- {renderItems(headerPreferences.zones.center)} + {renderItems(headerPreferences.zones.center, desktopDisplay)}
- {renderItems(headerPreferences.zones.right)} + {renderItems(headerPreferences.zones.right, desktopDisplay)} {actions ? {actions} : null}
- + {hasNavigation ? : null} - + {renderItems(headerPreferences.zones.left, mobileDisplay)} - - {actions} - {boardEditAction} - {boardSettingsAction} - - - - - - + + {renderItems(headerPreferences.zones.center, mobileDisplay)} + {renderItems(headerPreferences.zones.right, mobileDisplay)} + {actions ? {actions} : null}
@@ -298,7 +288,9 @@ const HeaderItem = ({ return ( - + + ? + ); @@ -416,14 +408,19 @@ const HeaderItem = ({ return ( - +
+ +
); }; -const getBoardInitial = (name: string) => Array.from(name.trim())[0]?.toLocaleUpperCase() ?? "?"; +const getBoardInitial = (name: string) => { + const firstCharacter = Array.from(name.trim())[0] ?? "?"; + return Array.from(firstCharacter.toUpperCase())[0] ?? "?"; +}; diff --git a/apps/nextjs/src/components/layout/shell.tsx b/apps/nextjs/src/components/layout/shell.tsx index 40a3c99f9..558ba0c69 100644 --- a/apps/nextjs/src/components/layout/shell.tsx +++ b/apps/nextjs/src/components/layout/shell.tsx @@ -22,7 +22,7 @@ export const ClientShell = ({ const collapsed = useAtomValue(navigationCollapsedAtom); const backgroundProps = useOptionalBackgroundProps(); const { headerPreferences } = useSettings(); - const headerHeight = headerPreferences.visible ? appShellHeaderHeight : { base: appShellHeaderHeight, sm: 0 }; + const headerHeight = headerPreferences.visible ? appShellHeaderHeight : 0; return ( }> {t("homeBoard")} + {board && boardPermissions?.hasChangeAccess && ( + } + > + {tBoard("action.settings")} + + )} } rightSection={{formatHotkeyLabel(boardSwitcher.hotkey, t("modifier"))}} diff --git a/packages/forms-collection/src/board-create/board-create-form.tsx b/packages/forms-collection/src/board-create/board-create-form.tsx index e96a13597..ccfa17440 100644 --- a/packages/forms-collection/src/board-create/board-create-form.tsx +++ b/packages/forms-collection/src/board-create/board-create-form.tsx @@ -49,8 +49,7 @@ export const BoardCreateForm = ({ onCancel }: BoardCreateFormProps) => { message: tBoard("action.create.notification.success.message", { name: values.name }), }); const name = encodeURIComponent(result.name); - const layoutId = encodeURIComponent(result.layoutId); - router.push(`/boards/${name}?layout=${layoutId}&edit=true&add=true`); + router.push(`/boards/${name}`); }, onError() { showErrorNotification({ diff --git a/packages/modals/src/modal-form-footer.tsx b/packages/modals/src/modal-form-footer.tsx index f274b2dd4..8927ee081 100644 --- a/packages/modals/src/modal-form-footer.tsx +++ b/packages/modals/src/modal-form-footer.tsx @@ -7,6 +7,7 @@ import { useI18n } from "@homarr/translation/client"; interface ModalFormFooterProps { onCancel: () => void; + form?: string; submitLabel?: string; cancelLabel?: string; loading?: boolean; @@ -18,24 +19,38 @@ const ActionButtons = ({ submitLabel, cancelLabel, loading, -}: Pick) => { - const t = useI18n(); + form, +}: Pick) => { + const t = useI18n("common.action"); return ( <> - ); }; -export const ModalFormFooter = ({ onCancel, submitLabel, cancelLabel, loading, leftSection }: ModalFormFooterProps) => { +export const ModalFormFooter = ({ + onCancel, + form, + submitLabel, + cancelLabel, + loading, + leftSection, +}: ModalFormFooterProps) => { const actionButtons = ( - + ); if (leftSection) { diff --git a/packages/validation/src/header-preferences.ts b/packages/validation/src/header-preferences.ts index 5b7abe1b6..1235809b5 100644 --- a/packages/validation/src/header-preferences.ts +++ b/packages/validation/src/header-preferences.ts @@ -16,7 +16,7 @@ export const headerBuiltinItemIds = [ "user", ] as const; -export const requiredHeaderBuiltinItemIds = ["boardEdit", "boardSettings", "user"] as const; +export const requiredHeaderBuiltinItemIds = ["user"] as const; export const headerPreferenceItemIds = headerBuiltinItemIds; export const headerItemIds = headerBuiltinItemIds; diff --git a/packages/widgets/src/modals/widget-edit-modal.module.css b/packages/widgets/src/modals/widget-edit-modal.module.css index 9294af70d..1e3008958 100644 --- a/packages/widgets/src/modals/widget-edit-modal.module.css +++ b/packages/widgets/src/modals/widget-edit-modal.module.css @@ -143,6 +143,7 @@ margin-inline: calc(var(--mantine-spacing-md) * -1); margin-bottom: calc(var(--mantine-spacing-md) * -1); padding: var(--mantine-spacing-sm) var(--mantine-spacing-md); + padding-bottom: max(var(--mantine-spacing-sm), env(safe-area-inset-bottom)); border-top: 1px solid var(--mantine-color-default-border); background: color-mix(in srgb, var(--mantine-color-body) 94%, transparent); backdrop-filter: blur(10px); diff --git a/packages/widgets/src/modals/widget-edit-modal.tsx b/packages/widgets/src/modals/widget-edit-modal.tsx index b493f92ad..3c9d71708 100644 --- a/packages/widgets/src/modals/widget-edit-modal.tsx +++ b/packages/widgets/src/modals/widget-edit-modal.tsx @@ -36,7 +36,7 @@ import { objectEntries } from "@homarr/common"; import { IntegrationProvider, useSession } from "@homarr/auth/client"; import { useOptionalBoard } from "@homarr/boards/context"; import type { WidgetKind } from "@homarr/definitions"; -import { createModal, modalSizeForm } from "@homarr/modals"; +import { createModal, ModalFormFooter, modalSizeForm } from "@homarr/modals"; import type { SettingsContextProps } from "@homarr/settings/creator"; import { SpotlightProvider } from "@homarr/spotlight"; import { useI18n } from "@homarr/translation/client"; @@ -674,16 +674,7 @@ export const WidgetEditModal = createModal>(({ const footer = ( - - - - - - + ); From f455e71be5efc58174925f635e888a439d584de0 Mon Sep 17 00:00:00 2001 From: Bergasha Date: Fri, 28 Aug 2026 17:36:41 +1000 Subject: [PATCH 6/6] fix(board): remove per-widget refresh indicator (#6731) The small refresh-circle overlay that appeared in a widget's top-right corner during a background refetch wasn't adding useful signal and just added visual noise. Drop the isWidgetRefreshing detection, the indicator markup/CSS, and the now-dead query-key/runtime-query plumbing that only existed to power it. Updated the lazy-widgets e2e test to drop its assertions on the now -removed [data-widget-refreshing] marker; the behavior it was really verifying (cached content stays visible during a background refetch, which completes cleanly) is still covered by the existing network -level checks in the same test. --- .../board/items/item-content.module.css | 14 ---------- .../components/board/items/item-content.tsx | 27 +------------------ e2e/lazy-widgets.spec.ts | 2 -- 3 files changed, 1 insertion(+), 42 deletions(-) diff --git a/apps/nextjs/src/components/board/items/item-content.module.css b/apps/nextjs/src/components/board/items/item-content.module.css index 1e1226291..c00ac34d4 100644 --- a/apps/nextjs/src/components/board/items/item-content.module.css +++ b/apps/nextjs/src/components/board/items/item-content.module.css @@ -1,17 +1,3 @@ -.refreshIndicator { - position: absolute; - top: 0.25rem; - right: 0.25rem; - z-index: 3; - display: grid; - place-items: center; - width: 1.25rem; - height: 1.25rem; - border-radius: 999px; - background: color-mix(in srgb, var(--mantine-color-body) 78%, transparent); - pointer-events: none; -} - .settingsButton { position: absolute; top: -34px; diff --git a/apps/nextjs/src/components/board/items/item-content.tsx b/apps/nextjs/src/components/board/items/item-content.tsx index 7964923c5..2e5647ca9 100644 --- a/apps/nextjs/src/components/board/items/item-content.tsx +++ b/apps/nextjs/src/components/board/items/item-content.tsx @@ -4,7 +4,7 @@ import { createPortal } from "react-dom"; import dynamic from "next/dynamic"; import { Box, Button, Center, Loader, Portal } from "@mantine/core"; import { useElementSize, useIsomorphicEffect } from "@mantine/hooks"; -import { QueryErrorResetBoundary, useIsFetching, useQueryClient } from "@tanstack/react-query"; +import { QueryErrorResetBoundary, useQueryClient } from "@tanstack/react-query"; import combineClasses from "clsx"; import { NoIntegrationSelectedError } from "@homarr/widgets/errors/classes"; import type { FallbackProps } from "react-error-boundary"; @@ -19,7 +19,6 @@ import { WidgetError } from "@homarr/widgets/errors"; import { createWidgetRuntimeState, getWidgetQueryKeys, - getWidgetRuntimeQueries, supportsAdvancedFocus as definitionSupportsAdvancedFocus, } from "@homarr/widgets/definition"; import type { WidgetComponentProps, WidgetDefinition, WidgetRuntimeRef } from "@homarr/widgets/definition"; @@ -40,7 +39,6 @@ import { useItemActions } from "./item-actions"; import itemContentClasses from "./item-content.module.css"; import { WidgetContextMenu } from "./widget-context-menu"; import { removeWidgetDataQueries } from "./widget-query-recovery"; -import { matchesWidgetItemQuery } from "./widget-query-scope"; const BoardItemMenu = dynamic(() => import("./item-menu").then((module) => module.BoardItemMenu), { ssr: false, @@ -158,24 +156,6 @@ const LoadedBoardItemContent = ({ () => ({ ...item, options: reduceWidgetOptionsWithDefinition(definition, settings, item.options) }), [definition, item, settings], ); - const widgetQueryKeys = useMemo(() => getWidgetQueryKeys(definition, item.kind), [definition, item.kind]); - const isWidgetRefreshing = - useIsFetching({ - predicate: (query) => - query.state.data !== undefined && - matchesWidgetItemQuery( - query.queryKey, - widgetQueryKeys, - { - itemId: item.id, - boardId: board.id, - integrationIds: item.integrationIds, - options: menuItem.options, - runtimeQueries: getWidgetRuntimeQueries(widgetRuntimeRef), - }, - definition.queryMatcher, - ), - }) > 0; const [manualSurface, setManualSurface] = useState(null); const [surfacePortalTarget, setSurfacePortalTarget] = useState(null); const { active, viewportSize, open, close, dismiss, hover, leave } = useAdvancedFocus(); @@ -343,11 +323,6 @@ const LoadedBoardItemContent = ({ />
- {isWidgetRefreshing && ( - - - - )} ); diff --git a/e2e/lazy-widgets.spec.ts b/e2e/lazy-widgets.spec.ts index 4a595f754..bb6a6f268 100644 --- a/e2e/lazy-widgets.spec.ts +++ b/e2e/lazy-widgets.spec.ts @@ -163,7 +163,6 @@ describe("lazy widget application graph", () => { }); await expect.poll(() => refreshRequestStarted, { timeout: 10_000 }).toBe(true); await expect(cachedDownload).toBeVisible({ timeout: 5_000 }); - await expect(downloadsWidget.locator("[data-widget-refreshing]")).toBeVisible(); const refreshedDownloadsResponse = page.waitForResponse( (response) => response.url().includes("widget.downloads.getJobsAndStatuses") && response.ok(), @@ -171,7 +170,6 @@ describe("lazy widget application graph", () => { releaseRefresh(); releaseRefresh = undefined; await refreshedDownloadsResponse; - await expect(downloadsWidget.locator("[data-widget-refreshing]")).toHaveCount(0); await page.unroute(downloadsRequestPattern, delayDownloadsRefresh); expect(pageErrors).toEqual([]); expect(hydrationErrors).toEqual([]);