mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 04:33:17 +00:00
Stop Kubernetes names clipping at tablet widths
Between a 544px table container and a 768px viewport, platform tables show their phone-hidden columns again, but md:w widths are viewport media queries and do not apply yet, so every visible column took an equal share. A seven-column Kubernetes table left its name 83px, with the chevron and status dot inside it, so node names read "Pr..." and deployment names two letters. From 768px the Kubernetes name widths (14 to 19 percent) still cut common node, service and event names. The shared table shell now keeps the phone 30 percent name anchor in that band. Tables sized by a weighted colgroup are unaffected. Kubernetes name columns get room for typical names at 768px, with the Nodes columns rebalanced so Capacity fits at 1100px and wider. Generated names (pods, the node a pod runs on, controllers) truncate their head and keep the trailing segment that tells siblings apart, and that tail never outgrows its cell.
This commit is contained in:
@@ -2302,6 +2302,13 @@ not a replacement status card, CTA band, or page-local nested card.
|
||||
desktop table and inline detail. Consumers mark a normal-phone demotion with
|
||||
`platform-table-phone-hidden` on both its header and its body cell. The shared
|
||||
container rule owns the matching `th`/`td` visibility so the two cannot drift.
|
||||
Column visibility is container-led but `md:w-[...]` widths are viewport media
|
||||
queries, so between a 34rem container and a 768px viewport the phone-hidden
|
||||
columns return while no width applies, and every visible column took an equal
|
||||
share (a seven-column Kubernetes table left its name 83px). The shared shell
|
||||
keeps the 30 percent `platform-table-name-column` anchor in that band. Tables
|
||||
sized by a weighted colgroup keep their own widths there, since column widths
|
||||
win over header widths.
|
||||
Below a 360-pixel content width, the shared narrow layout promotes identity to
|
||||
40 percent and may remove one additional lowest-priority context column via
|
||||
`platform-table-narrow-hidden` rather than shrinking names and metric values
|
||||
|
||||
@@ -1166,6 +1166,17 @@ platform-details disclosure. The `KubernetesControllersTable` phone projection
|
||||
keeps controller, kind, ready, and issues and demotes Target with
|
||||
`platform-table-phone-hidden`, so kind labels such as `DaemonSet` and
|
||||
`StatefulSet` fit whole instead of clipping in a 15 percent track.
|
||||
Kubernetes name columns hold the chevron, status dot and name inside one
|
||||
track, so their md widths leave the name room for a typical node or service
|
||||
name at a 768px viewport: Nodes 20 percent (with Capacity at 16 so its
|
||||
cores, memory and pods label fits at 1100px and wider), Events 24, and
|
||||
Services, Configuration, Storage, Controllers and Policy 22. Generated names
|
||||
render through `KubernetesNameText`, which splits off the trailing segment
|
||||
(`splitKubernetesNameTail`) so a narrow cell truncates the head and keeps the
|
||||
part that tells siblings apart (`checkout-api-6d…-x7k2p`, `prod…-03`), with the
|
||||
full name on the title. Pod names, the Pods table's Node column and controller
|
||||
names use it. Deployment names are chosen by people, so they keep plain end
|
||||
truncation.
|
||||
Duration and interval cells keep the same split: unified-resource or
|
||||
source-specific consumers own which elapsed duration, human fallback, or poll
|
||||
interval field is meaningful, while dense platform table rendering must use
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "b69258f14f5c6174f7b979dac07e0c218b372403",
|
||||
"verified_at": "2026-10-05T09:28:53Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesNameText.tsx",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx",
|
||||
"frontend-modern/src/features/kubernetes/kubernetesPageModel.ts",
|
||||
"frontend-modern/src/index.css"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx": "3425465b2ef094146ac94e6b4ab2833e6601713a59200a3140d555d17e28e4e5",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx": "9a577f0f8cbe0437b80255a1a2ea41dd0c7e32b1c588464a4b89c0501d64a897",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx": "d4e9b2f2e83ef93fcc84946ea544dacb703e48dc2e181c2f66ae718af90af2c1",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesNameText.tsx": "d83694187f8d68d194e2474372f5d362b29f0baf5083edec1a057a327de05da2",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx": "4102884da1e308d07892ac25058f6e57831ecc20c6f4cd5da56171c42e8d55d5",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx": "82a32f2247138e7fb2db75bdc9efecb22249dd51b69f14aa98023f247a118c6c",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx": "22bddd9ff855dcce7a3d280688f8251d341ad77b7ca74bdbe901bd87f985dc20",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx": "0ab832b8c131ad9ae695c72e44cfbc92fcc37b0243217acba776b4af4ba5115e",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx": "f7b31d83a0c3377b32c7f893e0255221d075acb55654800d3a769c87358b3d71",
|
||||
"frontend-modern/src/features/kubernetes/kubernetesPageModel.ts": "1b84a2da0935626bdaf6819ca633cb4b1e2de1cf6d67d362081813c2f4b73277",
|
||||
"frontend-modern/src/index.css": "cc901783e7da07494d095b45d791e414803c659583ac5a1b447e1a38bf2c379c"
|
||||
},
|
||||
"routes": [
|
||||
"/kubernetes/overview",
|
||||
"/kubernetes/nodes",
|
||||
"/kubernetes/workloads",
|
||||
"/kubernetes/services",
|
||||
"/kubernetes/storage",
|
||||
"/kubernetes/configuration",
|
||||
"/kubernetes/events",
|
||||
"/truenas/storage",
|
||||
"/vmware/hosts",
|
||||
"/docker/hosts",
|
||||
"/proxmox/overview"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 600,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 700,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 768,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 820,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 900,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 1100,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 1000
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"Clip sweep (scrollWidth over clientWidth on every cell, every Kubernetes tab, 600 to 1440 wide): identity-column clips fall from 284 to 186, most of the rest being pod heads that now truncate by design with the generated tail kept",
|
||||
"Nodes at 700: Prod Euw1 K8s 03 reads in full, where main showed Pr..., and the Nodes table has no name clipping at any width from 600 to 1440 and no clipping at all at 1100 and 1440 (Capacity, Runtime and Roles fit)",
|
||||
"Workloads at 700: deployment names read in full (main cut them to two letters such as ac... and ch...), pods read checkout-a...-c7z2p and the Node column prod...-02",
|
||||
"Other platforms between 600 and 700: TrueNAS storage and vSphere resource name clips go from 14 to 0, Docker and vSphere host tables keep their names whole at about 37 percent, with no new clipping in any column; widths at 768 and up are unchanged there",
|
||||
"A ten-digit node tail forced into the Pods Node cell at 600 and 700 stays inside the cell and ends in an ellipsis"
|
||||
],
|
||||
"interactions": [
|
||||
"Ran the clip sweep on main and on the change across seven widths",
|
||||
"Injected an oversized tail into a live Node cell and measured it"
|
||||
]
|
||||
}
|
||||
@@ -274,6 +274,17 @@ describe('App architecture', () => {
|
||||
expect(appStylesSource).not.toContain('white-space: normal;\n -webkit-box-orient');
|
||||
});
|
||||
|
||||
it('keeps the identity anchor between the phone projection and the md column widths', () => {
|
||||
// Column visibility is container-led but md:w-[...] widths are viewport
|
||||
// media queries, so a 544-767px band showed every column with no widths and
|
||||
// split them equally (a Kubernetes node name got 83px). The shared shell
|
||||
// keeps the 30% name anchor there.
|
||||
const band = appStylesSource.slice(appStylesSource.indexOf('@media (width < 768px)'));
|
||||
expect(band).toMatch(
|
||||
/^@media \(width < 768px\) \{\s*@container \(min-width: 34rem\) \{\s*\.table-scroll-shell > \.table-fixed\.platform-table th\.platform-table-name-column \{\s*width: 30%;/,
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps compact phone settings spacing scoped to the settings content shell', () => {
|
||||
expect(appStylesSource).toContain('@media (max-width: 39.999rem)');
|
||||
expect(appStylesSource).toContain(
|
||||
|
||||
@@ -264,7 +264,7 @@ export const KubernetesConfigTable: Component<{
|
||||
kind="name"
|
||||
sort={sort}
|
||||
sortKey="resource"
|
||||
class="platform-table-mobile-w-30 md:w-[18%]"
|
||||
class="platform-table-mobile-w-30 md:w-[22%]"
|
||||
>
|
||||
Resource
|
||||
</PlatformSortableTableHead>
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
mapKubernetesControllerStatus,
|
||||
type KubernetesResourceStatusFilter,
|
||||
} from './kubernetesPageModel';
|
||||
import { KubernetesNameText } from './KubernetesNameText';
|
||||
|
||||
const controllerName = (resource: Resource): string =>
|
||||
asTrimmedString(resource.displayName) || asTrimmedString(resource.name) || resource.id;
|
||||
@@ -315,7 +316,7 @@ export const KubernetesControllersTable: Component<{
|
||||
kind="name"
|
||||
sort={sort}
|
||||
sortKey="controller"
|
||||
class="platform-table-mobile-w-30 md:w-[18%]"
|
||||
class="platform-table-mobile-w-30 md:w-[22%]"
|
||||
>
|
||||
Controller
|
||||
</PlatformSortableTableHead>
|
||||
@@ -424,9 +425,10 @@ export const KubernetesControllersTable: Component<{
|
||||
title={indicator().label}
|
||||
ariaHidden
|
||||
/>
|
||||
<span class="truncate font-semibold text-base-content" title={name()}>
|
||||
{name()}
|
||||
</span>
|
||||
<KubernetesNameText
|
||||
name={name()}
|
||||
class="font-semibold text-base-content"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
|
||||
@@ -113,7 +113,7 @@ export const KubernetesEventsTable: Component<{
|
||||
header={
|
||||
<>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('name')} platform-table-mobile-w-30 md:w-[16%]`}
|
||||
class={`${getPlatformTableHeadClassForKind('name')} platform-table-mobile-w-30 md:w-[24%]`}
|
||||
>
|
||||
Event
|
||||
</TableHead>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { Component } from 'solid-js';
|
||||
import { splitKubernetesNameTail } from './kubernetesPageModel';
|
||||
|
||||
/**
|
||||
* A Kubernetes name in a narrow cell. Generated and ordinal names end in the
|
||||
* part that tells siblings apart (checkout-api-6d8f9c7b5-x7k2p,
|
||||
* prod-euw1-k8s-03), so the head truncates and the tail stays visible:
|
||||
* checkout-api-6d…-x7k2p. The tail never outgrows the cell: when even it
|
||||
* does not fit, it truncates too once the head is gone. The full name stays on
|
||||
* the title, and the two spans read as one name to assistive technology.
|
||||
*/
|
||||
export const KubernetesNameText: Component<{ name: string; class?: string }> = (props) => {
|
||||
const parts = () => splitKubernetesNameTail(props.name);
|
||||
return (
|
||||
<span class={`flex min-w-0 ${props.class ?? ''}`} title={props.name} data-kubernetes-name>
|
||||
<span class="truncate">{parts().head}</span>
|
||||
<span class="max-w-full shrink-0 truncate" data-kubernetes-name-tail>
|
||||
{parts().tail}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesNameText;
|
||||
@@ -218,7 +218,7 @@ export const KubernetesNodesTable: Component<{
|
||||
kind="name"
|
||||
sort={sort}
|
||||
sortKey="node"
|
||||
class="platform-table-mobile-w-30 md:w-[14%]"
|
||||
class="platform-table-mobile-w-30 md:w-[20%]"
|
||||
>
|
||||
Node
|
||||
</PlatformSortableTableHead>
|
||||
@@ -256,7 +256,7 @@ export const KubernetesNodesTable: Component<{
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="kubelet"
|
||||
class="hidden md:table-cell md:w-[8%]"
|
||||
class="hidden md:table-cell md:w-[7%]"
|
||||
>
|
||||
Kubelet
|
||||
</PlatformSortableTableHead>
|
||||
@@ -272,7 +272,7 @@ export const KubernetesNodesTable: Component<{
|
||||
kind="metric-bar"
|
||||
sort={sort}
|
||||
sortKey="cpu"
|
||||
class="platform-table-mobile-w-15 md:w-[11%]"
|
||||
class="platform-table-mobile-w-15 md:w-[10%]"
|
||||
>
|
||||
CPU
|
||||
</PlatformSortableTableHead>
|
||||
@@ -280,7 +280,7 @@ export const KubernetesNodesTable: Component<{
|
||||
kind="metric-bar"
|
||||
sort={sort}
|
||||
sortKey="memory"
|
||||
class="platform-table-mobile-w-15 md:w-[11%]"
|
||||
class="platform-table-mobile-w-15 md:w-[10%]"
|
||||
>
|
||||
<PlatformResponsiveTableLabel compact="Mem" full="Memory" />
|
||||
</PlatformSortableTableHead>
|
||||
@@ -296,7 +296,7 @@ export const KubernetesNodesTable: Component<{
|
||||
kind="numeric-value"
|
||||
sort={sort}
|
||||
sortKey="capacity"
|
||||
class="platform-table-mobile-w-10 md:w-[14%]"
|
||||
class="platform-table-mobile-w-10 md:w-[16%]"
|
||||
>
|
||||
<PlatformResponsiveTableLabel compact="Cap" full="Capacity" />
|
||||
</PlatformSortableTableHead>
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
mapKubernetesPodStatus,
|
||||
type KubernetesResourceStatusFilter,
|
||||
} from './kubernetesPageModel';
|
||||
import { KubernetesNameText } from './KubernetesNameText';
|
||||
|
||||
const podName = (resource: Resource): string =>
|
||||
asTrimmedString(resource.kubernetes?.podName) ||
|
||||
@@ -324,8 +325,10 @@ export const KubernetesPodsTable: Component<{
|
||||
name={name()}
|
||||
url={resource.customUrl}
|
||||
class="min-w-0"
|
||||
nameClass="truncate font-semibold text-base-content"
|
||||
/>
|
||||
nameClass="flex min-w-0 font-semibold text-base-content"
|
||||
>
|
||||
<KubernetesNameText name={name()} />
|
||||
</ResourceNameWithWebInterfaceLink>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
@@ -338,9 +341,7 @@ export const KubernetesPodsTable: Component<{
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} platform-table-phone-hidden text-base-content`}
|
||||
>
|
||||
<span class="block max-w-full truncate" title={node()}>
|
||||
{node()}
|
||||
</span>
|
||||
<KubernetesNameText name={node()} class="max-w-full" />
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} platform-table-phone-hidden text-base-content`}
|
||||
|
||||
@@ -246,7 +246,7 @@ export const KubernetesPolicyTable: Component<{
|
||||
header={
|
||||
<>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('name')} platform-table-mobile-w-30 md:w-[17%]`}
|
||||
class={`${getPlatformTableHeadClassForKind('name')} platform-table-mobile-w-30 md:w-[22%]`}
|
||||
>
|
||||
Resource
|
||||
</TableHead>
|
||||
|
||||
@@ -166,7 +166,7 @@ export const KubernetesServicesTable: Component<{
|
||||
kind="name"
|
||||
sort={sort}
|
||||
sortKey="service"
|
||||
class="platform-table-mobile-w-30 kubernetes-service-name-column md:w-[19%]"
|
||||
class="platform-table-mobile-w-30 kubernetes-service-name-column md:w-[22%]"
|
||||
>
|
||||
Service
|
||||
</PlatformSortableTableHead>
|
||||
|
||||
@@ -278,7 +278,7 @@ export const KubernetesStorageTable: Component<{
|
||||
kind="name"
|
||||
sort={sort}
|
||||
sortKey="resource"
|
||||
class="platform-table-mobile-w-30 md:w-[19%]"
|
||||
class="platform-table-mobile-w-30 md:w-[22%]"
|
||||
>
|
||||
Resource
|
||||
</PlatformSortableTableHead>
|
||||
|
||||
+9
-2
@@ -15,6 +15,13 @@ import { formatPlatformTableDateTimeValue } from '@/features/platformPage/shared
|
||||
import type { Resource } from '@/types/resource';
|
||||
import { KubernetesControllersTable } from '../KubernetesControllersTable';
|
||||
|
||||
// Generated Kubernetes names render as a truncating head and a kept tail, so
|
||||
// the full name is the text of the name wrapper rather than of one text node.
|
||||
const kubernetesName =
|
||||
(name: string) =>
|
||||
(_content: string, element: Element | null): boolean =>
|
||||
element?.hasAttribute('data-kubernetes-name') === true && element.textContent === name;
|
||||
|
||||
const makeResource = ({
|
||||
id,
|
||||
type,
|
||||
@@ -250,7 +257,7 @@ describe('KubernetesControllersTable', () => {
|
||||
// Tapping the row opens the expansion; the Detail column is hidden below a
|
||||
// large container, so this is where a phone or half-width pane reads the
|
||||
// completion time.
|
||||
fireEvent.click(screen.getByText('nightly-import'));
|
||||
fireEvent.click(screen.getByText(kubernetesName('nightly-import')));
|
||||
const job = screen.getByTestId('resource-kubernetes-controller-section');
|
||||
expect(within(job).getByText('Started')).toBeInTheDocument();
|
||||
expect(
|
||||
@@ -266,7 +273,7 @@ describe('KubernetesControllersTable', () => {
|
||||
expect(within(job).getByText('10 completions')).toBeInTheDocument();
|
||||
expect(within(job).getByText('Failed').closest('tr')).toHaveTextContent('2');
|
||||
|
||||
fireEvent.click(screen.getByText('billing-rollup'));
|
||||
fireEvent.click(screen.getByText(kubernetesName('billing-rollup')));
|
||||
const cron = screen.getByTestId('resource-kubernetes-controller-section');
|
||||
expect(within(cron).getByText('Schedule')).toBeInTheDocument();
|
||||
expect(within(cron).getByText('*/5 * * * *')).toBeInTheDocument();
|
||||
|
||||
+9
-2
@@ -4,6 +4,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { Resource } from '@/types/resource';
|
||||
import { KubernetesPageSurface } from '../KubernetesPageSurface';
|
||||
|
||||
// Generated Kubernetes names render as a truncating head and a kept tail, so
|
||||
// the full name is the text of the name wrapper rather than of one text node.
|
||||
const kubernetesName =
|
||||
(name: string) =>
|
||||
(_content: string, element: Element | null): boolean =>
|
||||
element?.hasAttribute('data-kubernetes-name') === true && element.textContent === name;
|
||||
|
||||
// URL-backed shared-toolbar filters: the workloads / services / configuration
|
||||
// tabs read search (q) and status from the URL so bookmarks capture -term
|
||||
// exclusions, mirroring the Docker containers table. These tests render the
|
||||
@@ -199,9 +206,9 @@ describe('Kubernetes URL-backed shared toolbar filters', () => {
|
||||
renderSurfaceAt('/kubernetes/workloads?cluster=cluster-west');
|
||||
|
||||
expect(screen.getByText('west-api')).toBeInTheDocument();
|
||||
expect(screen.getByText('west-pod')).toBeInTheDocument();
|
||||
expect(screen.getByText(kubernetesName('west-pod'))).toBeInTheDocument();
|
||||
expect(screen.queryByText('east-api')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('east-pod')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(kubernetesName('east-pod'))).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('scopes the Overview workload inventory when a cluster name is selected', async () => {
|
||||
|
||||
@@ -4,6 +4,13 @@ import { afterEach, describe, expect, it } from 'vitest';
|
||||
import type { Resource } from '@/types/resource';
|
||||
import { KubernetesPodsTable } from '../KubernetesPodsTable';
|
||||
|
||||
// Generated Kubernetes names render as a truncating head and a kept tail, so
|
||||
// the full name is the text of the name wrapper rather than of one text node.
|
||||
const kubernetesName =
|
||||
(name: string) =>
|
||||
(_content: string, element: Element | null): boolean =>
|
||||
element?.hasAttribute('data-kubernetes-name') === true && element.textContent === name;
|
||||
|
||||
const makeResource = ({
|
||||
id,
|
||||
type = 'pod',
|
||||
@@ -104,9 +111,20 @@ describe('KubernetesPodsTable', () => {
|
||||
expect(screen.getByText('Image')).toBeInTheDocument();
|
||||
expect(screen.getByText('Age')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('checkout-api-6c746d5bcf-c7z2p')).toBeInTheDocument();
|
||||
expect(screen.getByText(kubernetesName('checkout-api-6c746d5bcf-c7z2p'))).toBeInTheDocument();
|
||||
expect(screen.getByText('prod-euw1/services')).toBeInTheDocument();
|
||||
expect(screen.getByText('prod-euw1-k8s-02')).toBeInTheDocument();
|
||||
expect(screen.getByText(kubernetesName('prod-euw1-k8s-02'))).toBeInTheDocument();
|
||||
// Narrow columns truncate the head and keep the generated tail, so
|
||||
// replicas and nodes stay distinguishable. The title keeps the full name.
|
||||
const podName = screen.getByText(kubernetesName('checkout-api-6c746d5bcf-c7z2p'));
|
||||
expect(podName).toHaveAttribute('title', 'checkout-api-6c746d5bcf-c7z2p');
|
||||
expect(podName.querySelector('[data-kubernetes-name-tail]')).toHaveTextContent('-c7z2p');
|
||||
expect(podName.querySelector('.truncate')).toHaveTextContent('checkout-api-6c746d5bcf');
|
||||
expect(
|
||||
screen
|
||||
.getByText(kubernetesName('prod-euw1-k8s-02'))
|
||||
.querySelector('[data-kubernetes-name-tail]'),
|
||||
).toHaveTextContent('-02');
|
||||
// Raw phase is Running, but the metrics-sidecar container is not ready;
|
||||
// the status column shows the mapped label, not the phase.
|
||||
expect(screen.getByText('Not ready')).toBeInTheDocument();
|
||||
@@ -123,7 +141,9 @@ describe('KubernetesPodsTable', () => {
|
||||
name: 'Open web interface for checkout-api-6c746d5bcf-c7z2p',
|
||||
}),
|
||||
).toHaveAttribute('href', 'https://checkout-pod.internal');
|
||||
expect(screen.getByText('checkout-api-6c746d5bcf-c7z2p').closest('a')).toBeNull();
|
||||
expect(
|
||||
screen.getByText(kubernetesName('checkout-api-6c746d5bcf-c7z2p')).closest('a'),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('renders pod rows with status mapped from podPhase + container readiness, attention rows first', () => {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { Resource } from '@/types/resource';
|
||||
import { getKubernetesNodesNeedingAttention } from '../kubernetesPageModel';
|
||||
import {
|
||||
getKubernetesNodesNeedingAttention,
|
||||
splitKubernetesNameTail,
|
||||
} from '../kubernetesPageModel';
|
||||
import {
|
||||
KUBERNETES_TAB_SPECS,
|
||||
buildKubernetesClusterChildCounts,
|
||||
@@ -44,6 +47,30 @@ const makeResource = (resource: Partial<Resource> & Pick<Resource, 'id' | 'type'
|
||||
});
|
||||
|
||||
describe('kubernetesPageModel', () => {
|
||||
it('splits off the generated tail that tells sibling names apart', () => {
|
||||
expect(splitKubernetesNameTail('checkout-api-6d8f9c7b5-x7k2p')).toEqual({
|
||||
head: 'checkout-api-6d8f9c7b5',
|
||||
tail: '-x7k2p',
|
||||
});
|
||||
expect(splitKubernetesNameTail('prod-euw1-k8s-03')).toEqual({
|
||||
head: 'prod-euw1-k8s',
|
||||
tail: '-03',
|
||||
});
|
||||
expect(splitKubernetesNameTail('nightly-backfill-28918234')).toEqual({
|
||||
head: 'nightly-backfill',
|
||||
tail: '-28918234',
|
||||
});
|
||||
// No dash, a trailing dash, or a tail too long to be a generated suffix
|
||||
// (a cloud node's domain) leaves the name whole for plain truncation.
|
||||
expect(splitKubernetesNameTail('postgres')).toEqual({ head: 'postgres', tail: '' });
|
||||
expect(splitKubernetesNameTail('web-')).toEqual({ head: 'web-', tail: '' });
|
||||
expect(splitKubernetesNameTail('ip-10-0-1-23.eu-west-1.compute.internal')).toEqual({
|
||||
head: 'ip-10-0-1-23.eu-west-1.compute.internal',
|
||||
tail: '',
|
||||
});
|
||||
expect(splitKubernetesNameTail('-')).toEqual({ head: '-', tail: '' });
|
||||
});
|
||||
|
||||
it('declares operator workflow tabs for Kubernetes inventory', () => {
|
||||
expect(KUBERNETES_TAB_SPECS.map((tab) => tab.id)).toEqual([
|
||||
'overview',
|
||||
|
||||
@@ -99,6 +99,20 @@ const displayName = (resource: Resource): string =>
|
||||
// is only a fallback for rows whose adapter carries no name. Every Kubernetes
|
||||
// page table shares this so the same cluster never reads as "Production EU"
|
||||
// in one column and "k8s-production-1" in another.
|
||||
// Pod names end in the generated segment that tells replicas apart
|
||||
// (checkout-api-6d8f9c7b5-x7k2p, web-0), which is exactly what end truncation
|
||||
// cuts off in a narrow column. Splitting it out lets the table truncate the
|
||||
// head and keep the tail, so a long name reads checkout-api-6d…-x7k2p.
|
||||
const KUBERNETES_NAME_TAIL_MAX = 10;
|
||||
|
||||
export function splitKubernetesNameTail(name: string): { head: string; tail: string } {
|
||||
const cut = name.lastIndexOf('-');
|
||||
if (cut <= 0 || name.length - cut - 1 > KUBERNETES_NAME_TAIL_MAX || cut === name.length - 1) {
|
||||
return { head: name, tail: '' };
|
||||
}
|
||||
return { head: name.slice(0, cut), tail: name.slice(cut) };
|
||||
}
|
||||
|
||||
export const kubernetesClusterLabel = (resource: Resource): string =>
|
||||
asTrimmedString(resource.kubernetes?.clusterName) ||
|
||||
asTrimmedString(resource.kubernetes?.clusterId);
|
||||
|
||||
+1
-1
@@ -367,7 +367,7 @@ describe('platform overview layout guardrails', () => {
|
||||
/sortKey="status"[\s\S]{0,120}?class="platform-table-phone-hidden md:w-\[8%\]"/,
|
||||
);
|
||||
expect(kubernetesNodesTableSource).toMatch(
|
||||
/sortKey="capacity"[\s\S]{0,120}?class="platform-table-mobile-w-10 md:w-\[14%\]"/,
|
||||
/sortKey="capacity"[\s\S]{0,120}?class="platform-table-mobile-w-10 md:w-\[16%\]"/,
|
||||
);
|
||||
expect(kubernetesDeploymentsTableSource).toMatch(
|
||||
/sortKey="namespace"[\s\S]{0,120}?class="platform-table-mobile-w-15 md:w-\[20%\]"/,
|
||||
|
||||
@@ -1434,6 +1434,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Between the phone projection and the md viewport breakpoint, tables that
|
||||
size their columns with md:w-[...] classes have no widths at all, while the
|
||||
phone-hidden columns are already back. Every visible column then took an
|
||||
equal share, so a seven-column Kubernetes table left its name column 83px
|
||||
with the chevron and status dot inside it. Keep the phone identity anchor
|
||||
until the md widths apply. Tables sized by a weighted colgroup keep their
|
||||
own widths, because column widths win over header widths. */
|
||||
@media (width < 768px) {
|
||||
@container (min-width: 34rem) {
|
||||
.table-scroll-shell > .table-fixed.platform-table th.platform-table-name-column {
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* At the narrowest supported phone width, spend the extra identity space once
|
||||
at the shared boundary. Source tables still choose which lower-priority
|
||||
field disappears; the shell only guarantees that the surviving identity
|
||||
|
||||
Reference in New Issue
Block a user