feat(v2 docker widget): Fixes column-resize squashing (#6646)
* fix(dataTable): resizing a column no longer squashes the others mantine-datatable's built-in column resize keeps the table's total width fixed by shrinking every other column to compensate, which collapses columns toward zero in a narrow widget (open upstream issue, no fix: icflorescu/mantine-datatable#discussions-757). Replaced it with our own resize handle that only ever changes the width of the column being dragged - the table grows and scrolls horizontally instead. Shared via usePersistedTableLayout, so docker, downloads, and beszel-system-table all get it. * feat(docker): enable hold-Shift advanced focus preview The widget already renders differently for displayMode="advanced", but was missing supportsAdvancedFocus: true, so the board never offered the hold-Shift preview for it - matching downloads and media-server, which both set this flag. * style(docker): tighten row density, decouple icon size from row height Circular avatar, denser row/footer padding, and action icons rendered larger than their button via absolute positioning so they don't inflate row height (the button stays small and fixed - it repeats once per row, so any growth there multiplies across every visible container). * fix(data-table): harden shared column resizing * fix(data-table): account for board scaling * fix(data-table): compact shared headers * fix(data-table): fit columns to widget * fix(downloads): simplify advanced details * test(widgets): align Docker advanced focus * fix(downloads): widen expanded details --------- Co-authored-by: ajnart <thomas@ajnart.dev>
This commit is contained in:
1 parent
9b406d5d83
commit
002381d294
13 files changed
+270
-62
No files matched your search
@@ -69,6 +69,18 @@ interface ContainerActionHandlers {
|
||||
const columnAccessors = ["name", "state", "host", "cpuUsage", "memoryUsage", "actions"] as const;
|
||||
const containerMenuWidth = 240;
|
||||
|
||||
// The *button* sizes stay small and fixed - row height is set by the tallest cell, and the
|
||||
// actions button is repeated once per row, so any growth here multiplies across every row and
|
||||
// tanks how many containers fit on screen. The *icon* sizes are rendered larger than their
|
||||
// button via absolute positioning (see ContainerMenuButton/footer refresh below) - an
|
||||
// absolutely-positioned element is removed from normal flow, so it doesn't count toward its
|
||||
// ancestor's height even though it visually overflows the button. This lets the icon look
|
||||
// properly sized without the button (and therefore the row) growing to match.
|
||||
const rowActionButtonSize = 22;
|
||||
const rowActionIconVisualSize = 32;
|
||||
const footerRefreshButtonSize = 24;
|
||||
const footerRefreshIconVisualSize = 30;
|
||||
|
||||
const createContainerLogsPath = (container: Pick<DockerContainer, "endpointId" | "id" | "name">) =>
|
||||
`/manage/tools/docker/logs/${container.id}?name=${encodeURIComponent(container.name)}&endpointId=${encodeURIComponent(container.endpointId)}`;
|
||||
|
||||
@@ -83,7 +95,13 @@ const ContainerStateBadge = ({ state }: { state: ContainerState }) => {
|
||||
const t = useScopedI18n("docker.field.state.option");
|
||||
|
||||
return (
|
||||
<Badge size="xs" radius="sm" variant="light" color={containerStateColorMap[state]}>
|
||||
<Badge
|
||||
size="xs"
|
||||
radius="sm"
|
||||
variant="light"
|
||||
color={containerStateColorMap[state]}
|
||||
styles={{ label: { lineHeight: 1.2 } }}
|
||||
>
|
||||
{t(state)}
|
||||
</Badge>
|
||||
);
|
||||
@@ -126,16 +144,15 @@ const createColumns = (
|
||||
<Group gap="xs" wrap="nowrap" style={{ overflow: "hidden" }}>
|
||||
{inlineState && <ContainerStateDot state={container.state} />}
|
||||
<Avatar
|
||||
variant="outline"
|
||||
radius="sm"
|
||||
size={20}
|
||||
radius="xl"
|
||||
size={24}
|
||||
styles={{ image: { objectFit: "contain" } }}
|
||||
src={container.iconUrl}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
{container.name.at(0)?.toUpperCase()}
|
||||
</Avatar>
|
||||
<Text size="sm" truncate>
|
||||
<Text size="sm" lh={1.2} truncate>
|
||||
{container.name}
|
||||
</Text>
|
||||
</Group>
|
||||
@@ -191,8 +208,12 @@ const createColumns = (
|
||||
{
|
||||
accessor: "actions",
|
||||
title: "",
|
||||
width: 44,
|
||||
width: rowActionButtonSize + 18,
|
||||
textAlign: "right",
|
||||
// The default `.homarr-data-table td { overflow: hidden }` (needed for name/host ellipsis
|
||||
// truncation) would clip the actions icon's intentional visual overflow - opt this one
|
||||
// cell out of it.
|
||||
cellsClassName: "docker-actions-cell",
|
||||
render: (container) => <ContainerMenuButton container={container} handlers={handlers} />,
|
||||
},
|
||||
];
|
||||
@@ -407,7 +428,7 @@ export default function DockerWidget({
|
||||
<Box style={{ flex: 1, minHeight: 0 }}>
|
||||
<HomarrDataTable
|
||||
isEditMode={isEditMode}
|
||||
cellPadding={width < 400 ? "2px 8px" : "4px 8px"}
|
||||
cellPadding="2px 8px"
|
||||
rowCursor="default"
|
||||
fetching={isFetching && containers.length === 0}
|
||||
fz={width < 400 ? "xs" : "sm"}
|
||||
@@ -431,38 +452,43 @@ export default function DockerWidget({
|
||||
style={{
|
||||
borderTop: "0.0625rem solid var(--border-color)",
|
||||
}}
|
||||
p={4}
|
||||
py={2}
|
||||
px={8}
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Group gap={4} wrap="nowrap">
|
||||
<IconBrandDocker size="var(--mantine-font-size-xl)" style={{ flexShrink: 0 }} />
|
||||
<Text size="sm" truncate>
|
||||
<IconBrandDocker size="var(--mantine-font-size-md)" style={{ flexShrink: 0 }} />
|
||||
<Text size="xs" truncate>
|
||||
{t("table.footer", { count: containers.length.toString() })}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
<Group gap="sm" wrap="nowrap" justify="flex-end" style={{ minWidth: 0 }}>
|
||||
{footerVisibility.cpu && (
|
||||
<Text size="sm" style={{ whiteSpace: "nowrap" }}>
|
||||
<Text size="xs" style={{ whiteSpace: "nowrap" }}>
|
||||
{t("table.totalCpu", { cpu: totals.cpu.toFixed(2) })}
|
||||
</Text>
|
||||
)}
|
||||
{footerVisibility.memory && (
|
||||
<Text size="sm" style={{ whiteSpace: "nowrap" }}>
|
||||
<Text size="xs" style={{ whiteSpace: "nowrap" }}>
|
||||
{t("table.totalMemory", { memory: formatBytes(totals.memory) })}
|
||||
</Text>
|
||||
)}
|
||||
<Tooltip label={t("table.refresh.lastUpdated", { when: relativeTime })}>
|
||||
<ActionIcon
|
||||
className={actionTargetClasses.root}
|
||||
size="sm"
|
||||
size={footerRefreshButtonSize}
|
||||
variant="transparent"
|
||||
c="var(--mantine-color-text)"
|
||||
loading={isFetching || refreshInventory.isPending}
|
||||
onClick={() => refreshInventory.mutate()}
|
||||
aria-label={t("table.refresh.lastUpdated", { when: relativeTime })}
|
||||
style={{ position: "relative", overflow: "visible" }}
|
||||
>
|
||||
<IconRefresh size="var(--mantine-font-size-md)" />
|
||||
<IconRefresh
|
||||
size={footerRefreshIconVisualSize}
|
||||
style={{ position: "absolute", top: "50%", left: "50%", transform: "translate(-50%, -50%)" }}
|
||||
/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
@@ -492,11 +518,18 @@ function ContainerMenuButton({
|
||||
className={actionTargetClasses.root}
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="sm"
|
||||
size={rowActionButtonSize}
|
||||
aria-label={t("title")}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
style={{ position: "relative", overflow: "visible" }}
|
||||
>
|
||||
<IconDots size="var(--mantine-font-size-md)" />
|
||||
{/* Rendered larger than the button and pulled out of flow via absolute positioning,
|
||||
so it looks properly sized without the button - and therefore the row - growing
|
||||
to match (row height is set by the tallest cell, and this button repeats per row). */}
|
||||
<IconDots
|
||||
size={rowActionIconVisualSize}
|
||||
style={{ position: "absolute", top: "50%", left: "50%", transform: "translate(-50%, -50%)" }}
|
||||
/>
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown w={containerMenuWidth} miw={containerMenuWidth} maw={containerMenuWidth}>
|
||||
|
||||
@@ -26,6 +26,7 @@ const columnTranslationKeyMap = {
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("dockerContainers", {
|
||||
icon: IconBrandDocker,
|
||||
supportsAdvancedFocus: true,
|
||||
queryKey: [["docker", "getContainers"]],
|
||||
refetchInterval: 30,
|
||||
createOptions() {
|
||||
|
||||
Reference in new issue
Block a user