mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
Stop the Restarts and Lifecycle headers clipping at tablet widths
Docker containers clipped its Restarts header to "RES..." between 600
and 800px, and widening the column clipped Memory instead. Below a 720px
table the count now waits in the row drawer, as it already did on
phones, and a running container that restarted more than five times
says so in its State cell there ("7 restarts"), so the crash-loop
warning stays visible on narrow screens; State sorts by that same text.
Kubernetes Configuration clipped "Lifecycle / trust" to "LIFECYCLE /
..." through the tablet band. The shared responsive header label gains
compactInBand, which keeps the compact label it already shows on phones
("Status") through that band; other headers are unchanged.
This commit is contained in:
@@ -2331,6 +2331,10 @@ not a replacement status card, CTA band, or page-local nested card.
|
||||
`platformTableBandWidthStyle` or the sortable header's `bandWidth`, which
|
||||
sets `--platform-table-band-width`; the shell applies it only inside the band,
|
||||
after the name anchor, so md widths and phone widths are unchanged.
|
||||
A header whose full label outgrows its band column (Kubernetes
|
||||
Configuration's `Lifecycle / trust`) passes `compactInBand` to
|
||||
`PlatformResponsiveTableLabel`, which keeps the compact label it already
|
||||
shows on phones through the band; other headers switch at 34rem as before.
|
||||
A column whose content must read in full (a status badge) is sized for its
|
||||
widest label at the narrowest table in each range: a 34rem container for the
|
||||
band, and the table a 768px viewport leaves (about 695px) for md widths. The
|
||||
|
||||
@@ -6034,6 +6034,10 @@ than the unified online/offline status, a running container's health check
|
||||
gets its own row (Failing, Starting, Passing), and Uptime is the current run,
|
||||
so a stopped container shows neither a stale health check nor leftover
|
||||
uptime. Both surfaces read `dockerContainerStatePresentation.ts`.
|
||||
Below a 720px table the containers table has no Restarts column (its header
|
||||
cannot fit beside State, the metric bars and the Update control), so a running
|
||||
container that restarted more than five times says `N restarts` in its State
|
||||
cell there instead; wider tables keep the count in its own column.
|
||||
|
||||
ResourceDetailDrawer headings wrap long display names rather than ellipsising
|
||||
them. Overview rows may still truncate identity; keyboard expansion exposes the
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "ce3262f4230aedc10acbd38dfb0482e6f0bbb298",
|
||||
"verified_at": "2026-10-06T08:14:26Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/features/docker/DockerContainersTable.tsx",
|
||||
"frontend-modern/src/features/docker/dockerContainerStatePresentation.ts",
|
||||
"frontend-modern/src/features/docker/dockerContainerTableModel.ts",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx",
|
||||
"frontend-modern/src/features/platformPage/sharedPlatformPage.tsx",
|
||||
"frontend-modern/src/index.css"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/features/docker/DockerContainersTable.tsx": "575efffccfc2285e11d497eaae3de3a7dd0dbbb1ba4c02a6d4adbc296b02ab2e",
|
||||
"frontend-modern/src/features/docker/dockerContainerStatePresentation.ts": "7316bb3852c75bb135025ba245e7a55d3014b9b5581d7fe0410b4fb9be6cdea7",
|
||||
"frontend-modern/src/features/docker/dockerContainerTableModel.ts": "0fac403071fbf75093e0d32425863c599471c7424ecb89f6d7ca91f3ea8930ae",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx": "6f59b8f00077029fd2a19298bb72ec7654c06b0600533ed91d84592d63278b18",
|
||||
"frontend-modern/src/features/platformPage/sharedPlatformPage.tsx": "717decd0b40532f47653d17cd13fe119acf4559aeb831a0b7515773259829807",
|
||||
"frontend-modern/src/index.css": "4884f5bed3239c2e06ddd2415a0b0c63eedd1ada8bee84d9e1d3ff534c65fe74"
|
||||
},
|
||||
"routes": [
|
||||
"/docker/containers",
|
||||
"/kubernetes/configuration"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 390,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 430,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 600,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 650,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 700,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 767,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 800,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 1000,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 1280,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 1600,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 1920,
|
||||
"height": 1000
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"Docker containers header sweep 390-1920 against main: main clipped the Restarts header at 600 (47px, RES...) and 800 (70px); the change has no clipped header at any width and no column's cell clips grew",
|
||||
"Below a 720px table Restarts moves to the row drawer as it already did on phones; at 800 it returns with 78px and its header whole",
|
||||
"A running, healthy container with 7 restarts (API response edited): at 600 its State cell reads 7 restarts in amber (95px, not clipped); at 1440 State reads Healthy and the Restarts column shows the count",
|
||||
"Kubernetes Configuration at 600/650/700/767: main clipped the Lifecycle / trust header to LIFECYCLE / ...; the change shows its compact Status label through the tablet band, with no header clip, and the full label from md widths up",
|
||||
"Re-checked after rebasing onto main with the Docker drawer health change (#2534): no clipped headers at 600/800/1440, the 7 restarts State warning at 600, the Kubernetes Lifecycle header at 700, and the drawer Running / Health check Failing rows unchanged"
|
||||
],
|
||||
"interactions": [
|
||||
"Measured header and cell clips on main (reverse-applied patch) and on the change",
|
||||
"Served an edited /api/resources response with the websocket held"
|
||||
]
|
||||
}
|
||||
@@ -302,6 +302,18 @@ describe('App architecture', () => {
|
||||
expect(band).toMatch(
|
||||
/width: 30%;\s*\}[\s\S]*?th\[style\*='--platform-table-band-width'\] \{\s*width: var\(--platform-table-band-width\);/,
|
||||
);
|
||||
// A header whose full label outgrows its band column keeps its compact
|
||||
// label there; these rules sit after the 34rem label switch so they win.
|
||||
expect(band).toMatch(
|
||||
/--platform-table-band-width\);\s*\}[\s\S]*?\.platform-table-label-compact\.platform-table-label-band-compact \{\s*display: inline;\s*\}\s*\.platform-table-label-full\.platform-table-label-band-compact \{\s*display: none;/,
|
||||
);
|
||||
const labelSwitch = appStylesSource.indexOf(
|
||||
'@container (min-width: 34rem) {\n .platform-table-label-compact',
|
||||
);
|
||||
expect(labelSwitch).toBeGreaterThan(0);
|
||||
expect(appStylesSource.indexOf('.platform-table-label-band-compact')).toBeGreaterThan(
|
||||
labelSwitch,
|
||||
);
|
||||
});
|
||||
|
||||
it('draws the shared select arrow that appearance-none removes', () => {
|
||||
|
||||
@@ -82,6 +82,7 @@ import {
|
||||
getDockerContainerSortKey,
|
||||
getDockerContainerTableMinWidthClass,
|
||||
getDockerContainerVisibleColumnsForLayout,
|
||||
isDockerContainerColumnInLayout,
|
||||
type DockerContainerSortKey,
|
||||
type DockerContainerTableColumn,
|
||||
} from './dockerContainerTableModel';
|
||||
@@ -91,6 +92,7 @@ import { DockerContainerLifecycleControls } from './DockerContainerLifecycleCont
|
||||
import { CONTAINER_CPU_CAPACITY_DESCRIPTION } from './dockerCpuPresentation';
|
||||
import { DockerImageReferenceText } from './DockerImageReferenceText';
|
||||
import {
|
||||
DOCKER_RESTART_ATTENTION_THRESHOLD,
|
||||
getDockerContainerStatePresentation,
|
||||
getDockerContainerUptimeSeconds,
|
||||
type DockerContainerStateTone,
|
||||
@@ -188,10 +190,6 @@ const DOCKER_CONTAINER_STATE_TONE_CLASS: Record<DockerContainerStateTone | 'none
|
||||
none: '',
|
||||
};
|
||||
|
||||
// v5 flagged crash-loopers in the restarts column; a container that restarted
|
||||
// more than this many times needs an operator's eye even while "running".
|
||||
const RESTART_ATTENTION_THRESHOLD = 5;
|
||||
|
||||
const updateStatusLabel = (resource: Resource): string => {
|
||||
const update = resource.docker?.updateStatus;
|
||||
if (!update) return '—';
|
||||
@@ -424,6 +422,13 @@ export const DockerContainersTable: Component<DockerContainersTableProps> = (pro
|
||||
// User-controlled sorting layered over the attention-first default: rows
|
||||
// are pre-sorted by the status compare, so a user sort keeps that order for
|
||||
// ties and the table falls straight back to it when the sort is cleared.
|
||||
// Where the layout has no Restarts column, a crash-looping container says
|
||||
// so in its State cell instead.
|
||||
const flagRestartsInState = createMemo(
|
||||
() => !isDockerContainerColumnInLayout('restarts', layoutMode()),
|
||||
);
|
||||
const stateFor = (resource: Resource) =>
|
||||
getDockerContainerStatePresentation(resource, { flagRestarts: flagRestartsInState() });
|
||||
// Sorting reorders rows only; grouped mode re-buckets the sorted rows, so
|
||||
// the sort applies within each host group and stays orthogonal to grouping.
|
||||
const sort = createPlatformTableSortState({
|
||||
@@ -432,7 +437,10 @@ export const DockerContainersTable: Component<DockerContainersTableProps> = (pro
|
||||
descendingFirst: ['cpu', 'memory', 'restarts', 'uptime'],
|
||||
});
|
||||
const sortedRows = createMemo(() =>
|
||||
sort.sortRows([...scopedRows()].sort(compareDockerContainers), getDockerContainerSortValue),
|
||||
sort.sortRows([...scopedRows()].sort(compareDockerContainers), (resource, key) =>
|
||||
// State sorts by the words the row shows, restart warning included.
|
||||
key === 'state' ? stateFor(resource).label : getDockerContainerSortValue(resource, key),
|
||||
),
|
||||
);
|
||||
// Grouped-by-host view, mirroring the workloads table: preference persists
|
||||
// locally (not in the URL) and only applies once the fleet spans more than
|
||||
@@ -510,7 +518,7 @@ export const DockerContainersTable: Component<DockerContainersTableProps> = (pro
|
||||
// A running container with a failing or starting health check needs the
|
||||
// State column as much as a stopped one does.
|
||||
const showStateColumn = createMemo(() =>
|
||||
scopedRows().some((resource) => getDockerContainerStatePresentation(resource).tone !== null),
|
||||
scopedRows().some((resource) => stateFor(resource).tone !== null),
|
||||
);
|
||||
const showUptimeColumn = createMemo(() =>
|
||||
scopedRows().some((resource) => getDockerContainerUptimeSeconds(resource) !== undefined),
|
||||
@@ -536,7 +544,7 @@ export const DockerContainersTable: Component<DockerContainersTableProps> = (pro
|
||||
const renderContainerRow = (resource: Resource): JSX.Element => {
|
||||
const indicator = mapDockerContainerStatus(resource);
|
||||
const image = () => dockerTextValue(resource.docker?.image);
|
||||
const state = () => getDockerContainerStatePresentation(resource);
|
||||
const state = () => stateFor(resource);
|
||||
const runtime = () => runtimeSummary(resource);
|
||||
const host = () => dockerHostName(resource);
|
||||
const running = () => isContainerRunning(resource);
|
||||
@@ -659,7 +667,7 @@ export const DockerContainersTable: Component<DockerContainersTableProps> = (pro
|
||||
>
|
||||
<span
|
||||
class={`tabular-nums ${
|
||||
restartCount() > RESTART_ATTENTION_THRESHOLD
|
||||
restartCount() > DOCKER_RESTART_ATTENTION_THRESHOLD
|
||||
? 'font-medium text-red-600 dark:text-red-400'
|
||||
: ''
|
||||
}`}
|
||||
|
||||
@@ -965,6 +965,72 @@ describe('Docker native tables', () => {
|
||||
expect(screen.getByText('Exited (1)')).toHaveAttribute('data-docker-container-state', 'danger');
|
||||
});
|
||||
|
||||
it('flags a crash-looping container in State where the Restarts column is hidden', () => {
|
||||
const rows = [
|
||||
makeResource({
|
||||
id: 'container-1',
|
||||
type: 'app-container',
|
||||
name: 'edge-web',
|
||||
status: 'running',
|
||||
docker: { hostname: 'edge-01', containerState: 'running', restartCount: 7 },
|
||||
}),
|
||||
makeResource({
|
||||
id: 'container-2',
|
||||
type: 'app-container',
|
||||
name: 'edge-api',
|
||||
status: 'running',
|
||||
docker: { hostname: 'edge-01', containerState: 'running', restartCount: 1 },
|
||||
}),
|
||||
];
|
||||
// 600px is the mobile layout, which has no Restarts column.
|
||||
setViewportWidth(600);
|
||||
const { unmount } = renderInRouter(() => (
|
||||
<DockerContainersTable
|
||||
resources={rows}
|
||||
emptyIcon={<span />}
|
||||
emptyTitle="No containers"
|
||||
emptyDescription="No containers"
|
||||
showToolbar={false}
|
||||
/>
|
||||
));
|
||||
expect(screen.queryByText('Restarts')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('7 restarts')).toHaveAttribute(
|
||||
'data-docker-container-state',
|
||||
'warning',
|
||||
);
|
||||
// State sorts by the words the row shows: "7 restarts" ranks apart from
|
||||
// "Running" both ways (with equal labels both orders would be the same).
|
||||
const rowOrder = () =>
|
||||
[...document.querySelectorAll('[data-docker-container-row]')].map((row) =>
|
||||
row.getAttribute('data-docker-container-row'),
|
||||
);
|
||||
const clickState = () => {
|
||||
const header = screen
|
||||
.getAllByRole('columnheader')
|
||||
.find((th) => th.textContent?.trim().startsWith('State'));
|
||||
fireEvent.click(header!.querySelector('button') ?? header!);
|
||||
};
|
||||
clickState();
|
||||
expect(rowOrder()).toEqual(['container-1', 'container-2']);
|
||||
clickState();
|
||||
expect(rowOrder()).toEqual(['container-2', 'container-1']);
|
||||
unmount();
|
||||
|
||||
// A wide table shows the count in its own column instead.
|
||||
setViewportWidth(WORKLOAD_TABLE_WIDE_LAYOUT_WIDTH);
|
||||
renderInRouter(() => (
|
||||
<DockerContainersTable
|
||||
resources={rows}
|
||||
emptyIcon={<span />}
|
||||
emptyTitle="No containers"
|
||||
emptyDescription="No containers"
|
||||
showToolbar={false}
|
||||
/>
|
||||
));
|
||||
expect(screen.getByText('Restarts')).toBeInTheDocument();
|
||||
expect(screen.queryByText('7 restarts')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('leaves out the Uptime column when no container reports one', () => {
|
||||
setViewportWidth(WORKLOAD_TABLE_WIDE_LAYOUT_WIDTH);
|
||||
|
||||
|
||||
+27
@@ -158,3 +158,30 @@ describe('drawer runtime rows', () => {
|
||||
expect(getDockerContainerHealthCheckPresentation(crashed)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('restart warning where the Restarts column is hidden', () => {
|
||||
it('names a crash-looping running container in State only when asked', () => {
|
||||
const looping = container({ containerState: 'running', health: 'healthy', restartCount: 7 });
|
||||
expect(getDockerContainerStatePresentation(looping, { flagRestarts: true })).toEqual({
|
||||
label: '7 restarts',
|
||||
title: 'Running, but Docker has restarted it 7 times',
|
||||
tone: 'warning',
|
||||
});
|
||||
expect(getDockerContainerStatePresentation(looping)).toMatchObject({ label: 'Healthy' });
|
||||
expect(
|
||||
getDockerContainerStatePresentation(
|
||||
container({ containerState: 'running', restartCount: 5 }),
|
||||
{ flagRestarts: true },
|
||||
),
|
||||
).toMatchObject({ label: 'Running', tone: null });
|
||||
});
|
||||
|
||||
it('keeps a worse state ahead of the restart count', () => {
|
||||
expect(
|
||||
getDockerContainerStatePresentation(
|
||||
container({ containerState: 'running', health: 'unhealthy', restartCount: 9 }),
|
||||
{ flagRestarts: true },
|
||||
),
|
||||
).toMatchObject({ label: 'Unhealthy', tone: 'danger' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,7 +57,8 @@ describe('dockerContainerTableModel', () => {
|
||||
const columns = getDockerContainerVisibleColumnsForLayout('mobile', true, true, true);
|
||||
const ids = columns.map((column) => column.id);
|
||||
|
||||
expect(ids).toEqual(['container', 'state', 'cpu', 'memory', 'restarts', 'updates', 'actions']);
|
||||
// Restarts waits for the tablet layout (its header clips below 720px).
|
||||
expect(ids).toEqual(['container', 'state', 'cpu', 'memory', 'updates', 'actions']);
|
||||
expect(getDockerContainerTableMinWidthClass()).toBe('min-w-full');
|
||||
expect(getDockerContainerColumnWidthStyle('container', 'mobile', ids)).toEqual({
|
||||
width: '30%',
|
||||
@@ -65,10 +66,10 @@ describe('dockerContainerTableModel', () => {
|
||||
// State spells out a problem ("Exited (139)", about 77px with padding), so
|
||||
// it takes the room the memory bar did not need.
|
||||
expect(getDockerContainerColumnWidthStyle('state', 'mobile', ids)).toEqual({
|
||||
width: '14.5%',
|
||||
width: '16.371%',
|
||||
});
|
||||
expect(getDockerContainerColumnWidthStyle('memory', 'mobile', ids)).toEqual({
|
||||
width: '13.5%',
|
||||
width: '15.2419%',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -120,6 +121,23 @@ describe('dockerContainerTableModel', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('gives the Restarts header room on tablets when one host or groups hide Host', () => {
|
||||
// "Restarts" is about 61px at 11px uppercase with 6px of padding a side.
|
||||
for (const hostOption of [{ groupedByHost: true }, { singleHost: true }]) {
|
||||
const ids = getDockerContainerVisibleColumnsForLayout(
|
||||
'tablet',
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
hostOption,
|
||||
).map((column) => column.id);
|
||||
const width = Number.parseFloat(
|
||||
String(getDockerContainerColumnWidthStyle('restarts', 'tablet', ids).width),
|
||||
);
|
||||
expect((width / 100) * 720).toBeGreaterThanOrEqual(73);
|
||||
}
|
||||
});
|
||||
|
||||
it('drops uptime when no container in view reports one', () => {
|
||||
const ids = getDockerContainerVisibleColumnsForLayout('compact', true, true, true, {
|
||||
includeUptime: false,
|
||||
|
||||
@@ -48,11 +48,31 @@ const runStateTitle = (resource: Resource): string | undefined => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// v5 flagged crash-loopers in the restarts column; a container that restarted
|
||||
// more than this many times needs an operator's eye even while "running".
|
||||
export const DOCKER_RESTART_ATTENTION_THRESHOLD = 5;
|
||||
|
||||
// flagRestarts: the row shows no Restarts column (narrow layouts), so a
|
||||
// running container past the restart threshold says so in its State cell.
|
||||
export const getDockerContainerStatePresentation = (
|
||||
resource: Resource,
|
||||
options: { flagRestarts?: boolean } = {},
|
||||
): DockerContainerStatePresentation => {
|
||||
const status = mapDockerContainerStatus(resource);
|
||||
const tone = presentationTone(status.variant);
|
||||
const restarts = resource.docker?.restartCount;
|
||||
if (
|
||||
options.flagRestarts &&
|
||||
tone === null &&
|
||||
typeof restarts === 'number' &&
|
||||
restarts > DOCKER_RESTART_ATTENTION_THRESHOLD
|
||||
) {
|
||||
return {
|
||||
label: `${restarts} restarts`,
|
||||
title: `Running, but Docker has restarted it ${restarts} times`,
|
||||
tone: 'warning',
|
||||
};
|
||||
}
|
||||
return { label: status.label, title: containerStateTitle(resource, status.label), tone };
|
||||
};
|
||||
|
||||
|
||||
@@ -72,9 +72,10 @@ const DOCKER_CONTAINER_COLUMN_MIN_LAYOUT: Record<
|
||||
state: 'narrow',
|
||||
cpu: 'narrow',
|
||||
memory: 'narrow',
|
||||
// A phone row cannot fit the Restarts header beside the Update control;
|
||||
// the count stays in the row expansion there and returns at mobile width.
|
||||
restarts: 'mobile',
|
||||
// Below a 720px table the Restarts header cannot fit beside State, the
|
||||
// metric bars and the Update control without clipping one of them; the
|
||||
// count stays in the row expansion there and returns at tablet width.
|
||||
restarts: 'tablet',
|
||||
// Uptime is the current run, so a short one shows a container that just
|
||||
// restarted. Its header needs about 57px, which a tablet row cannot spare
|
||||
// beside State, Restarts and the Update control; the drawer keeps it there.
|
||||
@@ -160,7 +161,6 @@ const DOCKER_CONTAINER_RESPONSIVE_WIDTHS: Record<
|
||||
state: 14.5,
|
||||
cpu: 10,
|
||||
memory: 13.5,
|
||||
restarts: 8,
|
||||
updates: 16.25,
|
||||
actions: 7.75,
|
||||
},
|
||||
@@ -169,8 +169,8 @@ const DOCKER_CONTAINER_RESPONSIVE_WIDTHS: Record<
|
||||
host: 15,
|
||||
state: 14,
|
||||
cpu: 9.5,
|
||||
memory: 13,
|
||||
restarts: 9,
|
||||
memory: 12,
|
||||
restarts: 10,
|
||||
updates: 14.5,
|
||||
actions: 6,
|
||||
},
|
||||
@@ -193,6 +193,15 @@ const DOCKER_CONTAINER_RESPONSIVE_WIDTHS: Record<
|
||||
},
|
||||
};
|
||||
|
||||
// Whether a layout has room for a column at all (before row-set gates such
|
||||
// as "only when some container restarted").
|
||||
export const isDockerContainerColumnInLayout = (
|
||||
columnId: DockerContainerTableColumnId,
|
||||
layoutMode: WorkloadTableLayoutMode,
|
||||
): boolean =>
|
||||
DOCKER_CONTAINER_TABLE_LAYOUT_ORDER[DOCKER_CONTAINER_COLUMN_MIN_LAYOUT[columnId]] <=
|
||||
DOCKER_CONTAINER_TABLE_LAYOUT_ORDER[layoutMode];
|
||||
|
||||
export const getDockerContainerVisibleColumnsForLayout = (
|
||||
layoutMode: WorkloadTableLayoutMode,
|
||||
includeRuntime: boolean,
|
||||
|
||||
@@ -297,7 +297,11 @@ export const KubernetesConfigTable: Component<{
|
||||
class="platform-table-phone-hidden md:w-[17%]"
|
||||
bandWidth={16}
|
||||
>
|
||||
<PlatformResponsiveTableLabel compact="Status" full="Lifecycle / trust" />
|
||||
<PlatformResponsiveTableLabel
|
||||
compact="Status"
|
||||
full="Lifecycle / trust"
|
||||
compactInBand
|
||||
/>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
|
||||
+12
@@ -21,6 +21,7 @@ import kubernetesAlertsTableSource from '@/features/kubernetes/KubernetesAlertsT
|
||||
import kubernetesAutoscalingTableSource from '@/features/kubernetes/KubernetesAutoscalingTable.tsx?raw';
|
||||
import kubernetesClustersTableSource from '@/features/kubernetes/KubernetesClustersTable.tsx?raw';
|
||||
import kubernetesConfigTableSource from '@/features/kubernetes/KubernetesConfigTable.tsx?raw';
|
||||
import dockerContainerTableModelSource from '@/features/docker/dockerContainerTableModel.ts?raw';
|
||||
import kubernetesControllersTableSource from '@/features/kubernetes/KubernetesControllersTable.tsx?raw';
|
||||
import kubernetesDeploymentsTableSource from '@/features/kubernetes/KubernetesDeploymentsTable.tsx?raw';
|
||||
import kubernetesEventsTableSource from '@/features/kubernetes/KubernetesEventsTable.tsx?raw';
|
||||
@@ -453,6 +454,17 @@ describe('platform overview layout guardrails', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps long tablet headers from clipping', () => {
|
||||
// "Lifecycle / trust" outgrows its tablet-band column, so it keeps the
|
||||
// compact label it shows on phones through that band.
|
||||
expect(kubernetesConfigTableSource).toMatch(
|
||||
/compact="Status"\s+full="Lifecycle \/ trust"\s+compactInBand/,
|
||||
);
|
||||
// The Docker Restarts header cannot fit below a 720px table; the count
|
||||
// waits in the drawer there and a crash-looper says so in State.
|
||||
expect(dockerContainerTableModelSource).toMatch(/restarts: 'tablet',/);
|
||||
});
|
||||
|
||||
it('keeps phone-priority visibility container-led and symmetric across table rows', () => {
|
||||
expect(indexCssSource).toContain(':is(th, td).platform-table-phone-hidden');
|
||||
expect(indexCssSource).toContain(':is(th, td).platform-table-phone-only');
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
withPlatformStatusCounts,
|
||||
type PlatformResourceStatusFilter,
|
||||
platformTableBandWidthStyle,
|
||||
PlatformResponsiveTableLabel,
|
||||
} from '../sharedPlatformPage';
|
||||
|
||||
afterEach(() => {
|
||||
@@ -924,4 +925,23 @@ describe('PlatformTableMetricFallback', () => {
|
||||
expect(platformTableBandWidthStyle(9.5)).toEqual({ '--platform-table-band-width': '9.5%' });
|
||||
expect(platformTableBandWidthStyle(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('lets a long header keep its compact label through the tablet band', () => {
|
||||
render(() =>
|
||||
createComponent(PlatformResponsiveTableLabel, {
|
||||
compact: 'Status',
|
||||
full: 'Lifecycle / trust',
|
||||
compactInBand: true,
|
||||
}),
|
||||
);
|
||||
expect(screen.getByText('Status').className).toBe(
|
||||
'platform-table-label-compact platform-table-label-band-compact',
|
||||
);
|
||||
expect(screen.getByText('Lifecycle / trust').className).toBe(
|
||||
'platform-table-label-full platform-table-label-band-compact',
|
||||
);
|
||||
cleanup();
|
||||
render(() => createComponent(PlatformResponsiveTableLabel, { compact: 'Rdy', full: 'Ready' }));
|
||||
expect(screen.getByText('Ready').className).toBe('platform-table-label-full');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -462,11 +462,18 @@ export const getPlatformTableCellClassForKind = (kind: PlatformTableColumnKind):
|
||||
// can expand without weakening the overview-table contract.
|
||||
export const getPlatformTableRowClass = (): string => PLATFORM_TABLE_SUMMARY_ROW_CLASS;
|
||||
|
||||
export function PlatformResponsiveTableLabel(props: { compact: string; full: string }) {
|
||||
// compactInBand keeps the compact label through the tablet band (a 34rem
|
||||
// container below a 768px viewport) for a full label longer than its column.
|
||||
export function PlatformResponsiveTableLabel(props: {
|
||||
compact: string;
|
||||
full: string;
|
||||
compactInBand?: boolean;
|
||||
}) {
|
||||
const band = () => (props.compactInBand ? ' platform-table-label-band-compact' : '');
|
||||
return (
|
||||
<>
|
||||
<span class="platform-table-label-compact">{props.compact}</span>
|
||||
<span class="platform-table-label-full">{props.full}</span>
|
||||
<span class={`platform-table-label-compact${band()}`}>{props.compact}</span>
|
||||
<span class={`platform-table-label-full${band()}`}>{props.full}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1489,6 +1489,16 @@
|
||||
.table-scroll-shell > .table-fixed.platform-table th[style*='--platform-table-band-width'] {
|
||||
width: var(--platform-table-band-width);
|
||||
}
|
||||
|
||||
/* A header whose full label is longer than its band column keeps the
|
||||
compact label it already shows on phones (compactInBand). */
|
||||
.platform-table-label-compact.platform-table-label-band-compact {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.platform-table-label-full.platform-table-label-band-compact {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user