fix: address dashboard review feedback
This commit is contained in:
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));
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in new issue
Block a user