mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
Keep Docker image names readable by their repository and tag (#2521)
## Problem
Image references that share a registry and namespace differ only at the
end, but table cells truncated them from the end:
- **Images (name column):** at 390 and 700px, three rows read
`ghcr.io/pulse-demo…`. From 390 to 900px the repository name was hidden
in 3–4 of 8 rows.
- **Swarm services (Image column):** from 390 to 700px, two of three
rows read `registry.…`.
- **Containers (Image column):** the repository name was hidden in up to
7 of 12 rows at 1024px, and in 3 rows even at 1440px.
So an update badge, a size or a service couldn't be traced to its image
without opening the row. Real fleets are worse: every
`lscr.io/linuxserver/*` image shares a 20-character prefix.
## Change
- **`splitDockerImageReference`** (in `dockerImageReference.ts`) splits
a reference just before its repository name. A digest never moves the
split, and a reference without a path stays whole.
- **`DockerImageReferenceText`** mirrors `KubernetesNameText`: the head
truncates, `name:tag` stays visible, and the full reference stays on the
title.
- **Where it's used:** the Images name cell (via `DockerResourceNameCell
imageReference`), the Containers Image column and the Services Image
column.
- **Copying:** head and tail are flex items, and a browser copy would
join them with a line break. So a selection inside one reference copies
without it; wider selections are left to the browser.
- **Unchanged:** sorting, search and the stored value.
This is a readability fix to existing columns, in the same class as
#2509, #2514 and #2516, so there is no demand-ledger entry.
## Verification
I ran a mock-mode private stack on main 800329cbd8, comparing before and
after at 12–18 widths from 390 to 1600px.
- **Visible text** (each character checked against its clipping box):
- Rows that read identically fall to 0 on every table at every width.
- Rows hiding the repository name fall to 0–1, and to 0 from 900px on
Images.
- **Clip sweep:**
- No column other than Image changes, and no metric sublabel is lost.
- Image cells with clipped text are never more numerous than on main at
any width, and fewer at most widths.
- Element counts rise only because a truncated reference now has two
clipping spans; the head truncates by design.
- **Clipboard:** a keyboard copy of one reference reads back
`ghcr.io/pulse-demo/backup-coordinator:2026.04` from
`navigator.clipboard`.
- **Interactions:** sort by Image, and row expand and collapse, work on
all three tables. No console errors.
- **Tests:**
- New tests: the split cases, a rendered Images-table test covering the
tail, the title and the copy scoping, and a source guardrail for the
three image cells.
- The new and updated tests fail against main's source.
- 544 test files pass across `src/features`, `src/hooks` and
`src/components/{shared,Workloads,Infrastructure}`.
The browser receipt is
`frontend-modern/browser-verification/a68fe9ea6a927e83.json`.
**Known residual:** when the head truncates completely there's no
leading ellipsis (as with `KubernetesNameText`). When it truncates
partly, the gap after its ellipsis can be up to one character wide.
This commit is contained in:
@@ -2355,7 +2355,12 @@ not a replacement status card, CTA band, or page-local nested card.
|
||||
truncate` span ignores the column width: it spills past a narrower cell
|
||||
with no ellipsis, wastes room in a wider one, and sits about 2 pixels above
|
||||
the row baseline. `platformOverviewLayout.guardrails.test.ts` holds the
|
||||
Docker, Kubernetes, Proxmox and vSphere table sources to that rule.
|
||||
Docker, Kubernetes, Proxmox and vSphere table sources to that rule. An
|
||||
identity value whose distinguishing part sits at its end (a generated
|
||||
Kubernetes name, a registry-prefixed Docker image reference) instead splits
|
||||
into a truncating head and a kept tail (`KubernetesNameText`,
|
||||
`DockerImageReferenceText`), with the full value on the title, so rows that
|
||||
share a prefix stay distinguishable in a narrow cell.
|
||||
When a platform table row itself owns that keyboard- and touch-operable
|
||||
disclosure, `SummaryRowActionButton` removes the redundant visible chevron
|
||||
below the shared mobile breakpoint while preserving the button for screen
|
||||
|
||||
@@ -923,6 +923,15 @@ the title; task starts use the shared relative-time primitive with their exact
|
||||
timestamp retained. Native table/model/guardrail tests and the production
|
||||
`browser-tests/proxmox-docker-rows.cjs` exercise these surfaces without executing
|
||||
a container action or claiming native appliance or release acceptance.
|
||||
Image references (the Images name, the Containers Image column and the Services
|
||||
Image column) render through `DockerImageReferenceText`, which splits before
|
||||
the repository name (`splitDockerImageReference`), because references that
|
||||
share a registry and namespace differ only at the end: a narrow cell truncates
|
||||
the head and keeps `name:tag` (`ghcr.io/pul…backup-coordinator:2026.04`), with
|
||||
the full reference on the title. End truncation left rows that all read
|
||||
`ghcr.io/pulse-demo…`. A selection inside one reference copies without the
|
||||
line break the two flex items would add. Sorting and the stored value are
|
||||
unchanged.
|
||||
|
||||
### PBS host history correlation
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "800329cbd83c48fdb7402a18633189f2c8a2d6ff",
|
||||
"verified_at": "2026-10-05T14:57:23Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/features/docker/DockerContainersTable.tsx",
|
||||
"frontend-modern/src/features/docker/DockerImageReferenceText.tsx",
|
||||
"frontend-modern/src/features/docker/DockerImagesTable.tsx",
|
||||
"frontend-modern/src/features/docker/DockerNativeTableShared.tsx",
|
||||
"frontend-modern/src/features/docker/DockerServicesTable.tsx",
|
||||
"frontend-modern/src/features/docker/dockerImageReference.ts"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/features/docker/DockerContainersTable.tsx": "07345ed1b0efb0f22d9c4d47648505d0564745cd088f9a6c095eabbf8d5f005e",
|
||||
"frontend-modern/src/features/docker/DockerImageReferenceText.tsx": "0ae194ed57a9e9397e9091323fdb10de0b96ede1db52fb4b4a080c2b9fbce794",
|
||||
"frontend-modern/src/features/docker/DockerImagesTable.tsx": "c336bed9851f8b9e4ca52b365bc6000926f4c98baf580234dcd46edf26978222",
|
||||
"frontend-modern/src/features/docker/DockerNativeTableShared.tsx": "ae9b54af27933bff2859e2ed5e6b31f82e396007b971861bf4638dbf463f2f06",
|
||||
"frontend-modern/src/features/docker/DockerServicesTable.tsx": "9f89bd4555185b5b30f3df9618835b83f2cae3525ec766f62744fa93cabae59a",
|
||||
"frontend-modern/src/features/docker/dockerImageReference.ts": "d9e940885e1bd69b6586dbb5c9e250a0b441440002615151874bd92aea9e3bce"
|
||||
},
|
||||
"routes": [
|
||||
"/docker/images",
|
||||
"/docker/containers",
|
||||
"/docker/swarm"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 390,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 430,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 480,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 540,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 573,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 600,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 640,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 700,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 740,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 768,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 800,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 900,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 960,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 1024,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 1100,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 1280,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 1600,
|
||||
"height": 900
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"Private mock stack (PULSE_MOCK_MODE, random metrics off) serving this tree on main 800329cbd8; before = main, after = main plus this change, measured in the same session",
|
||||
"Visible-text probe per image cell (each character checked against its clipping box): Images name rows that read identically fell from 3 to 0 at 390 and 700, rows whose repository name was hidden fell from 3-4 to 0-1 between 390 and 900 and to 0 from 900; Swarm services read registry.\u2026 for two of three rows from 390 to 700 on main and \u2026backen\u2026 / \u2026fronten\u2026 patched; the Containers Image column (grouped and ungrouped, Host and Engine shown) hid the repository name in up to 7 of 12 rows at 1024 and 3 at 1440 on main, and in at most 1 patched",
|
||||
"Clip sweep over every cell and its overflow-hidden or ellipsis descendants on all four tables: no column other than Image changes at any width and no metric sublabel is lost; Image cells with any clipped text are never more than on main at any width (fewer at most widths); element counts rise only because a truncated reference now has two clipping spans, the head by design",
|
||||
"References without a path (postgres:16.4, traefik:v3.1) render as before; the full reference stays on the title of every reference element (8/8 images, 3/3 services, 12/12 containers)",
|
||||
"Clipboard: a keyboard copy of one reference reads ghcr.io/pulse-demo/backup-coordinator:2026.04 from navigator.clipboard; a whole-row selection keeps the browser's own text",
|
||||
"Before and after element screenshots: Images at 390, 700 and 1100, Swarm services at 600 and 1024, ungrouped Containers at 1024"
|
||||
],
|
||||
"interactions": [
|
||||
"Loaded each route fresh, waited for the row count to settle, resized through every viewport before measuring",
|
||||
"Sorted by Image on all three tables (aria-sort ascending, same order as main) and reset",
|
||||
"Expanded and collapsed the first row on all three tables at 390, 700 and 1100 (containers at 1100 and 1440); no console errors",
|
||||
"Selected one image reference and a whole row, pressed the copy shortcut, and read the clipboard",
|
||||
"Switched the Docker containers table to the ungrouped view so the Host and Engine columns render"
|
||||
]
|
||||
}
|
||||
@@ -87,6 +87,7 @@ import type { DockerContainerUpdateStatus } from '@/types/api';
|
||||
import type { Resource } from '@/types/resource';
|
||||
import { DockerContainerLifecycleControls } from './DockerContainerLifecycleControls';
|
||||
import { CONTAINER_CPU_CAPACITY_DESCRIPTION } from './dockerCpuPresentation';
|
||||
import { DockerImageReferenceText } from './DockerImageReferenceText';
|
||||
|
||||
type DockerNetwork = NonNullable<NonNullable<Resource['docker']>['networks']>[number];
|
||||
type DockerMount = NonNullable<NonNullable<Resource['docker']>['mounts']>[number];
|
||||
@@ -584,9 +585,7 @@ export const DockerContainersTable: Component<DockerContainersTableProps> = (pro
|
||||
case 'image':
|
||||
return (
|
||||
<TableCell class={`${getPlatformTableCellClassForKind(column.kind)} text-base-content`}>
|
||||
<span class="block max-w-full truncate" title={image()}>
|
||||
{image()}
|
||||
</span>
|
||||
<DockerImageReferenceText reference={image()} class="max-w-full" />
|
||||
</TableCell>
|
||||
);
|
||||
case 'state':
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { Component } from 'solid-js';
|
||||
import { splitDockerImageReference } from './dockerImageReference';
|
||||
|
||||
/**
|
||||
* An image reference in a narrow cell. References that share a registry and
|
||||
* namespace (ghcr.io/pulse-demo/…, lscr.io/linuxserver/…) differ only in the
|
||||
* repository name and tag at the end, so the head truncates and the tail stays
|
||||
* visible: ghcr.io/pul…backup-coordinator:2026.04. The tail never outgrows the
|
||||
* cell: when even it does not fit, it truncates too once the head is gone. The
|
||||
* full reference stays on the title, and the two spans read as one reference
|
||||
* to assistive technology. Mirrors KubernetesNameText.
|
||||
*/
|
||||
export const DockerImageReferenceText: Component<{ reference: string; class?: string }> = (
|
||||
props,
|
||||
) => {
|
||||
const parts = () => splitDockerImageReference(props.reference);
|
||||
// The head and tail are flex items, so a plain copy inserts a line break
|
||||
// between them and a pasted `docker pull` breaks. A selection inside one
|
||||
// reference copies without it; wider selections copy as the browser builds
|
||||
// them.
|
||||
const copyWithoutBreak = (event: ClipboardEvent & { currentTarget: HTMLSpanElement }) => {
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.rangeCount === 0 || !event.clipboardData) return;
|
||||
if (!event.currentTarget.contains(selection.getRangeAt(0).commonAncestorContainer)) return;
|
||||
event.clipboardData.setData('text/plain', selection.toString().replace(/\r?\n/g, ''));
|
||||
event.preventDefault();
|
||||
};
|
||||
return (
|
||||
<span
|
||||
class={`flex min-w-0 ${props.class ?? ''}`}
|
||||
title={props.reference}
|
||||
data-docker-image-reference
|
||||
onCopy={copyWithoutBreak}
|
||||
>
|
||||
<span class="truncate">{parts().head}</span>
|
||||
<span class="max-w-full shrink-0 truncate" data-docker-image-reference-tail>
|
||||
{parts().tail}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default DockerImageReferenceText;
|
||||
@@ -216,6 +216,7 @@ export const DockerImagesTable: Component<
|
||||
>
|
||||
<DockerResourceNameCell
|
||||
resource={resource}
|
||||
imageReference
|
||||
detailToggle={
|
||||
<PlatformResourceDetailToggleButton
|
||||
expanded={isExpanded()}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@/features/platformPage/sharedPlatformPage';
|
||||
import type { Resource } from '@/types/resource';
|
||||
import type { DockerResourceStatusFilter } from './dockerPageModel';
|
||||
import { DockerImageReferenceText } from './DockerImageReferenceText';
|
||||
|
||||
export type DockerNativeTableProps = {
|
||||
resources: Resource[];
|
||||
@@ -69,6 +70,9 @@ export const DockerResourceNameCell: Component<{
|
||||
// etc.) rather than from the generic resource.status triad.
|
||||
indicator?: StatusIndicator;
|
||||
detailToggle?: JSX.Element;
|
||||
// Image rows name the resource by its reference, so a narrow cell keeps the
|
||||
// repository and tag visible instead of the shared registry prefix.
|
||||
imageReference?: boolean;
|
||||
}> = (props) => {
|
||||
const resolvedIndicator = (): StatusIndicator =>
|
||||
props.indicator ?? getSimpleStatusIndicator(props.resource.status);
|
||||
@@ -88,8 +92,10 @@ export const DockerResourceNameCell: Component<{
|
||||
name={name()}
|
||||
url={props.resource.customUrl}
|
||||
class="min-w-0 max-[359px]:[&>a]:hidden"
|
||||
nameClass="truncate font-semibold text-base-content"
|
||||
/>
|
||||
nameClass={`${props.imageReference ? 'flex min-w-0' : 'truncate'} font-semibold text-base-content`}
|
||||
>
|
||||
{props.imageReference ? <DockerImageReferenceText reference={name()} /> : undefined}
|
||||
</ResourceNameWithWebInterfaceLink>
|
||||
</div>
|
||||
</TableCell>
|
||||
);
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
mapDockerServiceStatus,
|
||||
type DockerResourceStatusFilter,
|
||||
} from './dockerPageModel';
|
||||
import { DockerImageReferenceText } from './DockerImageReferenceText';
|
||||
|
||||
// Docker Swarm services are cluster-scoped declarations, not running
|
||||
// processes — they have no CPU / Memory / Disk / Disk I/O / Uptime /
|
||||
@@ -338,9 +339,7 @@ export const DockerServicesTable: Component<{
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content md:table-cell`}
|
||||
>
|
||||
<span class="block truncate" title={image()}>
|
||||
{image()}
|
||||
</span>
|
||||
<DockerImageReferenceText reference={image()} class="max-w-full" />
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} platform-table-phone-hidden text-base-content`}
|
||||
|
||||
@@ -1114,6 +1114,64 @@ describe('Docker native tables', () => {
|
||||
expect(screen.getByTitle('Healthy')).toHaveClass('bg-emerald-500');
|
||||
});
|
||||
|
||||
it('keeps the repository and tag of a registry-prefixed image name visible', () => {
|
||||
const { container } = render(() => (
|
||||
<DockerImagesTable
|
||||
resources={[
|
||||
makeResource({
|
||||
id: 'image-ghcr',
|
||||
type: 'docker-image',
|
||||
name: 'ghcr.io/pulse-demo/backup-coordinator:2026.04',
|
||||
}),
|
||||
makeResource({ id: 'image-hub', type: 'docker-image', name: 'postgres:16.4' }),
|
||||
]}
|
||||
emptyIcon={<span />}
|
||||
emptyTitle="No images"
|
||||
emptyDescription="No images"
|
||||
showToolbar={false}
|
||||
/>
|
||||
));
|
||||
|
||||
// Images that share a registry and namespace differ only at the end, so a
|
||||
// narrow name cell truncates the shared head and keeps the tail visible.
|
||||
const references = [
|
||||
...container.querySelectorAll<HTMLElement>('[data-docker-image-reference]'),
|
||||
];
|
||||
const prefixed = references.find(
|
||||
(element) => element.title === 'ghcr.io/pulse-demo/backup-coordinator:2026.04',
|
||||
);
|
||||
expect(prefixed).toHaveTextContent('ghcr.io/pulse-demo/backup-coordinator:2026.04');
|
||||
expect(prefixed?.querySelector('[data-docker-image-reference-tail]')).toHaveTextContent(
|
||||
'backup-coordinator:2026.04',
|
||||
);
|
||||
const bare = references.find((element) => element.title === 'postgres:16.4');
|
||||
expect(bare).toHaveTextContent('postgres:16.4');
|
||||
expect(bare?.querySelector('[data-docker-image-reference-tail]')?.textContent).toBe('');
|
||||
|
||||
// Head and tail are flex items, so a browser copy would put a line break
|
||||
// between them; a selection inside one reference copies as the reference.
|
||||
const copy = (selected: Node) => {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(selected);
|
||||
window.getSelection()?.removeAllRanges();
|
||||
window.getSelection()?.addRange(range);
|
||||
const setData = vi.fn();
|
||||
const event = new Event('copy', { bubbles: true, cancelable: true });
|
||||
Object.defineProperty(event, 'clipboardData', { value: { setData } });
|
||||
prefixed!.dispatchEvent(event);
|
||||
return { setData, prevented: event.defaultPrevented };
|
||||
};
|
||||
const inside = copy(prefixed!);
|
||||
expect(inside.setData).toHaveBeenCalledWith(
|
||||
'text/plain',
|
||||
'ghcr.io/pulse-demo/backup-coordinator:2026.04',
|
||||
);
|
||||
expect(inside.prevented).toBe(true);
|
||||
const wider = copy(container);
|
||||
expect(wider.setData).not.toHaveBeenCalled();
|
||||
expect(wider.prevented).toBe(false);
|
||||
});
|
||||
|
||||
it('renders Docker image API fields', () => {
|
||||
render(() => (
|
||||
<DockerImagesTable
|
||||
@@ -1560,7 +1618,13 @@ describe('Docker native tables', () => {
|
||||
expect(screen.getByText('Stack')).toBeInTheDocument();
|
||||
expect(screen.getByText('shop')).toBeInTheDocument();
|
||||
expect(screen.getByText('checkout-api')).toBeInTheDocument();
|
||||
expect(screen.getByText('registry.example.com/checkout-api:2026.05')).toBeInTheDocument();
|
||||
// The image keeps its repository and tag visible when the cell truncates;
|
||||
// the full reference stays on the title.
|
||||
const serviceImage = screen.getByTitle('registry.example.com/checkout-api:2026.05');
|
||||
expect(serviceImage).toHaveTextContent('registry.example.com/checkout-api:2026.05');
|
||||
expect(serviceImage.querySelector('[data-docker-image-reference-tail]')).toHaveTextContent(
|
||||
'checkout-api:2026.05',
|
||||
);
|
||||
expect(screen.getByText('replicated')).toBeInTheDocument();
|
||||
expect(screen.getByText('4')).toBeInTheDocument();
|
||||
expect(screen.getByText('2')).toBeInTheDocument();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getDockerImageRegistryLink } from '../dockerImageReference';
|
||||
import { getDockerImageRegistryLink, splitDockerImageReference } from '../dockerImageReference';
|
||||
|
||||
describe('getDockerImageRegistryLink', () => {
|
||||
it.each([
|
||||
@@ -35,3 +35,26 @@ describe('getDockerImageRegistryLink', () => {
|
||||
expect(getDockerImageRegistryLink(image)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('splitDockerImageReference', () => {
|
||||
it.each([
|
||||
[
|
||||
'ghcr.io/pulse-demo/backup-coordinator:2026.04',
|
||||
'ghcr.io/pulse-demo/',
|
||||
'backup-coordinator:2026.04',
|
||||
],
|
||||
['lscr.io/linuxserver/sonarr:latest', 'lscr.io/linuxserver/', 'sonarr:latest'],
|
||||
['registry.example.com:5000/team/app:1.2', 'registry.example.com:5000/team/', 'app:1.2'],
|
||||
['louislam/uptime-kuma:1.23.16', 'louislam/', 'uptime-kuma:1.23.16'],
|
||||
['quay.io/org/app@sha256:abc/def', 'quay.io/org/', 'app@sha256:abc/def'],
|
||||
])('keeps the repository and tag of %s in the tail', (reference, head, tail) => {
|
||||
expect(splitDockerImageReference(reference)).toEqual({ head, tail });
|
||||
});
|
||||
|
||||
it.each(['postgres:16.4', 'sha256:0123456789ab', '<none>:<none>', '—', '/app', 'team/'])(
|
||||
'leaves %s whole when it has no repository path to split',
|
||||
(reference) => {
|
||||
expect(splitDockerImageReference(reference)).toEqual({ head: reference, tail: '' });
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -78,3 +78,18 @@ export function getDockerImageRegistryLink(
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits an image reference before its repository name, so a narrow cell can
|
||||
* truncate the shared registry and namespace and keep the part that tells
|
||||
* images apart: `ghcr.io/pulse-demo/` + `backup-coordinator:2026.04`. A digest
|
||||
* never moves the split. A reference without a path keeps everything in the
|
||||
* head, like a Kubernetes name without a tail.
|
||||
*/
|
||||
export function splitDockerImageReference(reference: string): { head: string; tail: string } {
|
||||
const digestStart = reference.indexOf('@');
|
||||
const path = digestStart >= 0 ? reference.slice(0, digestStart) : reference;
|
||||
const cut = path.lastIndexOf('/');
|
||||
if (cut <= 0 || cut === path.length - 1) return { head: reference, tail: '' };
|
||||
return { head: reference.slice(0, cut + 1), tail: reference.slice(cut + 1) };
|
||||
}
|
||||
|
||||
+11
@@ -410,6 +410,17 @@ describe('platform overview layout guardrails', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps Docker image references readable by their repository and tag', () => {
|
||||
// References that share a registry and namespace differ only at the end, so
|
||||
// every image cell truncates the shared head and keeps name:tag visible.
|
||||
expect(dockerImagesTableSource).toMatch(/<DockerResourceNameCell[\s\S]{0,80}?imageReference/);
|
||||
for (const source of [dockerContainersTableSource, dockerServicesTableSource]) {
|
||||
expect(source).toContain(
|
||||
'<DockerImageReferenceText reference={image()} class="max-w-full" />',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps platform summary cells truncating inside their own column', () => {
|
||||
// A capped inline-block (max-w-52 truncate) ignored the column width, so a
|
||||
// long value spilled past a narrow cell without an ellipsis and sat above
|
||||
|
||||
Reference in New Issue
Block a user