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:
Bergashaandajnart authored and GitHub committed 2026-08-21 15:12:31 +02:00
1 parent 9b406d5d83
commit 002381d294
13 files changed
+270 -62

No files matched your search

+49 -16
View File
@@ -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}>
+1
View File
@@ -26,6 +26,7 @@ const columnTranslationKeyMap = {
export const { definition, componentLoader } = createWidgetDefinition("dockerContainers", {
icon: IconBrandDocker,
supportsAdvancedFocus: true,
queryKey: [["docker", "getContainers"]],
refetchInterval: 30,
createOptions() {