Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ded5bf03bb | ||
|
|
f484306bb3 | ||
|
|
649008d327 | ||
|
|
e716f847ab |
No files matched your search
@@ -113,7 +113,7 @@ The OpenAPI and MCP APIs include board management endpoints for common automatio
|
|||||||
- Set desktop and mobile home boards for the current user
|
- Set desktop and mobile home boards for the current user
|
||||||
- Configure global board defaults such as public home boards and status behavior
|
- Configure global board defaults such as public home boards and status behavior
|
||||||
|
|
||||||
Board layout responses identify each layout with a `mobile`, `base`, or `custom` role. Mobile and Base are protected. Layout-saving clients must preserve existing roles, keep Mobile at breakpoint `0`, keep Base as the highest breakpoint, and use unique custom breakpoints between them. The layout save operation returns the canonical saved layouts, including server-generated IDs for new custom layouts. A non-Base layout can be regenerated from Base with the protected reset operation.
|
Board layout responses identify each layout with a `mobile`, `base`, or `custom` role. Mobile and Base are protected. Layout-saving clients must preserve existing roles, keep Mobile at breakpoint `0`, and use a unique breakpoint for every layout. Base and custom layouts can appear in any breakpoint order. The layout save operation returns the canonical saved layouts, including server-generated IDs for new custom layouts. A non-Base layout can be regenerated from Base with the protected reset operation.
|
||||||
|
|
||||||
The internal `board.getManageOverview` tRPC query returns compact board previews limited to 12 layout rows by default. Callers may provide `previewRowLimit` from 1 through 48; `fullPreview: true` requests the server's 48-row maximum.
|
The internal `board.getManageOverview` tRPC query returns compact board previews limited to 12 layout rows by default. Callers may provide `previewRowLimit` from 1 through 48; `fullPreview: true` requests the server's 48-row maximum.
|
||||||
|
|
||||||
|
|||||||
@@ -133,20 +133,21 @@ On larger canvases, the complete board grows together like browser zoom.
|
|||||||
The complete canvas always fits the available width and never adds horizontal board scrolling.
|
The complete canvas always fits the available width and never adds horizontal board scrolling.
|
||||||
|
|
||||||
- **Mobile** starts at `0px` and uses three columns on new boards.
|
- **Mobile** starts at `0px` and uses three columns on new boards.
|
||||||
- **Base** is the widest layout and starts at `768px` on new boards.
|
- **Base** is the main layout and starts at `768px` on new boards.
|
||||||
|
|
||||||
Mobile and Base can be renamed and their column counts can be changed, but they cannot be removed. Mobile has no fixed
|
Mobile and Base can be renamed and their column counts can be changed, but they cannot be removed. Mobile has no fixed
|
||||||
rails; rail configuration is available only on Base and custom layouts. You can add removable custom layouts between
|
rails; rail configuration is available only on Base and custom layouts. You can add removable custom layouts at any
|
||||||
their breakpoints.
|
unused breakpoint above Mobile.
|
||||||
|
|
||||||
Each layout stores independent positions and sizes. Moving or resizing a widget while editing Mobile does not move it in
|
Each layout stores independent positions and sizes. Moving or resizing a widget while editing Mobile does not move it in
|
||||||
Base or any custom layout. New custom layouts start from Base's current column count and rail widths; Homarr chooses a
|
Base or any custom layout. New custom layouts start from Base's current column count and rail widths; Homarr chooses an
|
||||||
free breakpoint between Mobile and Base, and you can then change the breakpoint, columns, and rails.
|
available breakpoint, and you can then change the breakpoint, columns, and rails.
|
||||||
|
|
||||||
Older or incomplete boards are repaired automatically. A board with no layouts receives Mobile at `0px` with three
|
Older or incomplete boards are repaired automatically. A board with no layouts receives Mobile at `0px` with three
|
||||||
columns and Base at `768px` with ten columns. A board with one layout keeps it as Base and receives a generated Mobile
|
columns and Base at `768px` with ten columns. A board with one layout keeps it as Base and receives a generated Mobile
|
||||||
layout. With multiple layouts, the narrowest becomes Mobile, the widest becomes Base, and intermediate layouts remain
|
layout. When protected roles are missing from a board with multiple layouts, the narrowest becomes Mobile, the widest
|
||||||
custom. Duplicate or out-of-order breakpoints are normalized without discarding saved positions.
|
becomes Base, and intermediate layouts become custom. Once roles exist, breakpoint changes do not reassign them.
|
||||||
|
Duplicate breakpoints are normalized without discarding saved positions.
|
||||||
|
|
||||||
Use **Reset from Base** on Mobile or a custom layout to replace all of its positions and sizes with a newly generated version of Base. The layout's name, breakpoint, and column count are kept.
|
Use **Reset from Base** on Mobile or a custom layout to replace all of its positions and sizes with a newly generated version of Base. The layout's name, breakpoint, and column count are kept.
|
||||||
|
|
||||||
@@ -182,7 +183,8 @@ Apps, widgets, and Containers inside them still use the same fixed `200 × 200`
|
|||||||
#### Breakpoint
|
#### Breakpoint
|
||||||
|
|
||||||
The minimum screen width where the layout is used; a layout is eligible at its breakpoint and above. Mobile is fixed at
|
The minimum screen width where the layout is used; a layout is eligible at its breakpoint and above. Mobile is fixed at
|
||||||
`0px`. Custom breakpoints must be unique and sit between Mobile and Base. Base must remain the highest breakpoint.
|
`0px`. Every other breakpoint must be unique, but Base and custom layouts can be ordered at any width. Homarr uses the
|
||||||
|
eligible layout with the highest breakpoint, regardless of its role.
|
||||||
When the initial viewport is not available, Homarr uses its remembered viewport information, client hints, or User-Agent
|
When the initial viewport is not available, Homarr uses its remembered viewport information, client hints, or User-Agent
|
||||||
fallback to select a layout.
|
fallback to select a layout.
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { speedtestTrackerIntegration } from "@site/docs/integrations/speedtest-t
|
|||||||
This widget displays internet speed test results from your [Speedtest Tracker](https://docs.speedtest-tracker.dev/) instance,
|
This widget displays internet speed test results from your [Speedtest Tracker](https://docs.speedtest-tracker.dev/) instance,
|
||||||
including the latest result, historical averages, and a live chart of the last 12 hours.
|
including the latest result, historical averages, and a live chart of the last 12 hours.
|
||||||
|
|
||||||
Advanced view renders a source-labeled panel for every healthy integration. Each panel exposes that source's latest test, statistics, speed and ping charts, plus 12-hour, 24-hour, and truthful **Last 30** result ranges regardless of compact visibility switches. Compact mode keeps every configured overview section in its familiar card layout and names its healthy sources when multiple integrations are selected.
|
The widget keeps every configured overview section in a compact card layout and names its healthy sources when multiple integrations are selected.
|
||||||
|
|
||||||
When multiple Speedtest Tracker integrations are selected, results from healthy sources remain visible if another source fails. A warning indicator identifies each unavailable integration without exposing private request details. If every selected integration fails, the widget reports an error; if cached data remains visible after a later total failure, a stale-data warning is shown.
|
When multiple Speedtest Tracker integrations are selected, results from healthy sources remain visible if another source fails. A warning indicator identifies each unavailable integration without exposing private request details. If every selected integration fails, the widget reports an error; if cached data remains visible after a later total failure, a stale-data warning is shown.
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ interface Props {
|
|||||||
saveSettingsAsync: () => Promise<FormValues | null>;
|
saveSettingsAsync: () => Promise<FormValues | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const layoutRoleOrder = { base: 0, custom: 1, mobile: 2 } as const;
|
||||||
|
|
||||||
export const LayoutSettingsContent = ({ board, form, isSaving, saveSettingsAsync }: Props) => {
|
export const LayoutSettingsContent = ({ board, form, isSaving, saveSettingsAsync }: Props) => {
|
||||||
const tBoard = useI18n("board");
|
const tBoard = useI18n("board");
|
||||||
const tLayout = useI18n("layout");
|
const tLayout = useI18n("layout");
|
||||||
@@ -101,6 +103,9 @@ export const LayoutSettingsContent = ({ board, form, isSaving, saveSettingsAsync
|
|||||||
|
|
||||||
const nextBreakpoint = getNextCustomBreakpoint(form.values.layouts);
|
const nextBreakpoint = getNextCustomBreakpoint(form.values.layouts);
|
||||||
const baseLayout = form.values.layouts.find((layout) => layout.role === "base");
|
const baseLayout = form.values.layouts.find((layout) => layout.role === "base");
|
||||||
|
const displayedLayouts = form.values.layouts
|
||||||
|
.map((layout, index) => ({ layout, index }))
|
||||||
|
.toSorted((first, second) => layoutRoleOrder[first.layout.role] - layoutRoleOrder[second.layout.role]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionCard title={tBoard("setting.section.layout.title")}>
|
<SectionCard title={tBoard("setting.section.layout.title")}>
|
||||||
@@ -134,13 +139,9 @@ export const LayoutSettingsContent = ({ board, form, isSaving, saveSettingsAsync
|
|||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{form.values.layouts.map((layout, index) => {
|
{displayedLayouts.map(({ layout, index }) => {
|
||||||
const persistedLayout = board.layouts.find((candidate) => candidate.id === layout.id);
|
const persistedLayout = board.layouts.find((candidate) => candidate.id === layout.id);
|
||||||
const sourceLayout = persistedLayout ?? board.layouts.find((candidate) => candidate.role === "base");
|
const sourceLayout = persistedLayout ?? board.layouts.find((candidate) => candidate.role === "base");
|
||||||
const customBreakpoints = form.values.layouts
|
|
||||||
.filter((candidate) => candidate.role === "custom" && candidate.id !== layout.id)
|
|
||||||
.map((candidate) => candidate.breakpoint);
|
|
||||||
const baseBreakpoint = form.values.layouts.find((candidate) => candidate.role === "base")?.breakpoint ?? 768;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fieldset
|
<Fieldset
|
||||||
@@ -190,12 +191,8 @@ export const LayoutSettingsContent = ({ board, form, isSaving, saveSettingsAsync
|
|||||||
}
|
}
|
||||||
disabled={layout.role === "mobile"}
|
disabled={layout.role === "mobile"}
|
||||||
styles={{ description: { color: "var(--mantine-color-text)" } }}
|
styles={{ description: { color: "var(--mantine-color-text)" } }}
|
||||||
min={
|
min={layout.role === "mobile" ? 0 : 1}
|
||||||
layout.role === "base"
|
max={32767}
|
||||||
? Math.max(1, ...customBreakpoints.map((breakpoint) => breakpoint + 1))
|
|
||||||
: 1
|
|
||||||
}
|
|
||||||
max={layout.role === "custom" ? baseBreakpoint - 1 : 32767}
|
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.settingsButton {
|
.settingsButton {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -34px;
|
top: calc(var(--mantine-spacing-sm) / 2);
|
||||||
right: 14px;
|
right: calc(var(--mantine-spacing-sm) / 2);
|
||||||
z-index: 201;
|
z-index: 201;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@@ -52,7 +52,6 @@
|
|||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.settingsButton {
|
.settingsButton {
|
||||||
right: 4px;
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,12 +49,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.widgetIcon {
|
.widgetIcon {
|
||||||
|
width: var(--mantine-font-size-sm);
|
||||||
|
height: var(--mantine-font-size-sm);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
color: var(--mantine-color-dimmed);
|
color: var(--mantine-color-dimmed);
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
font-size: 11px;
|
font-size: var(--mantine-font-size-xs);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -94,7 +96,7 @@
|
|||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: var(--mantine-radius-md);
|
border-radius: var(--mantine-radius-md);
|
||||||
background-color: var(--chip-background);
|
background-color: var(--chip-background);
|
||||||
font-size: 10px;
|
font-size: var(--mantine-font-size-xxs);
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: clip;
|
overflow-x: clip;
|
||||||
|
overflow-y: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.viewport[data-board-hydrated="false"] {
|
.viewport[data-board-hydrated="false"] {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
.board-grid-entry:has([data-board-widget-header]):hover,
|
.board-grid-entry:has([data-board-widget-header]):hover,
|
||||||
.board-grid-entry:has([data-board-widget-header]):focus-within {
|
.board-grid-entry:has([data-board-widget-header]):focus-within {
|
||||||
z-index: 10;
|
z-index: var(--homarr-z-index-widget-focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
.board-grid-entry[data-grid-preview="true"] {
|
.board-grid-entry[data-grid-preview="true"] {
|
||||||
|
|||||||
@@ -2776,14 +2776,11 @@ export const boardLayoutsNeedRepair = (
|
|||||||
) => {
|
) => {
|
||||||
const mobileLayouts = boardLayouts.filter((layout) => layout.role === "mobile");
|
const mobileLayouts = boardLayouts.filter((layout) => layout.role === "mobile");
|
||||||
const baseLayouts = boardLayouts.filter((layout) => layout.role === "base");
|
const baseLayouts = boardLayouts.filter((layout) => layout.role === "base");
|
||||||
const baseLayout = baseLayouts.at(0);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
mobileLayouts.length !== 1 ||
|
mobileLayouts.length !== 1 ||
|
||||||
baseLayouts.length !== 1 ||
|
baseLayouts.length !== 1 ||
|
||||||
mobileLayouts.at(0)?.breakpoint !== 0 ||
|
mobileLayouts.at(0)?.breakpoint !== 0 ||
|
||||||
!baseLayout ||
|
|
||||||
boardLayouts.some((layout) => layout.id !== baseLayout.id && layout.breakpoint >= baseLayout.breakpoint) ||
|
|
||||||
new Set(boardLayouts.map((layout) => layout.breakpoint)).size !== boardLayouts.length
|
new Set(boardLayouts.map((layout) => layout.breakpoint)).size !== boardLayouts.length
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -143,8 +143,8 @@ export const seedProtectedBoardLayoutsAsync = async (db: Database, boardId?: str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mobileLayout = normalizedLayouts[0];
|
const mobileLayout = normalizedLayouts.find((layout) => layout.role === "mobile");
|
||||||
const baseLayout = normalizedLayouts.at(-1);
|
const baseLayout = normalizedLayouts.find((layout) => layout.role === "base");
|
||||||
if (mobileLayout && baseLayout) {
|
if (mobileLayout && baseLayout) {
|
||||||
await insertMissingProjectedPositionsAsync(
|
await insertMissingProjectedPositionsAsync(
|
||||||
db,
|
db,
|
||||||
|
|||||||
@@ -33,14 +33,14 @@ export const normalizeBoardLayoutRoles = <
|
|||||||
const baseLayouts = sourceLayouts.filter((layout) => layout.role === "base");
|
const baseLayouts = sourceLayouts.filter((layout) => layout.role === "base");
|
||||||
const mobileLayout = mobileLayouts.length === 1 ? mobileLayouts[0] : undefined;
|
const mobileLayout = mobileLayouts.length === 1 ? mobileLayouts[0] : undefined;
|
||||||
const baseLayout = baseLayouts.length === 1 ? baseLayouts[0] : undefined;
|
const baseLayout = baseLayouts.length === 1 ? baseLayouts[0] : undefined;
|
||||||
const orderedLayouts =
|
const hasProtectedLayouts = mobileLayout !== undefined && baseLayout !== undefined;
|
||||||
mobileLayout && baseLayout
|
let orderedLayouts = sourceLayouts.toSorted(compareBoardLayouts);
|
||||||
? [
|
if (hasProtectedLayouts) {
|
||||||
mobileLayout,
|
orderedLayouts = [
|
||||||
...sourceLayouts.filter((layout) => layout.role === "custom").toSorted(compareBoardLayouts),
|
mobileLayout,
|
||||||
baseLayout,
|
...sourceLayouts.filter((layout) => layout.id !== mobileLayout.id).toSorted(compareBoardLayouts),
|
||||||
]
|
];
|
||||||
: sourceLayouts.toSorted(compareBoardLayouts);
|
}
|
||||||
|
|
||||||
let previousBreakpoint = -1;
|
let previousBreakpoint = -1;
|
||||||
return orderedLayouts.map((layout, index) => {
|
return orderedLayouts.map((layout, index) => {
|
||||||
@@ -49,7 +49,12 @@ export const normalizeBoardLayoutRoles = <
|
|||||||
index === 0
|
index === 0
|
||||||
? 0
|
? 0
|
||||||
: Math.min(MAX_LAYOUT_BREAKPOINT - remainingLayouts, Math.max(previousBreakpoint + 1, layout.breakpoint));
|
: Math.min(MAX_LAYOUT_BREAKPOINT - remainingLayouts, Math.max(previousBreakpoint + 1, layout.breakpoint));
|
||||||
const role: LayoutRole = index === 0 ? "mobile" : index === orderedLayouts.length - 1 ? "base" : "custom";
|
let role: LayoutRole = layout.role ?? "custom";
|
||||||
|
if (!hasProtectedLayouts) {
|
||||||
|
if (index === 0) role = "mobile";
|
||||||
|
else if (index === orderedLayouts.length - 1) role = "base";
|
||||||
|
else role = "custom";
|
||||||
|
}
|
||||||
previousBreakpoint = breakpoint;
|
previousBreakpoint = breakpoint;
|
||||||
return { ...layout, breakpoint, role };
|
return { ...layout, breakpoint, role };
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4672,12 +4672,6 @@
|
|||||||
"noSectionsEnabled": "Enable at least one section in widget options.",
|
"noSectionsEnabled": "Enable at least one section in widget options.",
|
||||||
"error": {
|
"error": {
|
||||||
"internalServerError": "Failed to fetch speedtest data"
|
"internalServerError": "Failed to fetch speedtest data"
|
||||||
},
|
|
||||||
"range": {
|
|
||||||
"twelveHours": "12h",
|
|
||||||
"oneDay": "24h",
|
|
||||||
"all": "All",
|
|
||||||
"lastThirty": "Last 30"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uptimeKuma": {
|
"uptimeKuma": {
|
||||||
@@ -5424,7 +5418,7 @@
|
|||||||
"title": "Layout",
|
"title": "Layout",
|
||||||
"responsive": {
|
"responsive": {
|
||||||
"title": "Responsive layouts",
|
"title": "Responsive layouts",
|
||||||
"description": "Each layout keeps its own widget positions and sizes. Mobile and Base are always available; add custom layouts for widths in between.",
|
"description": "Each layout keeps its own widget positions and sizes. Mobile and Base are always available; add custom layouts at any additional breakpoint.",
|
||||||
"action": {
|
"action": {
|
||||||
"add": "Add layout"
|
"add": "Add layout"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,10 +116,6 @@ export const responsiveBoardLayoutsSchema = z
|
|||||||
ctx.addIssue({ code: "custom", message: "The Mobile layout breakpoint must be 0" });
|
ctx.addIssue({ code: "custom", message: "The Mobile layout breakpoint must be 0" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (layouts.some((layout) => layout.id !== baseLayout.id && layout.breakpoint >= baseLayout.breakpoint)) {
|
|
||||||
ctx.addIssue({ code: "custom", message: "The Base layout must have the highest breakpoint" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (new Set(layouts.map((layout) => layout.breakpoint)).size !== layouts.length) {
|
if (new Set(layouts.map((layout) => layout.breakpoint)).size !== layouts.length) {
|
||||||
ctx.addIssue({ code: "custom", message: "Layout breakpoints must be unique" });
|
ctx.addIssue({ code: "custom", message: "Layout breakpoints must be unique" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo } from "react";
|
||||||
import { Box, Card, Group, ScrollArea, SegmentedControl, SimpleGrid, Stack, Text } from "@mantine/core";
|
import { Box, Group, Stack, Text } from "@mantine/core";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
@@ -21,14 +21,10 @@ export default function SpeedtestTrackerWidget({
|
|||||||
options,
|
options,
|
||||||
integrationIds,
|
integrationIds,
|
||||||
width,
|
width,
|
||||||
height,
|
|
||||||
displayMode = "compact",
|
|
||||||
}: WidgetComponentProps<"speedtestTracker">) {
|
}: WidgetComponentProps<"speedtestTracker">) {
|
||||||
const t = useI18n("widget.speedtestTracker");
|
const t = useI18n("widget.speedtestTracker");
|
||||||
const dashboardQuery = clientApi.widget.speedtestTracker.getDashboard.useQuery({ integrationIds });
|
const dashboardQuery = clientApi.widget.speedtestTracker.getDashboard.useQuery({ integrationIds });
|
||||||
const dashboardData = getUsableWidgetQueryData(dashboardQuery) ?? emptyDashboardData;
|
const dashboardData = getUsableWidgetQueryData(dashboardQuery) ?? emptyDashboardData;
|
||||||
const [advancedRange, setAdvancedRange] = useState("24");
|
|
||||||
const isAdvanced = displayMode === "advanced";
|
|
||||||
const successfulDashboards = useMemo(() => getAvailableSpeedtestDashboards(dashboardData), [dashboardData]);
|
const successfulDashboards = useMemo(() => getAvailableSpeedtestDashboards(dashboardData), [dashboardData]);
|
||||||
|
|
||||||
const combined = useMemo(
|
const combined = useMemo(
|
||||||
@@ -36,98 +32,24 @@ export default function SpeedtestTrackerWidget({
|
|||||||
[successfulDashboards],
|
[successfulDashboards],
|
||||||
);
|
);
|
||||||
|
|
||||||
const rangeHours = isAdvanced ? Number(advancedRange) : 12;
|
const cutoff = Date.now() - 12 * 60 * 60 * 1000;
|
||||||
const cutoff = rangeHours > 0 ? Date.now() - rangeHours * 60 * 60 * 1000 : 0;
|
|
||||||
const recentFiltered = combined.recentResults.filter((result) => result.created_at.getTime() > cutoff);
|
const recentFiltered = combined.recentResults.filter((result) => result.created_at.getTime() > cutoff);
|
||||||
|
|
||||||
const showLatestResult = isAdvanced || options.showLatestResult;
|
const showLatestResult = options.showLatestResult;
|
||||||
const showStats = isAdvanced || options.showStats;
|
const showStats = options.showStats;
|
||||||
const showRecentResults = isAdvanced || options.showRecentResults;
|
const showRecentResults = options.showRecentResults;
|
||||||
const showPingGraph = isAdvanced || options.showPingGraph;
|
const showPingGraph = options.showPingGraph;
|
||||||
const hasStatSection = (showLatestResult && combined.latestResult !== null) || (showStats && combined.stats !== null);
|
const hasStatSection = (showLatestResult && combined.latestResult !== null) || (showStats && combined.stats !== null);
|
||||||
const hasChart = showRecentResults && recentFiltered.length > 0;
|
const hasChart = showRecentResults && recentFiltered.length > 0;
|
||||||
const noSectionsEnabled =
|
const noSectionsEnabled = !options.showLatestResult && !options.showStats && !options.showRecentResults;
|
||||||
!isAdvanced && !options.showLatestResult && !options.showStats && !options.showRecentResults;
|
|
||||||
const advancedSectionWidth = width >= 720 ? width / 2 : width;
|
|
||||||
|
|
||||||
const latest = showLatestResult && combined.latestResult && (
|
const latest = showLatestResult && combined.latestResult && (
|
||||||
<LatestResultSection
|
<LatestResultSection result={combined.latestResult} width={width} compactSurface />
|
||||||
result={combined.latestResult}
|
|
||||||
width={isAdvanced ? advancedSectionWidth : width}
|
|
||||||
compactSurface={!isAdvanced}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
const averages = showStats && combined.stats && (
|
const averages = showStats && combined.stats && (
|
||||||
<AveragesSection
|
<AveragesSection stats={combined.stats} width={width} compactSurface />
|
||||||
stats={combined.stats}
|
|
||||||
width={isAdvanced ? advancedSectionWidth : width}
|
|
||||||
compactSurface={!isAdvanced}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (displayMode === "advanced") {
|
|
||||||
const sourceColumns = successfulDashboards.length > 1 && width >= 900 ? 2 : 1;
|
|
||||||
const sourcePanelWidth = sourceColumns === 2 ? Math.max(0, (width - 48) / 2) : Math.max(0, width - 32);
|
|
||||||
const sourceSectionWidth = sourcePanelWidth >= 720 ? sourcePanelWidth / 2 : sourcePanelWidth;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box h="100%" pos="relative">
|
|
||||||
<Group pos="absolute" top={4} right={8} gap={0} style={{ zIndex: 2 }}>
|
|
||||||
<IntegrationErrorIndicator results={dashboardData} />
|
|
||||||
</Group>
|
|
||||||
<ScrollArea h="100%">
|
|
||||||
<Stack gap="md" p="md">
|
|
||||||
<SegmentedControl
|
|
||||||
size="xs"
|
|
||||||
value={advancedRange}
|
|
||||||
onChange={setAdvancedRange}
|
|
||||||
data={[
|
|
||||||
{ value: "12", label: t("range.twelveHours") },
|
|
||||||
{ value: "24", label: t("range.oneDay") },
|
|
||||||
{ value: "0", label: t("range.lastThirty") },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
{successfulDashboards.length === 0 ? (
|
|
||||||
<WidgetEmptyState />
|
|
||||||
) : (
|
|
||||||
<SimpleGrid cols={sourceColumns} spacing="md">
|
|
||||||
{successfulDashboards.map(({ integrationId, integrationName, dashboard }) => {
|
|
||||||
const sourceRecent = dashboard.recentResults.filter((result) => result.created_at.getTime() > cutoff);
|
|
||||||
const sourceHasStats = dashboard.latestResult !== null || dashboard.stats !== null;
|
|
||||||
const sourceHasChart = sourceRecent.length > 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card key={integrationId} data-integration-id={integrationId} p="sm" withBorder>
|
|
||||||
<Stack gap="md">
|
|
||||||
<Text size="sm" fw={600} truncate>
|
|
||||||
{integrationName}
|
|
||||||
</Text>
|
|
||||||
{sourceHasStats && (
|
|
||||||
<SimpleGrid cols={sourcePanelWidth >= 720 ? 2 : 1} spacing="md">
|
|
||||||
{dashboard.latestResult && (
|
|
||||||
<LatestResultSection result={dashboard.latestResult} width={sourceSectionWidth} />
|
|
||||||
)}
|
|
||||||
{dashboard.stats && <AveragesSection stats={dashboard.stats} width={sourceSectionWidth} />}
|
|
||||||
</SimpleGrid>
|
|
||||||
)}
|
|
||||||
{sourceHasChart && (
|
|
||||||
<Box h={Math.max(260, Math.min(420, height / 2))}>
|
|
||||||
<RecentResultsSection results={sourceRecent} showPingGraph />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
{!sourceHasStats && !sourceHasChart && <WidgetEmptyState />}
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</SimpleGrid>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</ScrollArea>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box h="100%" pos="relative">
|
<Box h="100%" pos="relative">
|
||||||
<Group pos="absolute" top={4} right={8} gap={0} style={{ zIndex: 2 }}>
|
<Group pos="absolute" top={4} right={8} gap={0} style={{ zIndex: 2 }}>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { optionsBuilder } from "../options";
|
|||||||
|
|
||||||
export const { definition, componentLoader } = createWidgetDefinition("speedtestTracker", {
|
export const { definition, componentLoader } = createWidgetDefinition("speedtestTracker", {
|
||||||
icon: IconSpeedboat,
|
icon: IconSpeedboat,
|
||||||
supportsAdvancedFocus: true,
|
|
||||||
refetchInterval: null,
|
refetchInterval: null,
|
||||||
createOptions() {
|
createOptions() {
|
||||||
return optionsBuilder.from(
|
return optionsBuilder.from(
|
||||||
|
|||||||
Reference in new issue
Block a user