fix(board): improve widget scaling and edit layering
This commit is contained in:
1 parent
ea250d418d
commit
dc600fc5c8
7 files changed
+110
-100
No files matched your search
@@ -13,7 +13,7 @@
|
||||
|
||||
.board-grid-entry:has([data-board-widget-header]):hover,
|
||||
.board-grid-entry:has([data-board-widget-header]):focus-within {
|
||||
z-index: var(--homarr-z-index-widget-focus);
|
||||
z-index: calc(var(--homarr-z-index-board-header) - 1);
|
||||
}
|
||||
|
||||
.board-grid-entry[data-grid-preview="true"] {
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
.beszelStatsControls {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: var(--mantine-spacing-sm);
|
||||
right: var(--mantine-spacing-sm);
|
||||
left: var(--mantine-spacing-sm);
|
||||
opacity: 0;
|
||||
transition: opacity 150ms ease-out;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.beszelStatsContainer:hover .beszelStatsControls,
|
||||
.beszelStatsContainer:focus-within .beszelStatsControls {
|
||||
.beszelStatsRoot:hover .beszelStatsControls,
|
||||
.beszelStatsRoot:focus-within .beszelStatsControls {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@@ -23,9 +23,8 @@ import { getUsableWidgetQueryData } from "../common/query-state";
|
||||
import { BeszelStatsView } from "../beszel/_shared/stats-view";
|
||||
import { createBeszelSystemChoices, resolveBeszelSystemChoice } from "./selection";
|
||||
|
||||
const CONTROLS_ROW_HEIGHT = 36;
|
||||
const CONTROLS_ROW_GAP = 16;
|
||||
const STACK_PADDING = 24;
|
||||
const SCROLL_FIT_BUFFER = 2;
|
||||
|
||||
export default function BeszelSystemStatsWidget({
|
||||
options,
|
||||
@@ -33,6 +32,7 @@ export default function BeszelSystemStatsWidget({
|
||||
isEditMode,
|
||||
width,
|
||||
height,
|
||||
displayScale = 1,
|
||||
boardId,
|
||||
itemId,
|
||||
setOptions,
|
||||
@@ -71,6 +71,12 @@ export default function BeszelSystemStatsWidget({
|
||||
{ value: "1w", label: t("period.oneWeek") },
|
||||
{ value: "30d", label: t("period.thirtyDays") },
|
||||
];
|
||||
let responsiveWidth = width;
|
||||
let responsiveHeight = height;
|
||||
if (Number.isFinite(displayScale) && displayScale > 0) {
|
||||
responsiveWidth *= displayScale;
|
||||
responsiveHeight *= displayScale;
|
||||
}
|
||||
useWidgetRuntimeQueries(
|
||||
widgetRuntimeRef,
|
||||
selectedSystem && options.timePeriod !== "1m"
|
||||
@@ -185,86 +191,83 @@ export default function BeszelSystemStatsWidget({
|
||||
}
|
||||
|
||||
return (
|
||||
<Box h="100%" pos="relative">
|
||||
<Box h="100%" pos="relative" className={classes.beszelStatsRoot}>
|
||||
<Box pos="absolute" top={4} right={8} style={{ zIndex: 1 }}>
|
||||
<Group gap={0}>
|
||||
<IntegrationErrorIndicator results={systemsResult} />
|
||||
</Group>
|
||||
</Box>
|
||||
{!isEditMode && (
|
||||
<Group gap="xs" wrap="nowrap" className={classes.beszelStatsControls}>
|
||||
{systems.length > 1 && (
|
||||
<Menu position="bottom-start" withArrow shadow="md" withinPortal>
|
||||
<Menu.Target>
|
||||
<Button
|
||||
variant="default"
|
||||
size="compact-xs"
|
||||
leftSection={<IconServer style={iconSizes.sm} />}
|
||||
className={classes.beszelStatsSystemToggle}
|
||||
disabled={isSelectionSavePending}
|
||||
>
|
||||
{selectedLabel}
|
||||
</Button>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
{systems.map((system) => (
|
||||
<Menu.Item
|
||||
key={system.value}
|
||||
fz="xs"
|
||||
fw={system.value === selectedValue ? 600 : 400}
|
||||
c={system.value !== selectedValue ? "dimmed" : undefined}
|
||||
disabled={isSelectionSavePending}
|
||||
onClick={() => handleSelectSystem(system.value)}
|
||||
>
|
||||
{system.label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
)}
|
||||
{responsiveWidth >= 560 ? (
|
||||
<Button.Group className={classes.beszelStatsPeriodToggle}>
|
||||
{periodOptions.map((period) => (
|
||||
<Button
|
||||
key={period.value}
|
||||
size="compact-xs"
|
||||
variant={period.value === options.timePeriod ? "filled" : "default"}
|
||||
className={classes.beszelStatsPeriodButton}
|
||||
aria-pressed={period.value === options.timePeriod}
|
||||
disabled={isSelectionSavePending}
|
||||
onClick={() => handleTimePeriod(period.value)}
|
||||
>
|
||||
{period.label}
|
||||
</Button>
|
||||
))}
|
||||
</Button.Group>
|
||||
) : (
|
||||
<Select
|
||||
size="xs"
|
||||
value={options.timePeriod}
|
||||
onChange={(value) => value && handleTimePeriod(value)}
|
||||
disabled={isSelectionSavePending}
|
||||
data={periodOptions}
|
||||
className={classes.beszelStatsPeriodToggle}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
<ScrollArea
|
||||
h="100%"
|
||||
className={classes.beszelStatsContainer}
|
||||
style={{ pointerEvents: isEditMode ? "none" : undefined }}
|
||||
>
|
||||
<Stack gap="md" p="sm">
|
||||
{!isEditMode && (
|
||||
<Group gap="xs" wrap="nowrap" className={classes.beszelStatsControls}>
|
||||
{systems.length > 1 && (
|
||||
<Menu position="bottom-start" withArrow shadow="md" withinPortal>
|
||||
<Menu.Target>
|
||||
<Button
|
||||
variant="default"
|
||||
size="compact-xs"
|
||||
leftSection={<IconServer style={iconSizes.sm} />}
|
||||
className={classes.beszelStatsSystemToggle}
|
||||
disabled={isSelectionSavePending}
|
||||
>
|
||||
{selectedLabel}
|
||||
</Button>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
{systems.map((system) => (
|
||||
<Menu.Item
|
||||
key={system.value}
|
||||
fz="xs"
|
||||
fw={system.value === selectedValue ? 600 : 400}
|
||||
c={system.value !== selectedValue ? "dimmed" : undefined}
|
||||
disabled={isSelectionSavePending}
|
||||
onClick={() => handleSelectSystem(system.value)}
|
||||
>
|
||||
{system.label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
)}
|
||||
{width >= 560 ? (
|
||||
<Button.Group className={classes.beszelStatsPeriodToggle}>
|
||||
{periodOptions.map((period) => (
|
||||
<Button
|
||||
key={period.value}
|
||||
size="compact-xs"
|
||||
variant={period.value === options.timePeriod ? "filled" : "default"}
|
||||
className={classes.beszelStatsPeriodButton}
|
||||
aria-pressed={period.value === options.timePeriod}
|
||||
disabled={isSelectionSavePending}
|
||||
onClick={() => handleTimePeriod(period.value)}
|
||||
>
|
||||
{period.label}
|
||||
</Button>
|
||||
))}
|
||||
</Button.Group>
|
||||
) : (
|
||||
<Select
|
||||
size="xs"
|
||||
value={options.timePeriod}
|
||||
onChange={(value) => value && handleTimePeriod(value)}
|
||||
disabled={isSelectionSavePending}
|
||||
data={periodOptions}
|
||||
className={classes.beszelStatsPeriodToggle}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
<Stack p="sm">
|
||||
<BeszelStatsView
|
||||
integrationIds={selectedSystem ? [selectedSystem.integrationId] : []}
|
||||
systemId={selectedSystem?.systemId ?? ""}
|
||||
timePeriod={options.timePeriod as BeszelTimePeriod}
|
||||
columns={width > 600 ? 2 : 1}
|
||||
availableHeight={Math.max(
|
||||
0,
|
||||
height - STACK_PADDING - (isEditMode ? 0 : CONTROLS_ROW_HEIGHT + CONTROLS_ROW_GAP),
|
||||
)}
|
||||
columns={responsiveWidth > 600 ? 2 : 1}
|
||||
availableHeight={Math.max(0, responsiveHeight - STACK_PADDING - SCROLL_FIT_BUFFER)}
|
||||
visibility={{
|
||||
cpu: options.showCpu,
|
||||
memory: options.showMemory,
|
||||
|
||||
@@ -101,12 +101,12 @@ interface BeszelChartPanelProps {
|
||||
|
||||
export const BeszelChartPanel = memo(({ title, subtitle, chartProps }: BeszelChartPanelProps) => (
|
||||
<Stack gap={4} style={panelStyle}>
|
||||
<Group gap="xs">
|
||||
<Text size="sm" fw={600}>
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Text size="sm" fw={600} style={{ flexShrink: 0 }}>
|
||||
{title}
|
||||
</Text>
|
||||
{subtitle && (
|
||||
<Text size="xs" c="dimmed">
|
||||
<Text size="xs" c="dimmed" truncate style={{ minWidth: 0 }}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -35,13 +35,13 @@ const tooltips = {
|
||||
bytesTotal: makeTooltipProps(formatStorageBytes, true),
|
||||
};
|
||||
|
||||
const ChartSkeleton = () => (
|
||||
const ChartSkeleton = ({ height }: { height: number }) => (
|
||||
<Stack gap={4} style={{ minWidth: 0 }}>
|
||||
<Group gap="xs">
|
||||
<Skeleton h={14} w={72} radius="sm" />
|
||||
<Skeleton h={10} w={110} radius="sm" />
|
||||
</Group>
|
||||
<Skeleton h={CHART_HEIGHT} radius="sm" />
|
||||
<Skeleton h={height} radius="sm" />
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -67,7 +67,7 @@ interface BeszelStatsViewProps {
|
||||
}
|
||||
|
||||
const MIN_CHART_HEIGHT = 100;
|
||||
const PANEL_ROW_OVERHEAD = 28;
|
||||
const PANEL_ROW_OVERHEAD = 25;
|
||||
const GRID_ROW_GAP = 16;
|
||||
|
||||
const computeChartHeight = (availableHeight: number | undefined, rowCount: number) => {
|
||||
@@ -238,10 +238,13 @@ export function BeszelStatsView({
|
||||
|
||||
if (!activeStats) {
|
||||
const visibleChartCount = Object.values(visibility).filter(Boolean).length;
|
||||
const effectiveColumns = Math.min(columns, Math.max(1, visibleChartCount)) as 1 | 2;
|
||||
const rowCount = Math.ceil(visibleChartCount / effectiveColumns);
|
||||
const chartHeight = computeChartHeight(availableHeight, rowCount);
|
||||
return (
|
||||
<SimpleGrid cols={columns} spacing="md" aria-label={t("name")}>
|
||||
<SimpleGrid cols={effectiveColumns} spacing="md" aria-label={t("name")}>
|
||||
{Array.from({ length: visibleChartCount }, (_, index) => (
|
||||
<ChartSkeleton key={index} />
|
||||
<ChartSkeleton key={index} height={chartHeight} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
);
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: clamp(6px, 3cqi, 14px);
|
||||
gap: clamp(4px, 2cqi, 10px);
|
||||
padding: clamp(6px, 3cqmin, 14px);
|
||||
gap: clamp(4px, 2cqmin, 10px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -13,11 +13,10 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: clamp(8px, 3cqi, 18px);
|
||||
gap: clamp(8px, 3cqmin, 18px);
|
||||
flex-shrink: 0;
|
||||
padding-bottom: clamp(4px, 2cqi, 10px);
|
||||
border-bottom: 1px solid
|
||||
rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45));
|
||||
padding-bottom: clamp(4px, 2cqmin, 10px);
|
||||
border-bottom: 1px solid rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45));
|
||||
}
|
||||
|
||||
.heroExpanded {
|
||||
@@ -37,25 +36,25 @@
|
||||
}
|
||||
|
||||
.heroTextOnly .heroValue {
|
||||
font-size: clamp(22px, 10cqi, 36px);
|
||||
font-size: clamp(22px, 10cqmin, 36px);
|
||||
}
|
||||
|
||||
.heroText {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: clamp(2px, 1cqi, 4px);
|
||||
gap: clamp(2px, 1cqmin, 4px);
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.heroLabel {
|
||||
font-size: clamp(9px, 2.8cqi, 13px);
|
||||
font-size: clamp(9px, 2.8cqmin, 13px);
|
||||
color: var(--mantine-color-dimmed);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.heroValue {
|
||||
font-size: clamp(18px, 8cqi, 32px);
|
||||
font-size: clamp(18px, 8cqmin, 32px);
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
}
|
||||
@@ -68,7 +67,7 @@
|
||||
display: grid;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
gap: clamp(4px, 2cqi, 10px);
|
||||
gap: clamp(4px, 2cqmin, 10px);
|
||||
grid-template-columns: repeat(var(--stat-cols, 2), minmax(0, 1fr));
|
||||
grid-auto-rows: 1fr;
|
||||
align-content: stretch;
|
||||
@@ -81,11 +80,10 @@
|
||||
justify-content: center;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
gap: clamp(2px, 1.5cqi, 5px);
|
||||
padding: clamp(4px, 2.5cqi, 10px);
|
||||
gap: clamp(2px, 1.5cqmin, 5px);
|
||||
padding: clamp(4px, 2.5cqmin, 10px);
|
||||
border-radius: var(--mantine-radius-md);
|
||||
border: 1px solid
|
||||
rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45));
|
||||
border: 1px solid rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45));
|
||||
background-color: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -95,14 +93,14 @@
|
||||
}
|
||||
|
||||
.statValue {
|
||||
font-size: clamp(14px, 5.5cqi, 24px);
|
||||
font-size: clamp(14px, 5.5cqmin, 24px);
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.statLabel {
|
||||
font-size: clamp(8px, 2.5cqi, 12px);
|
||||
font-size: clamp(8px, 2.5cqmin, 12px);
|
||||
color: var(--mantine-color-dimmed);
|
||||
line-height: 1.2;
|
||||
text-align: center;
|
||||
@@ -119,6 +117,6 @@
|
||||
justify-content: center;
|
||||
min-height: 0;
|
||||
color: var(--mantine-color-dimmed);
|
||||
font-size: clamp(11px, 3cqi, 14px);
|
||||
font-size: clamp(11px, 3cqmin, 14px);
|
||||
text-align: center;
|
||||
}
|
||||
@@ -129,8 +129,9 @@ function UptimeKumaContent({ integrationIds, options, width, height, displayMode
|
||||
);
|
||||
const uptimeValue = clampPercent(combined.averageUptimePercent);
|
||||
const uptimeColor = getUptimeColor(uptimeValue);
|
||||
const ringSize = getRingSize(width);
|
||||
const iconSize = getIconSize(width);
|
||||
const visualScale = Math.min(width, height);
|
||||
const ringSize = getRingSize(visualScale);
|
||||
const iconSize = getIconSize(visualScale);
|
||||
const gridCols = getGridCols(width);
|
||||
const isAdvanced = displayMode === "advanced";
|
||||
|
||||
@@ -247,7 +248,7 @@ function UptimeKumaContent({ integrationIds, options, width, height, displayMode
|
||||
</Group>
|
||||
<ScrollArea h="100%">
|
||||
<Stack gap="lg" p="md">
|
||||
<Box h={Math.max(280, Math.min(420, height - 120))}>{summaryContent}</Box>
|
||||
<Box h={Math.min(420, Math.max(180, Math.floor(height * 0.55)))}>{summaryContent}</Box>
|
||||
{monitorList}
|
||||
</Stack>
|
||||
</ScrollArea>
|
||||
|
||||
Reference in new issue
Block a user