fix: address dashboard review feedback

This commit is contained in:
ajnart committed 2026-08-22 14:27:42 +02:00
1 parent ebe7a36ba9
commit cd86b10167
9 files changed
+93 -118

No files matched your search

@@ -42,18 +42,7 @@ export const BoardContainerSection = ({ section }: Props) => {
const labelRight = isEditMode ? 48 : options.showOpenAll ? 40 : 8;
return (
<Box
className={combineClasses("board-grid-item-content", classes.containerRoot)}
pos="relative"
data-grid-item-content
w="100%"
h="100%"
style={{
overflow: "visible",
"--opacity": board.opacity / 100,
"--border-color": options.borderColor || undefined,
}}
>
<Box className="board-grid-item-content" data-grid-item-content w="100%" h="100%" style={{ overflow: "visible" }}>
<Card
className={combineClasses(
classes.itemCard,
@@ -66,6 +55,8 @@ export const BoardContainerSection = ({ section }: Props) => {
styles={{
root: {
overflow: "visible",
"--opacity": board.opacity / 100,
"--border-color": options.borderColor || undefined,
},
}}
radius={board.itemRadius}
@@ -110,6 +101,31 @@ export const BoardContainerSection = ({ section }: Props) => {
)}
</Button>
)}
{!isVisuallyCollapsed && !options.collapsible && options.showLabel && options.title && (
<Badge
pos="absolute"
top="calc(var(--mantine-spacing-xs) * -1)"
left={labelLeft}
maw={`calc(100% - ${labelLeft + labelRight}px)`}
size="md"
radius="sm"
variant="default"
c="var(--mantine-color-text)"
style={{
zIndex: 9,
overflow: "hidden",
pointerEvents: "none",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
backgroundColor: "var(--background-color)",
borderColor: "var(--border-color)",
}}
title={options.title}
data-board-container-label
>
{options.title}
</Badge>
)}
{options.showOpenAll && !isEditMode && (
<ActionIcon
pos="absolute"
@@ -138,24 +154,6 @@ export const BoardContainerSection = ({ section }: Props) => {
<SectionGrid section={section} columnCount={section.width} requestedRowCount={section.height} label={label} />
</Box>
</Card>
{!isVisuallyCollapsed && !options.collapsible && options.showLabel && options.title && (
<Badge
className={classes.containerLabel}
pos="absolute"
top="calc(var(--mantine-spacing-lg) * -1 - 2px)"
left={labelLeft}
maw={`calc(100% - ${labelLeft + labelRight}px)`}
size="md"
radius="sm"
fz="md"
variant="default"
c="var(--mantine-color-text)"
title={options.title}
data-board-container-label
>
{options.title}
</Badge>
)}
{isEditMode && <BoardContainerMenu section={section} />}
</Box>
);
@@ -1,4 +1,4 @@
.containerRoot {
.itemCard {
@mixin dark {
--background-color: rgb(from var(--mantine-color-dark-6) r g b / var(--opacity));
--border-color: rgb(from var(--mantine-color-dark-4) r g b / var(--opacity));
@@ -7,9 +7,6 @@
--background-color: rgb(from var(--mantine-color-white) r g b / var(--opacity));
--border-color: rgb(from var(--mantine-color-gray-3) r g b / var(--opacity));
}
}
.itemCard {
background-color: var(--background-color) !important;
border-color: var(--border-color) !important;
}
@@ -45,21 +42,6 @@
white-space: nowrap;
}
.containerLabel {
overflow: visible;
pointer-events: none;
background-color: var(--background-color);
border-color: var(--border-color);
}
.containerLabel :global(.mantine-Badge-label) {
min-width: 0;
overflow: hidden;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.containerBody {
visibility: visible;
opacity: 1;
@@ -5,6 +5,8 @@
}
.errorIndicator {
top: calc(var(--mantine-spacing-md) + var(--mantine-spacing-xl));
inset-inline-end: 4px;
z-index: 1;
padding: 2px;
border: 1px solid rgb(from var(--mantine-primary-color-filled) r g b / calc(var(--opacity, 1) * 0.45));
+1 -1
View File
@@ -257,7 +257,7 @@ const CalendarBase = ({
}}
/>
{hasErrors && (
<Group className={classes.errorIndicator} gap={0} pos="absolute" top={4} right={4}>
<Group className={classes.errorIndicator} gap={0} pos="absolute">
<IntegrationErrorIndicator results={failedIntegrations} />
<WidgetQueryErrorIndicator error={queryError} label={t("name")} />
</Group>
-19
View File
@@ -3,22 +3,3 @@
--mantine-datatable-background-color: transparent !important;
--mantine-datatable-striped-color: transparent !important;
}
.homarr-data-table.docker-table,
.homarr-data-table.docker-table .mantine-datatable-table,
.homarr-data-table.docker-table .mantine-datatable-table thead,
.homarr-data-table.docker-table .mantine-datatable-table tbody,
.homarr-data-table.docker-table .mantine-datatable-table tfoot,
.homarr-data-table.docker-table .mantine-datatable-table tr,
.homarr-data-table.docker-table th,
.homarr-data-table.docker-table td {
background: transparent !important;
}
.homarr-data-table.docker-table .mantine-datatable-table tbody tr:hover {
background: transparent !important;
}
.homarr-data-table.docker-table th {
backdrop-filter: none;
}
+3 -19
View File
@@ -1,20 +1,4 @@
/* Keep the downloads table translucent so the board wallpaper and opacity remain visible. */
.homarr-data-table.downloads-table,
.homarr-data-table.downloads-table .mantine-datatable-table,
.homarr-data-table.downloads-table .mantine-datatable-table thead,
.homarr-data-table.downloads-table .mantine-datatable-table tbody,
.homarr-data-table.downloads-table .mantine-datatable-table tfoot,
.homarr-data-table.downloads-table .mantine-datatable-table tr,
.homarr-data-table.downloads-table th,
.homarr-data-table.downloads-table td,
.homarr-data-table.downloads-table .mantine-datatable-row-expansion-cell {
background-color: transparent !important;
}
.homarr-data-table.downloads-table .mantine-datatable-table tbody tr:hover {
background-color: transparent !important;
}
.homarr-data-table.downloads-table th {
backdrop-filter: none;
/* Row expansion styling */
.downloads-table .mantine-datatable-row-expansion-cell {
background-color: color-mix(in srgb, var(--mantine-color-body) 40%, transparent) !important;
}
+1 -1
View File
@@ -219,7 +219,7 @@ export const getRssEntryDisplay = ({
descriptionLines,
}: RssEntryDisplayInput) => ({
showDescription: isAdvanced || (!hideDescription && !isTiny),
showImage: isAdvanced || (showPosterImage && !isDense),
showImage: isAdvanced || (showPosterImage && !isTiny),
showSource: isAdvanced || !isDense,
descriptionLineClamp: isAdvanced ? undefined : descriptionLines,
});
+53 -26
View File
@@ -1,6 +1,6 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { Box, Card, Group, ScrollArea, SimpleGrid, Text, Tooltip } from "@mantine/core";
import { clientApi } from "@homarr/api/client";
@@ -18,6 +18,34 @@ import { NoIntegrationDataError } from "../errors/no-data-integration";
type DiskDisplayMode = WidgetComponentProps<"systemDisks">["options"]["displayMode"];
const advancedColumnBreakpoints = [
{ minimumWidth: 1820, columns: 7 },
{ minimumWidth: 1560, columns: 6 },
{ minimumWidth: 1300, columns: 5 },
{ minimumWidth: 1040, columns: 4 },
{ minimumWidth: 780, columns: 3 },
{ minimumWidth: 520, columns: 2 },
{ minimumWidth: 0, columns: 1 },
] as const;
const getSystemDisksLayout = (width: number, height: number, diskCount: number, isAdvanced: boolean) => {
if (!isAdvanced) {
return {
columns: 1,
showSecondaryText: height >= 160 && width >= 260,
showTemperature: height >= 100 && width >= 220,
};
}
const breakpoint = advancedColumnBreakpoints.find((candidate) => width >= candidate.minimumWidth);
const columns = Math.min(diskCount, breakpoint?.columns ?? 1);
return {
columns: Math.max(1, columns),
showSecondaryText: true,
showTemperature: true,
};
};
export const clampPercentage = (percentage: number): number => Math.min(100, Math.max(0, percentage));
export const getDisplayText = (
@@ -201,6 +229,27 @@ export default function SystemResources({
const data = results.filter(
(entry): entry is typeof entry & { healthInfo: NonNullable<typeof entry.healthInfo> } => entry.healthInfo !== null,
);
const disks = data.flatMap((entry) => {
const fileSystem = filterStorageVolumes(
entry.healthInfo.fileSystem,
options.visibleStorageVolumes,
entry.integrationId,
);
const smart = filterStorageVolumes(entry.healthInfo.smart, options.visibleStorageVolumes, entry.integrationId);
return fileSystem.map((item) => ({
integrationId: entry.integrationId,
integrationName: entry.integrationName,
item,
smartItem: smart.find(
(candidate) => normalizeStorageDeviceName(candidate.deviceName) === normalizeStorageDeviceName(item.deviceName),
),
}));
});
const isAdvanced = displayMode === "advanced";
const layout = useMemo(
() => getSystemDisksLayout(width, height, disks.length, isAdvanced),
[disks.length, height, isAdvanced, width],
);
const queryIndicators = (
<Group gap={0}>
<IntegrationErrorIndicator results={results} />
@@ -219,40 +268,18 @@ export default function SystemResources({
if (isInitialWidgetQueryPending(healthQuery)) return <WidgetQueryLoadingState />;
if (data.length === 0) return emptyState;
const disks = data.flatMap((entry) => {
const fileSystem = filterStorageVolumes(
entry.healthInfo.fileSystem,
options.visibleStorageVolumes,
entry.integrationId,
);
const smart = filterStorageVolumes(entry.healthInfo.smart, options.visibleStorageVolumes, entry.integrationId);
return fileSystem.map((item) => ({
integrationId: entry.integrationId,
integrationName: entry.integrationName,
item,
smartItem: smart.find(
(candidate) => normalizeStorageDeviceName(candidate.deviceName) === normalizeStorageDeviceName(item.deviceName),
),
}));
});
const hasNoFileSystems = data.every((entry) => entry.healthInfo.fileSystem.length === 0);
const hasFailedSource = results.some((entry) => Boolean(entry.error));
if (hasNoFileSystems && !hasFailedSource && !healthQuery.error) throw new NoIntegrationDataError();
if (hasNoFileSystems || disks.length === 0) return emptyState;
const isAdvanced = displayMode === "advanced";
const minimumCardWidth = 260;
const columns = isAdvanced ? Math.max(1, Math.min(disks.length, Math.floor(width / minimumCardWidth))) : 1;
const cellWidth = width / columns;
return (
<Box h="100%" pos="relative">
<Box pos="absolute" top={4} right={8} style={{ zIndex: 2 }}>
{queryIndicators}
</Box>
<ScrollArea h="100%">
<SimpleGrid cols={columns} spacing="xs" p="xs">
<SimpleGrid cols={layout.columns} spacing="xs" p="xs">
{disks.map(({ integrationId, integrationName, item, smartItem }) => {
const freeText = t("status.free", {
percentage: String(Math.round(100 - clampPercentage(item.percentage))),
@@ -275,8 +302,8 @@ export default function SystemResources({
freeText={advancedDisplayTexts.free}
smartStatus={smartItem?.overallStatus}
isAdvanced={isAdvanced}
showSecondaryText={isAdvanced || (height >= 160 && cellWidth >= 260)}
showTemperature={isAdvanced || (height >= 100 && cellWidth >= 220)}
showSecondaryText={layout.showSecondaryText}
showTemperature={layout.showTemperature}
/>
);
})}
@@ -230,18 +230,19 @@ const SystemCharts = ({
const labelDisplayMode = isAdvanced ? "textWithIcon" : options.labelDisplayMode;
const chartCount = visibleCharts.length;
const chartColumns = isAdvanced && width >= 560 ? 2 : 1;
const compactChartGap = 8;
const chartHeight = isAdvanced
? 180
: `calc((100% - ${Math.max(0, chartCount - 1) * 8}px) / ${Math.max(1, chartCount)})`;
: `calc((100% - ${Math.max(0, chartCount - 1) * compactChartGap}px) / ${Math.max(1, chartCount)})`;
return (
<Stack gap={isAdvanced ? "xs" : 8} h={isAdvanced ? "auto" : height} miw={0}>
<Stack gap={isAdvanced ? "xs" : compactChartGap} h={isAdvanced ? "auto" : height} miw={0}>
{showTitle && (
<Text size="sm" fw={600} truncate="end">
{integrationName}
</Text>
)}
<SimpleGrid cols={chartColumns} spacing="xs" style={{ flex: 1, minHeight: 0 }}>
<SimpleGrid cols={chartColumns} spacing={isAdvanced ? "xs" : compactChartGap} style={{ flex: 1, minHeight: 0 }}>
{chartCount === 0 && (
<Center h="100%">
<Text size="sm" c="dimmed">