mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 04:33:17 +00:00
Stop Kubernetes Config and Deployments tables clipping names (#2509)
Follow-up to #2506. The Kubernetes Configuration and Deployments tables still clipped names and values between phone and desktop widths. - **Tablet band widths.** Between the phone projection and the md column widths (a 34rem container below a 768px viewport) these tables split their width evenly, which cut names to a few letters. Headers can now declare a band percent through `bandWidth` / `platformTableBandWidthStyle`, and both tables give names the most room. - **Scope shows the namespace.** With one cluster in view, every scope cell repeated the cluster name ("Production EU/ser…"). The cell now shows the namespace (`Cluster` for cluster-scoped objects) and keeps the full scope on the title and for screen readers. Several clusters keep the combined label. - **Deployments.** Desired appears only on wide tables, because Ready already reads `2/2`. Ready keeps its denominator until Desired is shown. On a phone, Age takes Ready's spare width so `18h ago` fits. - **Summary cells.** Values sat in a capped `inline-block` span that ignored the column width. Long values spilled past narrow cells with no ellipsis and sat about 4px above the row. A `block truncate` span fills the cell, ends in an ellipsis inside it, and lines up with the rest of the row. Verification: - Clip sweep of every Kubernetes tab at 600–1440px, compared with main: clipped cells drop from 566 to 356, and no column is worse at any width. - Deployment, Resource and Namespace clips on Workloads and Configuration drop from 25 to 0, and Scope clips from 136 to 7. - Row text alignment is 0px spread on every tab. - The browser receipt is included.
This commit is contained in:
@@ -2309,6 +2309,11 @@ not a replacement status card, CTA band, or page-local nested card.
|
||||
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.
|
||||
A table whose columns need unequal room in that band (a long name beside
|
||||
short counts) declares each header's percent through
|
||||
`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.
|
||||
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
|
||||
|
||||
@@ -1177,6 +1177,19 @@ part that tells siblings apart (`checkout-api-6d…-x7k2p`, `prod…-03`), with
|
||||
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.
|
||||
With one cluster in view, every Kubernetes scope cell repeated the cluster
|
||||
name and crowded out the namespace that differs between rows, so the tables
|
||||
show the namespace alone (`Cluster` for cluster-scoped objects) through
|
||||
`kubernetesScopeDisplayLabel` and keep the full cluster/namespace scope on the
|
||||
title; several clusters keep the combined label. The Deployments table keeps
|
||||
Desired for wide tables only, because Ready already shows ready over desired,
|
||||
and sizes the tablet band for a long name beside short counts. The
|
||||
Configuration tables size their band and md widths for ConfigMap, Secret and
|
||||
RBAC names first, then the namespace scope, then the short Lifecycle and Data
|
||||
shape values, then Kind. Kubernetes summary cells truncate through a block span
|
||||
that fills the cell, so a long value ends in an ellipsis inside its own column
|
||||
on the row's text line; a fixed-width inline-block cap spilled past narrow
|
||||
cells without an ellipsis and sat above the row baseline.
|
||||
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,122 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "3af37a88625701f6dad057fc8889d09a61dffd24",
|
||||
"verified_at": "2026-10-05T11:01:05Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.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/KubernetesScopeText.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/features/platformPage/sharedPlatformPage.tsx",
|
||||
"frontend-modern/src/index.css"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx": "b66c8c5c705b9478d98ba2bfd51ff199c07619ecf40516d7f276a13c5409a825",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx": "bc3e01de166d4467e7b547a72958091c465768ef552ae31118969d6b4d6c36fd",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx": "b1b184557d03331d8d2ef19d9ab8af9d083013ac3b839fde719bd5d5e4d12d9e",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx": "95cca80263abaf11c0025f1c0bf7a2ccc4421acb9a0d2c611096602353ea75d5",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx": "814ce1c8358cdde955da9707498d491bc3081dded52e86405edd9517e3f71f65",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx": "6a0c1e57cddf59acaf0f0a21690fde5db75f08a9fddd4ac957086b5d7ac307c9",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx": "487541b013886c3f371762a585443ae790d6ec7c72531ac7991ec1a5c76b73a5",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx": "5ed2a52f1a7e5c27a96d2df832d4b80ef210873c84051eb396d4f9cf31f42f51",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx": "91293a32d6cb195a07d39f472c9a7e204bd96de709c65d82728e30d74aac77a9",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesScopeText.tsx": "50a0468caf73eb7658bbc90e26cd5b48702af7a441e384ab70fde5a6a0daf557",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx": "48ccb976014015513b1aa52391da969604c541d2f27a20d8efac098d24890f15",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx": "b0c87e0386b0d4f7440a7859a106d73daf611867bfd88418e1c6ed7cf8456ae9",
|
||||
"frontend-modern/src/features/kubernetes/kubernetesPageModel.ts": "9cf09fca38e4c64616e1cd4ee578c6c03bddb810498abdd974e0450a68aa81b1",
|
||||
"frontend-modern/src/features/platformPage/sharedPlatformPage.tsx": "843a1efe473f4092a1ee508bd1634bc51fdcce395e2fe7dd41998ba1c7aebf8b",
|
||||
"frontend-modern/src/index.css": "ce8f3f0d5a8da290fad1545eedca6a46d4211b4e98239fb1a8e4773cf063e4b2"
|
||||
},
|
||||
"routes": [
|
||||
"/kubernetes/overview",
|
||||
"/kubernetes/nodes",
|
||||
"/kubernetes/workloads",
|
||||
"/kubernetes/services",
|
||||
"/kubernetes/storage",
|
||||
"/kubernetes/configuration",
|
||||
"/kubernetes/events"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 360,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"height": 1100
|
||||
},
|
||||
{
|
||||
"width": 430,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 520,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 600,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 650,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 700,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 767,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 768,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 820,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 900,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 1100,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 1300,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 1000
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"Clip sweep (scrollWidth over clientWidth on every cell, every Kubernetes tab, 600 to 1440 wide, screen-reader text excluded): clipped cells fall from 566 to 356 against main with no column worse at any width; Deployment, Resource and Namespace clips on Workloads and Configuration go from 25 to 0, Scope from 136 to 7, Age from 22 to 1",
|
||||
"Workloads at 700: every deployment and namespace reads in full (main showed ingress-nginx-cont... and monitori...); columns are Deployment, Namespace, Updated, Ready, Available, Age with Ready reading 2/2; at 1300 and 1440 Cluster and Desired return and Ready reads 2",
|
||||
"Configuration at 700: ConfigMap, Secret and RBAC names read in full (main cut them to checkout-api-runti... and payments-worker-...), Scope shows the namespace (apps, monitoring, services, Cluster) instead of Production EU/ser, and long Lifecycle and Data shape values end in an ellipsis inside their own cell where main spilled 1 subject - Service past the card edge",
|
||||
"Row text alignment on every Kubernetes tab at 700 and 1440: each row's cell text sits on one line (0px spread); main's Scope and Data shape spans sat about 4px above the row",
|
||||
"All 205 block truncate summary spans are the only content of their cell and sit in fixed-layout tables",
|
||||
"Phone at 390: Deployments Age reads 18h ago (main showed 18h a...), Ready keeps 2/2; Configuration shows the namespace scope and ellipsizes Data inside the cell; at 360 Age stays hidden as before",
|
||||
"Scope cell exposes the full cluster/namespace scope to assistive technology through a screen-reader span and keeps it on the title"
|
||||
],
|
||||
"interactions": [
|
||||
"Ran the clip sweep on main (reverse-applied patch) and on the change across seven widths",
|
||||
"Tuned tablet-band widths live by injecting --platform-table-band-width at 600, 650, 700 and 767",
|
||||
"Measured row text alignment and block-span containment in the live DOM",
|
||||
"Captured before/after table screenshots at 700 and 390 for Workloads and Configuration"
|
||||
]
|
||||
}
|
||||
@@ -274,6 +274,20 @@ describe('App architecture', () => {
|
||||
expect(appStylesSource).not.toContain('white-space: normal;\n -webkit-box-orient');
|
||||
});
|
||||
|
||||
it('keeps the desired replica count on screen in the Deployments table', () => {
|
||||
// Desired returns with the 72rem tier, so Ready's own /desired total hides
|
||||
// only there, after its base rule so the container rule wins.
|
||||
const base = appStylesSource.indexOf('.k8s-deployment-ready-total {\n display: inline;');
|
||||
const hide = appStylesSource.indexOf(
|
||||
'@container (min-width: 72rem) {\n .k8s-deployment-ready-total {\n display: none;',
|
||||
);
|
||||
expect(base).toBeGreaterThan(-1);
|
||||
expect(hide).toBeGreaterThan(base);
|
||||
expect(appStylesSource.match(/\.k8s-deployment-ready-total \{\s*display: none;/g)).toHaveLength(
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
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
|
||||
@@ -283,6 +297,11 @@ describe('App architecture', () => {
|
||||
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%;/,
|
||||
);
|
||||
// A table that needs unequal room in the band declares each header's
|
||||
// width, and the shell applies it only there (after the name anchor).
|
||||
expect(band).toMatch(
|
||||
/width: 30%;\s*\}[\s\S]*?th\[style\*='--platform-table-band-width'\] \{\s*width: var\(--platform-table-band-width\);/,
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps compact phone settings spacing scoped to the settings content shell', () => {
|
||||
|
||||
@@ -32,7 +32,9 @@ import {
|
||||
filterKubernetesResources,
|
||||
kubernetesScopeLabel,
|
||||
type KubernetesResourceStatusFilter,
|
||||
isSingleKubernetesCluster,
|
||||
} from './kubernetesPageModel';
|
||||
import { KubernetesScopeText } from './KubernetesScopeText';
|
||||
|
||||
const autoscalerName = (resource: Resource): string =>
|
||||
asTrimmedString(resource.displayName) || asTrimmedString(resource.name) || resource.id;
|
||||
@@ -105,6 +107,7 @@ export const KubernetesAutoscalingTable: Component<{
|
||||
externalSearch?: () => string;
|
||||
externalStatus?: () => KubernetesResourceStatusFilter;
|
||||
}> = (props) => {
|
||||
const singleCluster = createMemo(() => isSingleKubernetesCluster(props.resources));
|
||||
const tableState = createPlatformTableFilterState({
|
||||
resources: () => props.resources,
|
||||
initialStatus: 'all' as KubernetesResourceStatusFilter,
|
||||
@@ -237,7 +240,6 @@ export const KubernetesAutoscalingTable: Component<{
|
||||
{(resource) => {
|
||||
const indicator = () => getSimpleStatusIndicator(resource.status);
|
||||
const name = () => autoscalerName(resource);
|
||||
const scope = () => kubernetesScopeLabel(resource);
|
||||
const target = () => targetRef(resource);
|
||||
const metrics = () => metricSources(resource);
|
||||
const labels = () => labelSummary(resource);
|
||||
@@ -273,14 +275,16 @@ export const KubernetesAutoscalingTable: Component<{
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content lg:table-cell`}
|
||||
>
|
||||
<span class="inline-block max-w-48 truncate" title={scope()}>
|
||||
{scope()}
|
||||
</span>
|
||||
<KubernetesScopeText
|
||||
resource={resource}
|
||||
singleCluster={singleCluster()}
|
||||
class="block truncate"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
|
||||
>
|
||||
<span class="inline-block max-w-56 truncate" title={target()}>
|
||||
<span class="block truncate" title={target()}>
|
||||
{target()}
|
||||
</span>
|
||||
</TableCell>
|
||||
@@ -306,14 +310,14 @@ export const KubernetesAutoscalingTable: Component<{
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content sm:table-cell`}
|
||||
>
|
||||
<span class="inline-block max-w-40 truncate" title={metrics().title}>
|
||||
<span class="block truncate" title={metrics().title}>
|
||||
{metrics().label}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content lg:table-cell`}
|
||||
>
|
||||
<span class="inline-block max-w-32 truncate" title={labels().title}>
|
||||
<span class="block truncate" title={labels().title}>
|
||||
{labels().label}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
@@ -31,7 +31,9 @@ import {
|
||||
filterKubernetesResources,
|
||||
kubernetesScopeLabel,
|
||||
type KubernetesResourceStatusFilter,
|
||||
isSingleKubernetesCluster,
|
||||
} from './kubernetesPageModel';
|
||||
import { KubernetesScopeText } from './KubernetesScopeText';
|
||||
|
||||
// ConfigMaps and Secrets are intentionally rendered as API metadata. Pulse may
|
||||
// know that keys exist, but metadata-only collection must never imply that
|
||||
@@ -198,6 +200,7 @@ export const KubernetesConfigTable: Component<{
|
||||
externalSearch?: () => string;
|
||||
externalStatus?: () => KubernetesResourceStatusFilter;
|
||||
}> = (props) => {
|
||||
const singleCluster = createMemo(() => isSingleKubernetesCluster(props.resources));
|
||||
const tableState = createPlatformTableFilterState({
|
||||
resources: () => props.resources,
|
||||
initialStatus: 'all' as KubernetesResourceStatusFilter,
|
||||
@@ -264,7 +267,8 @@ export const KubernetesConfigTable: Component<{
|
||||
kind="name"
|
||||
sort={sort}
|
||||
sortKey="resource"
|
||||
class="platform-table-mobile-w-30 md:w-[22%]"
|
||||
class="platform-table-mobile-w-30 md:w-[27%]"
|
||||
bandWidth={35}
|
||||
>
|
||||
Resource
|
||||
</PlatformSortableTableHead>
|
||||
@@ -272,7 +276,8 @@ export const KubernetesConfigTable: Component<{
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="kind"
|
||||
class="platform-table-mobile-w-15 md:w-[13%]"
|
||||
class="platform-table-mobile-w-15 md:w-[15%]"
|
||||
bandWidth={16}
|
||||
>
|
||||
Kind
|
||||
</PlatformSortableTableHead>
|
||||
@@ -280,7 +285,8 @@ export const KubernetesConfigTable: Component<{
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="scope"
|
||||
class="platform-table-mobile-w-20 md:w-[16%]"
|
||||
class="platform-table-mobile-w-20 md:w-[10%]"
|
||||
bandWidth={14}
|
||||
>
|
||||
Scope
|
||||
</PlatformSortableTableHead>
|
||||
@@ -288,7 +294,8 @@ export const KubernetesConfigTable: Component<{
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="lifecycle"
|
||||
class="platform-table-phone-hidden md:w-[16%]"
|
||||
class="platform-table-phone-hidden md:w-[17%]"
|
||||
bandWidth={16}
|
||||
>
|
||||
<PlatformResponsiveTableLabel compact="Status" full="Lifecycle / trust" />
|
||||
</PlatformSortableTableHead>
|
||||
@@ -296,6 +303,7 @@ export const KubernetesConfigTable: Component<{
|
||||
kind="text"
|
||||
sort={sort}
|
||||
class="platform-table-mobile-w-15 md:w-[14%]"
|
||||
bandWidth={19}
|
||||
>
|
||||
<PlatformResponsiveTableLabel compact="Data" full="Data shape" />
|
||||
</PlatformSortableTableHead>
|
||||
@@ -322,7 +330,6 @@ export const KubernetesConfigTable: Component<{
|
||||
const indicator = () => getSimpleStatusIndicator(resource.status);
|
||||
const name = () => configName(resource);
|
||||
const kind = () => configKind(resource);
|
||||
const scope = () => kubernetesScopeLabel(resource);
|
||||
const state = () => lifecycleOrTrust(resource);
|
||||
const data = () => dataShape(resource);
|
||||
const refs = () => serviceAccountRefs(resource);
|
||||
@@ -364,9 +371,11 @@ export const KubernetesConfigTable: Component<{
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
|
||||
>
|
||||
<span class="inline-block max-w-52 truncate" title={scope()}>
|
||||
{scope()}
|
||||
</span>
|
||||
<KubernetesScopeText
|
||||
resource={resource}
|
||||
singleCluster={singleCluster()}
|
||||
class="block truncate"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} platform-table-phone-hidden text-base-content`}
|
||||
@@ -376,21 +385,21 @@ export const KubernetesConfigTable: Component<{
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
|
||||
>
|
||||
<span class="inline-block max-w-52 truncate" title={data().title}>
|
||||
<span class="block truncate" title={data().title}>
|
||||
{data().label}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
|
||||
>
|
||||
<span class="inline-block max-w-52 truncate" title={refs().title}>
|
||||
<span class="block truncate" title={refs().title}>
|
||||
{refs().label}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
|
||||
>
|
||||
<span class="inline-block max-w-36 truncate" title={labels().title}>
|
||||
<span class="block truncate" title={labels().title}>
|
||||
{labels().label}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
@@ -36,7 +36,9 @@ import {
|
||||
kubernetesScopeLabel,
|
||||
mapKubernetesControllerStatus,
|
||||
type KubernetesResourceStatusFilter,
|
||||
isSingleKubernetesCluster,
|
||||
} from './kubernetesPageModel';
|
||||
import { KubernetesScopeText } from './KubernetesScopeText';
|
||||
import { KubernetesNameText } from './KubernetesNameText';
|
||||
|
||||
const controllerName = (resource: Resource): string =>
|
||||
@@ -243,6 +245,7 @@ export const KubernetesControllersTable: Component<{
|
||||
externalSearch?: () => string;
|
||||
externalStatus?: () => KubernetesResourceStatusFilter;
|
||||
}> = (props) => {
|
||||
const singleCluster = createMemo(() => isSingleKubernetesCluster(props.resources));
|
||||
const tableState = createPlatformTableFilterState({
|
||||
resources: () => props.resources,
|
||||
initialStatus: 'all' as KubernetesResourceStatusFilter,
|
||||
@@ -397,7 +400,6 @@ export const KubernetesControllersTable: Component<{
|
||||
const indicator = () => mapKubernetesControllerStatus(resource);
|
||||
const name = () => controllerName(resource);
|
||||
const kind = () => controllerKind(resource);
|
||||
const scope = () => kubernetesScopeLabel(resource);
|
||||
const target = () => targetValue(resource);
|
||||
const exceptions = () => exceptionSummary(resource);
|
||||
const detail = () => apiDetail(resource);
|
||||
@@ -439,14 +441,16 @@ export const KubernetesControllersTable: Component<{
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content lg:table-cell`}
|
||||
>
|
||||
<span class="inline-block max-w-48 truncate" title={scope()}>
|
||||
{scope()}
|
||||
</span>
|
||||
<KubernetesScopeText
|
||||
resource={resource}
|
||||
singleCluster={singleCluster()}
|
||||
class="block truncate"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} platform-table-phone-hidden text-base-content`}
|
||||
>
|
||||
<span class="inline-block max-w-48 truncate" title={target()}>
|
||||
<span class="block truncate" title={target()}>
|
||||
{target()}
|
||||
</span>
|
||||
</TableCell>
|
||||
@@ -468,7 +472,7 @@ export const KubernetesControllersTable: Component<{
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
|
||||
>
|
||||
<span class="inline-block max-w-52 truncate" title={exceptions()}>
|
||||
<span class="block truncate" title={exceptions()}>
|
||||
{exceptions()}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
@@ -174,13 +174,18 @@ export const KubernetesDeploymentsTable: Component<{
|
||||
The integer-count columns (Desired / Updated / Ready /
|
||||
Available) trim to what their headers plus 1-2 digit
|
||||
values need. The basic tier restores namespace context
|
||||
without crowding the phone tier.
|
||||
without crowding the phone tier. Desired is the
|
||||
denominator Ready already shows (2/2), so it waits for a
|
||||
wide table. bandWidth sizes the tablet band below the
|
||||
md widths, where a long deployment name needs about a
|
||||
third of the row and the counts need little.
|
||||
*/}
|
||||
<PlatformSortableTableHead
|
||||
kind="name"
|
||||
sort={sort}
|
||||
sortKey="deployment"
|
||||
class="platform-table-mobile-w-30 md:w-[25%]"
|
||||
bandWidth={36}
|
||||
>
|
||||
Deployment
|
||||
</PlatformSortableTableHead>
|
||||
@@ -188,7 +193,8 @@ export const KubernetesDeploymentsTable: Component<{
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="namespace"
|
||||
class="platform-table-mobile-w-15 md:w-[20%]"
|
||||
class="platform-table-mobile-w-15 md:w-[18%]"
|
||||
bandWidth={16}
|
||||
>
|
||||
Namespace
|
||||
</PlatformSortableTableHead>
|
||||
@@ -204,7 +210,7 @@ export const KubernetesDeploymentsTable: Component<{
|
||||
kind="numeric-value"
|
||||
sort={sort}
|
||||
sortKey="desired"
|
||||
class="platform-table-phone-hidden md:w-[8%]"
|
||||
class="hidden md:table-cell md:w-[8%]"
|
||||
>
|
||||
<PlatformResponsiveTableLabel compact="Des" full="Desired" />
|
||||
</PlatformSortableTableHead>
|
||||
@@ -213,6 +219,7 @@ export const KubernetesDeploymentsTable: Component<{
|
||||
sort={sort}
|
||||
sortKey="updated"
|
||||
class="platform-table-phone-hidden md:w-[8%]"
|
||||
bandWidth={12}
|
||||
>
|
||||
<PlatformResponsiveTableLabel compact="Upd" full="Updated" />
|
||||
</PlatformSortableTableHead>
|
||||
@@ -220,7 +227,8 @@ export const KubernetesDeploymentsTable: Component<{
|
||||
kind="numeric-value"
|
||||
sort={sort}
|
||||
sortKey="ready"
|
||||
class="platform-table-mobile-w-15 md:w-[7%]"
|
||||
class="platform-table-mobile-w-10 md:w-[7%]"
|
||||
bandWidth={9.5}
|
||||
>
|
||||
<PlatformResponsiveTableLabel compact="Rdy" full="Ready" />
|
||||
</PlatformSortableTableHead>
|
||||
@@ -229,6 +237,7 @@ export const KubernetesDeploymentsTable: Component<{
|
||||
sort={sort}
|
||||
sortKey="available"
|
||||
class="platform-table-phone-hidden md:w-[9%]"
|
||||
bandWidth={14}
|
||||
>
|
||||
<PlatformResponsiveTableLabel compact="Avail" full="Available" />
|
||||
</PlatformSortableTableHead>
|
||||
@@ -236,7 +245,8 @@ export const KubernetesDeploymentsTable: Component<{
|
||||
kind="numeric-value"
|
||||
sort={sort}
|
||||
sortKey="age"
|
||||
class="platform-table-mobile-w-10 md:w-[6%] platform-table-narrow-hidden"
|
||||
class="platform-table-mobile-w-15 md:w-[8%] platform-table-narrow-hidden"
|
||||
bandWidth={12.5}
|
||||
>
|
||||
Age
|
||||
</PlatformSortableTableHead>
|
||||
@@ -292,7 +302,7 @@ export const KubernetesDeploymentsTable: Component<{
|
||||
{cluster()}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('numeric-value')} platform-table-phone-hidden text-base-content`}
|
||||
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content md:table-cell`}
|
||||
>
|
||||
<PlatformTableNumberValue
|
||||
value={deployment.kubernetes?.desiredReplicas ?? 0}
|
||||
|
||||
@@ -28,10 +28,11 @@ import type { Resource } from '@/types/resource';
|
||||
import {
|
||||
compareKubernetesEvents,
|
||||
filterKubernetesResources,
|
||||
kubernetesScopeLabel,
|
||||
mapKubernetesEventSeverity,
|
||||
type KubernetesResourceStatusFilter,
|
||||
isSingleKubernetesCluster,
|
||||
} from './kubernetesPageModel';
|
||||
import { KubernetesScopeText } from './KubernetesScopeText';
|
||||
|
||||
const eventName = (resource: Resource): string =>
|
||||
asTrimmedString(resource.displayName) || asTrimmedString(resource.name) || resource.id;
|
||||
@@ -59,6 +60,7 @@ export const KubernetesEventsTable: Component<{
|
||||
showToolbar?: boolean;
|
||||
}> = (props) => {
|
||||
const sortedEvents = createMemo(() => [...props.resources].sort(compareKubernetesEvents));
|
||||
const singleCluster = createMemo(() => isSingleKubernetesCluster(props.resources));
|
||||
const tableState = createPlatformTableFilterState({
|
||||
resources: sortedEvents,
|
||||
initialStatus: 'all' as KubernetesResourceStatusFilter,
|
||||
@@ -161,7 +163,6 @@ export const KubernetesEventsTable: Component<{
|
||||
const indicator = () =>
|
||||
mapKubernetesEventSeverity(resource.kubernetes?.eventType);
|
||||
const name = () => eventName(resource);
|
||||
const scope = () => kubernetesScopeLabel(resource);
|
||||
const observed = () => observedTimestamp(resource);
|
||||
const message = () =>
|
||||
formatPlatformTableTextValue(resource.kubernetes?.message);
|
||||
@@ -197,9 +198,11 @@ export const KubernetesEventsTable: Component<{
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content lg:table-cell`}
|
||||
>
|
||||
<span class="inline-block max-w-48 truncate" title={scope()}>
|
||||
{scope()}
|
||||
</span>
|
||||
<KubernetesScopeText
|
||||
resource={resource}
|
||||
singleCluster={singleCluster()}
|
||||
class="block truncate"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} platform-table-phone-hidden text-base-content`}
|
||||
@@ -210,7 +213,7 @@ export const KubernetesEventsTable: Component<{
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
|
||||
>
|
||||
<span
|
||||
class="inline-block max-w-48 truncate"
|
||||
class="block truncate"
|
||||
title={formatPlatformTableTextValue(resource.kubernetes?.reason)}
|
||||
>
|
||||
{formatPlatformTableTextValue(resource.kubernetes?.reason)}
|
||||
@@ -219,10 +222,7 @@ export const KubernetesEventsTable: Component<{
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
|
||||
>
|
||||
<span
|
||||
class="inline-block max-w-52 truncate"
|
||||
title={involvedObject(resource)}
|
||||
>
|
||||
<span class="block truncate" title={involvedObject(resource)}>
|
||||
{involvedObject(resource)}
|
||||
</span>
|
||||
</TableCell>
|
||||
@@ -234,14 +234,14 @@ export const KubernetesEventsTable: Component<{
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content`}
|
||||
>
|
||||
<span class="inline-block max-w-48 truncate" title={observed() || '—'}>
|
||||
<span class="block truncate" title={observed() || '—'}>
|
||||
<PlatformTableRelativeTimeValue value={observed()} />
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content sm:table-cell`}
|
||||
>
|
||||
<span class="inline-block max-w-[16rem] truncate" title={message()}>
|
||||
<span class="block truncate" title={message()}>
|
||||
{message()}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
@@ -32,7 +32,9 @@ import {
|
||||
filterKubernetesResources,
|
||||
kubernetesScopeLabel,
|
||||
type KubernetesResourceStatusFilter,
|
||||
isSingleKubernetesCluster,
|
||||
} from './kubernetesPageModel';
|
||||
import { KubernetesScopeText } from './KubernetesScopeText';
|
||||
|
||||
const resourceName = (resource: Resource): string =>
|
||||
asTrimmedString(resource.displayName) || asTrimmedString(resource.name) || resource.id;
|
||||
@@ -142,6 +144,7 @@ export const KubernetesNetworkingTable: Component<{
|
||||
externalSearch?: () => string;
|
||||
externalStatus?: () => KubernetesResourceStatusFilter;
|
||||
}> = (props) => {
|
||||
const singleCluster = createMemo(() => isSingleKubernetesCluster(props.resources));
|
||||
const tableState = createPlatformTableFilterState({
|
||||
resources: () => props.resources,
|
||||
initialStatus: 'all' as KubernetesResourceStatusFilter,
|
||||
@@ -265,7 +268,6 @@ export const KubernetesNetworkingTable: Component<{
|
||||
{(resource) => {
|
||||
const indicator = () => getSimpleStatusIndicator(resource.status);
|
||||
const name = () => resourceName(resource);
|
||||
const scope = () => kubernetesScopeLabel(resource);
|
||||
const address = () => addressOrHosts(resource);
|
||||
const ports = () => portLabel(resource);
|
||||
const targets = () => targetSummary(resource);
|
||||
@@ -309,9 +311,11 @@ export const KubernetesNetworkingTable: Component<{
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
|
||||
>
|
||||
<span class="inline-block max-w-48 truncate" title={scope()}>
|
||||
{scope()}
|
||||
</span>
|
||||
<KubernetesScopeText
|
||||
resource={resource}
|
||||
singleCluster={singleCluster()}
|
||||
class="block truncate"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
|
||||
@@ -321,24 +325,21 @@ export const KubernetesNetworkingTable: Component<{
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
|
||||
>
|
||||
<span
|
||||
class="inline-block max-w-[16rem] truncate"
|
||||
title={address().title}
|
||||
>
|
||||
<span class="block truncate" title={address().title}>
|
||||
{address().label}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
|
||||
>
|
||||
<span class="inline-block max-w-48 truncate" title={ports().title}>
|
||||
<span class="block truncate" title={ports().title}>
|
||||
{ports().label}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
|
||||
>
|
||||
<span class="inline-block max-w-52 truncate" title={targets().title}>
|
||||
<span class="block truncate" title={targets().title}>
|
||||
{targets().label}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
@@ -427,7 +427,7 @@ export const KubernetesNodesTable: Component<{
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden font-mono text-[11px] text-base-content md:table-cell`}
|
||||
>
|
||||
<span class="truncate inline-block max-w-40" title={runtime()}>
|
||||
<span class="block truncate" title={runtime()}>
|
||||
{runtime()}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
@@ -32,7 +32,9 @@ import {
|
||||
kubernetesScopeLabel,
|
||||
mapKubernetesPodStatus,
|
||||
type KubernetesResourceStatusFilter,
|
||||
isSingleKubernetesCluster,
|
||||
} from './kubernetesPageModel';
|
||||
import { KubernetesScopeText } from './KubernetesScopeText';
|
||||
import { KubernetesNameText } from './KubernetesNameText';
|
||||
|
||||
const podName = (resource: Resource): string =>
|
||||
@@ -143,6 +145,7 @@ export const KubernetesPodsTable: Component<{
|
||||
externalSearch?: () => string;
|
||||
externalStatus?: () => KubernetesResourceStatusFilter;
|
||||
}> = (props) => {
|
||||
const singleCluster = createMemo(() => isSingleKubernetesCluster(props.resources));
|
||||
const tableState = createPlatformTableFilterState({
|
||||
resources: () => props.resources,
|
||||
initialStatus: 'all' as KubernetesResourceStatusFilter,
|
||||
@@ -292,7 +295,6 @@ export const KubernetesPodsTable: Component<{
|
||||
{(resource) => {
|
||||
const indicator = () => mapKubernetesPodStatus(resource);
|
||||
const name = () => podName(resource);
|
||||
const scope = () => kubernetesScopeLabel(resource);
|
||||
const node = () => formatPlatformTableTextValue(resource.kubernetes?.nodeName);
|
||||
const owner = () => ownerValue(resource);
|
||||
const image = () => imageValue(resource);
|
||||
@@ -334,9 +336,11 @@ export const KubernetesPodsTable: Component<{
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
|
||||
>
|
||||
<span class="block max-w-full truncate" title={scope()}>
|
||||
{scope()}
|
||||
</span>
|
||||
<KubernetesScopeText
|
||||
resource={resource}
|
||||
singleCluster={singleCluster()}
|
||||
class="block max-w-full truncate"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} platform-table-phone-hidden text-base-content`}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Show, type Component, type JSX } from 'solid-js';
|
||||
import { createMemo, Show, type Component, type JSX } from 'solid-js';
|
||||
import { StatusDot } from '@/components/shared/StatusDot';
|
||||
import { TableCell, TableHead, TableRow } from '@/components/shared/Table';
|
||||
import { getSimpleStatusIndicator } from '@/utils/status';
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
summarizePlatformTableValues,
|
||||
PlatformTableShell,
|
||||
withPlatformStatusCounts,
|
||||
platformTableBandWidthStyle,
|
||||
} from '@/features/platformPage/sharedPlatformPage';
|
||||
import {
|
||||
PlatformResourceDetailToggleButton,
|
||||
@@ -27,9 +28,10 @@ import {
|
||||
import type { Resource } from '@/types/resource';
|
||||
import {
|
||||
filterKubernetesResources,
|
||||
kubernetesScopeLabel,
|
||||
type KubernetesResourceStatusFilter,
|
||||
isSingleKubernetesCluster,
|
||||
} from './kubernetesPageModel';
|
||||
import { KubernetesScopeText } from './KubernetesScopeText';
|
||||
|
||||
// Kubernetes policy resources carry very different API fields. Keep this table
|
||||
// on the documented NetworkPolicy, PDB, ResourceQuota, and LimitRange shapes
|
||||
@@ -187,6 +189,7 @@ export const KubernetesPolicyTable: Component<{
|
||||
externalSearch?: () => string;
|
||||
externalStatus?: () => KubernetesResourceStatusFilter;
|
||||
}> = (props) => {
|
||||
const singleCluster = createMemo(() => isSingleKubernetesCluster(props.resources));
|
||||
const tableState = createPlatformTableFilterState({
|
||||
resources: () => props.resources,
|
||||
initialStatus: 'all' as KubernetesResourceStatusFilter,
|
||||
@@ -247,26 +250,31 @@ export const KubernetesPolicyTable: Component<{
|
||||
<>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('name')} platform-table-mobile-w-30 md:w-[22%]`}
|
||||
style={platformTableBandWidthStyle(30)}
|
||||
>
|
||||
Resource
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} platform-table-mobile-w-15 md:w-[14%]`}
|
||||
class={`${getPlatformTableHeadClassForKind('text')} platform-table-mobile-w-15 md:w-[17%]`}
|
||||
style={platformTableBandWidthStyle(23)}
|
||||
>
|
||||
Kind
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} platform-table-mobile-w-15 md:w-[15%]`}
|
||||
class={`${getPlatformTableHeadClassForKind('text')} platform-table-mobile-w-15 md:w-[12%]`}
|
||||
style={platformTableBandWidthStyle(13)}
|
||||
>
|
||||
Scope
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} platform-table-mobile-w-20 md:w-[15%]`}
|
||||
style={platformTableBandWidthStyle(18)}
|
||||
>
|
||||
<PlatformResponsiveTableLabel compact="Policy" full="Policy shape" />
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} platform-table-mobile-w-20 md:w-[16%]`}
|
||||
class={`${getPlatformTableHeadClassForKind('text')} platform-table-mobile-w-20 md:w-[15%]`}
|
||||
style={platformTableBandWidthStyle(16)}
|
||||
>
|
||||
<PlatformResponsiveTableLabel compact="Limits" full="Spec / limits" />
|
||||
</TableHead>
|
||||
@@ -288,7 +296,6 @@ export const KubernetesPolicyTable: Component<{
|
||||
{(resource) => {
|
||||
const indicator = () => getSimpleStatusIndicator(resource.status);
|
||||
const name = () => policyName(resource);
|
||||
const scope = () => kubernetesScopeLabel(resource);
|
||||
const shape = () => policyShape(resource);
|
||||
const spec = () => policySpec(resource);
|
||||
const state = () => policyState(resource);
|
||||
@@ -330,35 +337,37 @@ export const KubernetesPolicyTable: Component<{
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
|
||||
>
|
||||
<span class="inline-block max-w-52 truncate" title={scope()}>
|
||||
{scope()}
|
||||
</span>
|
||||
<KubernetesScopeText
|
||||
resource={resource}
|
||||
singleCluster={singleCluster()}
|
||||
class="block truncate"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
|
||||
>
|
||||
<span class="inline-block max-w-52 truncate" title={shape().title}>
|
||||
<span class="block truncate" title={shape().title}>
|
||||
{shape().label}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
|
||||
>
|
||||
<span class="inline-block max-w-[16rem] truncate" title={spec().title}>
|
||||
<span class="block truncate" title={spec().title}>
|
||||
{spec().label}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
|
||||
>
|
||||
<span class="inline-block max-w-56 truncate" title={state().title}>
|
||||
<span class="block truncate" title={state().title}>
|
||||
{state().label}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
|
||||
>
|
||||
<span class="inline-block max-w-32 truncate" title={labels().title}>
|
||||
<span class="block truncate" title={labels().title}>
|
||||
{labels().label}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Show, type Component } from 'solid-js';
|
||||
import type { Resource } from '@/types/resource';
|
||||
import { kubernetesScopeDisplayLabel, kubernetesScopeLabel } from './kubernetesPageModel';
|
||||
|
||||
/**
|
||||
* A Kubernetes scope cell. With one cluster in view it shows the namespace
|
||||
* alone (the cluster name would repeat on every row), keeps the full
|
||||
* cluster/namespace scope on the title, and gives assistive technology the full
|
||||
* scope in place of the shortened text.
|
||||
*/
|
||||
export const KubernetesScopeText: Component<{
|
||||
resource: Resource;
|
||||
singleCluster: boolean;
|
||||
class?: string;
|
||||
}> = (props) => {
|
||||
const full = () => kubernetesScopeLabel(props.resource);
|
||||
const shown = () => kubernetesScopeDisplayLabel(props.resource, props.singleCluster);
|
||||
return (
|
||||
<span class={props.class} title={full()} data-kubernetes-scope>
|
||||
<span aria-hidden={shown() !== full() ? 'true' : undefined}>{shown()}</span>
|
||||
<Show when={shown() !== full()}>
|
||||
<span class="sr-only">{full()}</span>
|
||||
</Show>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesScopeText;
|
||||
@@ -32,7 +32,9 @@ import {
|
||||
filterKubernetesResources,
|
||||
kubernetesScopeLabel,
|
||||
type KubernetesResourceStatusFilter,
|
||||
isSingleKubernetesCluster,
|
||||
} from './kubernetesPageModel';
|
||||
import { KubernetesScopeText } from './KubernetesScopeText';
|
||||
|
||||
// Services have a distinct API contract from Ingress and EndpointSlice:
|
||||
// operators need the Service type, virtual IP, exposed ports, node ports, and
|
||||
@@ -100,6 +102,7 @@ export const KubernetesServicesTable: Component<{
|
||||
externalSearch?: () => string;
|
||||
externalStatus?: () => KubernetesResourceStatusFilter;
|
||||
}> = (props) => {
|
||||
const singleCluster = createMemo(() => isSingleKubernetesCluster(props.resources));
|
||||
const tableState = createPlatformTableFilterState({
|
||||
resources: () => props.resources,
|
||||
initialStatus: 'all' as KubernetesResourceStatusFilter,
|
||||
@@ -223,7 +226,6 @@ export const KubernetesServicesTable: Component<{
|
||||
{(resource) => {
|
||||
const indicator = () => getSimpleStatusIndicator(resource.status);
|
||||
const name = () => serviceName(resource);
|
||||
const scope = () => kubernetesScopeLabel(resource);
|
||||
const serviceType = () =>
|
||||
formatPlatformTableTextValue(resource.kubernetes?.serviceType);
|
||||
const clusterIp = () =>
|
||||
@@ -266,9 +268,11 @@ export const KubernetesServicesTable: Component<{
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
|
||||
>
|
||||
<span class="inline-block max-w-48 truncate" title={scope()}>
|
||||
{scope()}
|
||||
</span>
|
||||
<KubernetesScopeText
|
||||
resource={resource}
|
||||
singleCluster={singleCluster()}
|
||||
class="block truncate"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
|
||||
@@ -283,24 +287,21 @@ export const KubernetesServicesTable: Component<{
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} platform-table-phone-hidden text-base-content`}
|
||||
>
|
||||
<span
|
||||
class="inline-block max-w-48 truncate"
|
||||
title={externalIps().title}
|
||||
>
|
||||
<span class="block truncate" title={externalIps().title}>
|
||||
{externalIps().label}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
|
||||
>
|
||||
<span class="inline-block max-w-60 truncate" title={ports().title}>
|
||||
<span class="block truncate" title={ports().title}>
|
||||
{ports().label}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
|
||||
>
|
||||
<span class="inline-block max-w-52 truncate" title={selector().title}>
|
||||
<span class="block truncate" title={selector().title}>
|
||||
{selector().label}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
@@ -31,7 +31,9 @@ import {
|
||||
filterKubernetesResources,
|
||||
kubernetesScopeLabel,
|
||||
type KubernetesResourceStatusFilter,
|
||||
isSingleKubernetesCluster,
|
||||
} from './kubernetesPageModel';
|
||||
import { KubernetesScopeText } from './KubernetesScopeText';
|
||||
|
||||
// Kubernetes storage is intentionally not rendered by the generic
|
||||
// inventory table. StorageClass, PersistentVolume, and
|
||||
@@ -213,6 +215,7 @@ export const KubernetesStorageTable: Component<{
|
||||
title?: string;
|
||||
showToolbar?: boolean;
|
||||
}> = (props) => {
|
||||
const singleCluster = createMemo(() => isSingleKubernetesCluster(props.resources));
|
||||
const tableState = createPlatformTableFilterState({
|
||||
resources: () => props.resources,
|
||||
initialStatus: 'all' as KubernetesResourceStatusFilter,
|
||||
@@ -346,7 +349,6 @@ export const KubernetesStorageTable: Component<{
|
||||
const indicator = () => getSimpleStatusIndicator(resource.status);
|
||||
const name = () => storageName(resource);
|
||||
const kind = () => storageKind(resource);
|
||||
const scope = () => kubernetesScopeLabel(resource);
|
||||
const policy = () => policyLabel(resource);
|
||||
const target = () => bindingTarget(resource);
|
||||
const detailRowId = () => drawer.detailRowId(resource);
|
||||
@@ -386,9 +388,11 @@ export const KubernetesStorageTable: Component<{
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content sm:table-cell`}
|
||||
>
|
||||
<span class="inline-block max-w-48 truncate" title={scope()}>
|
||||
{scope()}
|
||||
</span>
|
||||
<KubernetesScopeText
|
||||
resource={resource}
|
||||
singleCluster={singleCluster()}
|
||||
class="block truncate"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
|
||||
@@ -408,17 +412,14 @@ export const KubernetesStorageTable: Component<{
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
|
||||
>
|
||||
<span class="inline-block max-w-48 truncate" title={policy().title}>
|
||||
<span class="block truncate" title={policy().title}>
|
||||
{policy().label}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
|
||||
>
|
||||
<span
|
||||
class="inline-block max-w-[18rem] truncate"
|
||||
title={target().title}
|
||||
>
|
||||
<span class="block truncate" title={target().title}>
|
||||
{target().label}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
+4
-1
@@ -61,7 +61,10 @@ describe('KubernetesAutoscalingTable', () => {
|
||||
expect(screen.getByText('Bounds')).toBeInTheDocument();
|
||||
expect(screen.getByText('Metrics')).toBeInTheDocument();
|
||||
expect(screen.getByText('checkout-api-hpa')).toBeInTheDocument();
|
||||
expect(screen.getByText('prod/apps')).toBeInTheDocument();
|
||||
expect(screen.getByText('apps').closest('[data-kubernetes-scope]')).toHaveAttribute(
|
||||
'title',
|
||||
'prod/apps',
|
||||
);
|
||||
expect(screen.getByText('Deployment/checkout-api')).toBeInTheDocument();
|
||||
expect(screen.getByText('2-10')).toBeInTheDocument();
|
||||
expect(screen.getByText('4')).toBeInTheDocument();
|
||||
|
||||
@@ -124,4 +124,39 @@ describe('KubernetesDeploymentsTable', () => {
|
||||
expect(screen.getByTitle('1 / 3 ready')).toHaveClass('bg-amber-500');
|
||||
expect(screen.getByTitle('Ready')).toHaveClass('bg-emerald-500');
|
||||
});
|
||||
|
||||
it('sizes the tablet band for a long name and keeps Desired for wide tables', () => {
|
||||
const { container } = render(() => (
|
||||
<KubernetesDeploymentsTable
|
||||
resources={[
|
||||
makeResource({
|
||||
id: 'ingress-nginx-controller',
|
||||
kubernetes: {
|
||||
namespace: 'ingress-nginx',
|
||||
resourceKind: 'Deployment',
|
||||
desiredReplicas: 2,
|
||||
readyReplicas: 2,
|
||||
},
|
||||
}),
|
||||
]}
|
||||
emptyIcon={<span />}
|
||||
emptyTitle="No deployments"
|
||||
emptyDescription="No deployments"
|
||||
showToolbar={false}
|
||||
/>
|
||||
));
|
||||
|
||||
const headers = [...container.querySelectorAll('thead th')];
|
||||
const byLabel = (label: string) =>
|
||||
headers.find((th) => th.textContent?.includes(label)) as HTMLElement | undefined;
|
||||
// The band gives the name about a third of the row and the counts little.
|
||||
expect(byLabel('Deployment')?.style.getPropertyValue('--platform-table-band-width')).toBe(
|
||||
'36%',
|
||||
);
|
||||
expect(byLabel('Ready')?.style.getPropertyValue('--platform-table-band-width')).toBe('9.5%');
|
||||
// Ready already shows the desired count as its denominator (2/2).
|
||||
const desired = byLabel('Desired');
|
||||
expect(desired).toHaveClass('hidden', 'md:table-cell');
|
||||
expect(desired?.style.getPropertyValue('--platform-table-band-width')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -112,7 +112,13 @@ describe('KubernetesPodsTable', () => {
|
||||
expect(screen.getByText('Age')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(kubernetesName('checkout-api-6c746d5bcf-c7z2p'))).toBeInTheDocument();
|
||||
expect(screen.getByText('prod-euw1/services')).toBeInTheDocument();
|
||||
// One cluster in view: the scope cell shows the namespace and keeps the
|
||||
// full cluster/namespace scope on hover.
|
||||
const scopeCell = screen.getByText('services').closest('[data-kubernetes-scope]');
|
||||
expect(scopeCell).toHaveAttribute('title', 'prod-euw1/services');
|
||||
// Assistive technology gets the full scope in place of the shortened text.
|
||||
expect(screen.getByText('services')).toHaveAttribute('aria-hidden', 'true');
|
||||
expect(scopeCell?.querySelector('.sr-only')).toHaveTextContent('prod-euw1/services');
|
||||
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.
|
||||
@@ -199,4 +205,28 @@ describe('KubernetesPodsTable', () => {
|
||||
screen.getAllByTitle('Running').some((el) => el.classList.contains('bg-emerald-500')),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps the cluster in the scope cell when several clusters are in view', () => {
|
||||
render(() => (
|
||||
<KubernetesPodsTable
|
||||
resources={[
|
||||
makeResource({
|
||||
id: 'eu-pod',
|
||||
kubernetes: { clusterName: 'prod-eu', namespace: 'services', podName: 'eu-pod' },
|
||||
}),
|
||||
makeResource({
|
||||
id: 'us-pod',
|
||||
kubernetes: { clusterName: 'prod-us', namespace: 'services', podName: 'us-pod' },
|
||||
}),
|
||||
]}
|
||||
emptyIcon={<span />}
|
||||
emptyTitle="No pods"
|
||||
emptyDescription="No pods"
|
||||
showToolbar={false}
|
||||
/>
|
||||
));
|
||||
|
||||
expect(screen.getByText('prod-eu/services')).toBeInTheDocument();
|
||||
expect(screen.getByText('prod-us/services')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { Resource } from '@/types/resource';
|
||||
import {
|
||||
getKubernetesNodesNeedingAttention,
|
||||
splitKubernetesNameTail,
|
||||
isSingleKubernetesCluster,
|
||||
kubernetesScopeDisplayLabel,
|
||||
} from '../kubernetesPageModel';
|
||||
import {
|
||||
KUBERNETES_TAB_SPECS,
|
||||
@@ -47,6 +49,52 @@ const makeResource = (resource: Partial<Resource> & Pick<Resource, 'id' | 'type'
|
||||
});
|
||||
|
||||
describe('kubernetesPageModel', () => {
|
||||
it('shows the namespace alone when one cluster is in view', () => {
|
||||
const inCluster = (id: string, clusterName: string, namespace?: string) =>
|
||||
({
|
||||
id,
|
||||
type: 'pod',
|
||||
name: id,
|
||||
kubernetes: { clusterName, ...(namespace ? { namespace } : {}) },
|
||||
}) as unknown as Resource;
|
||||
const services = inCluster('a', 'Production EU', 'services');
|
||||
const clusterRole = inCluster('b', 'Production EU');
|
||||
const otherCluster = inCluster('c', 'Staging', 'services');
|
||||
|
||||
expect(isSingleKubernetesCluster([services, clusterRole])).toBe(true);
|
||||
expect(isSingleKubernetesCluster([services, otherCluster])).toBe(false);
|
||||
expect(isSingleKubernetesCluster([])).toBe(true);
|
||||
// Two clusters can share a display name, and a row with no cluster at all
|
||||
// counts as its own, so neither case loses the cluster from the cell.
|
||||
const sameNameOtherId = {
|
||||
id: 'd',
|
||||
type: 'pod',
|
||||
name: 'd',
|
||||
kubernetes: { clusterId: 'eu-2', clusterName: 'Production EU', namespace: 'services' },
|
||||
} as unknown as Resource;
|
||||
const euOne = {
|
||||
id: 'e',
|
||||
type: 'pod',
|
||||
name: 'e',
|
||||
kubernetes: { clusterId: 'eu-1', clusterName: 'Production EU', namespace: 'apps' },
|
||||
} as unknown as Resource;
|
||||
const noCluster = {
|
||||
id: 'f',
|
||||
type: 'pod',
|
||||
name: 'f',
|
||||
kubernetes: { namespace: 'apps' },
|
||||
} as unknown as Resource;
|
||||
expect(isSingleKubernetesCluster([euOne, sameNameOtherId])).toBe(false);
|
||||
expect(isSingleKubernetesCluster([euOne, noCluster])).toBe(false);
|
||||
expect(isSingleKubernetesCluster([noCluster])).toBe(true);
|
||||
|
||||
expect(kubernetesScopeDisplayLabel(services, true)).toBe('services');
|
||||
expect(kubernetesScopeDisplayLabel(clusterRole, true)).toBe('Cluster');
|
||||
// Several clusters keep the cluster in the cell so rows stay unambiguous.
|
||||
expect(kubernetesScopeDisplayLabel(services, false)).toBe('Production EU/services');
|
||||
expect(kubernetesScopeDisplayLabel(clusterRole, false)).toBe('Production EU');
|
||||
});
|
||||
|
||||
it('splits off the generated tail that tells sibling names apart', () => {
|
||||
expect(splitKubernetesNameTail('checkout-api-6d8f9c7b5-x7k2p')).toEqual({
|
||||
head: 'checkout-api-6d8f9c7b5',
|
||||
|
||||
@@ -137,6 +137,25 @@ const podHasFatalContainer = (containers: ResourceKubernetesPodContainerStatus[]
|
||||
const podAllContainersReady = (containers: ResourceKubernetesPodContainerStatus[]): boolean =>
|
||||
containers.length > 0 && containers.every((container) => container.ready === true);
|
||||
|
||||
// With one cluster in view, every row's scope repeats the cluster name and
|
||||
// crowds out the namespace, the part that differs between rows. Tables then
|
||||
// show the namespace alone (Cluster-wide for cluster-scoped objects) and keep
|
||||
// the full cluster/namespace scope on hover. Several clusters keep both.
|
||||
// Clusters are told apart by ID (two can share a display name), and a row with
|
||||
// no cluster at all counts as its own, so a mix keeps the full scope.
|
||||
const kubernetesClusterKey = (resource: Resource): string =>
|
||||
asTrimmedString(resource.kubernetes?.clusterId) ||
|
||||
asTrimmedString(resource.kubernetes?.clusterName) ||
|
||||
'';
|
||||
|
||||
export const isSingleKubernetesCluster = (resources: readonly Resource[]): boolean =>
|
||||
new Set(resources.map(kubernetesClusterKey)).size <= 1;
|
||||
|
||||
export const kubernetesScopeDisplayLabel = (resource: Resource, singleCluster: boolean): string =>
|
||||
singleCluster
|
||||
? asTrimmedString(resource.kubernetes?.namespace) || 'Cluster'
|
||||
: kubernetesScopeLabel(resource);
|
||||
|
||||
export function mapKubernetesPodStatus(resource: Resource): StatusIndicator {
|
||||
const phase = normalizeKubernetesToken(
|
||||
resource.kubernetes?.podPhase || resource.kubernetes?.phase,
|
||||
|
||||
+34
-4
@@ -370,7 +370,7 @@ describe('platform overview layout guardrails', () => {
|
||||
/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%\]"/,
|
||||
/sortKey="namespace"[\s\S]{0,120}?class="platform-table-mobile-w-15 md:w-\[18%\]"/,
|
||||
);
|
||||
expect(kubernetesPodsTableSource).toMatch(
|
||||
/sortKey="scope"[\s\S]{0,120}?class="platform-table-mobile-w-15 md:w-\[13%\]"/,
|
||||
@@ -388,13 +388,34 @@ describe('platform overview layout guardrails', () => {
|
||||
/sortKey="class"[\s\S]{0,120}?class="platform-table-mobile-w-20 md:w-\[10%\]"/,
|
||||
);
|
||||
expect(kubernetesConfigTableSource).toMatch(
|
||||
/sortKey="scope"[\s\S]{0,120}?class="platform-table-mobile-w-20 md:w-\[16%\]"/,
|
||||
/sortKey="scope"[\s\S]{0,120}?class="platform-table-mobile-w-20 md:w-\[10%\]"/,
|
||||
);
|
||||
expect(kubernetesPolicyTableSource).toContain(
|
||||
"getPlatformTableHeadClassForKind('text')} platform-table-mobile-w-15 md:w-[15%]",
|
||||
"getPlatformTableHeadClassForKind('text')} platform-table-mobile-w-15 md:w-[12%]",
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps Kubernetes 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
|
||||
// the row's text line; a block span fills the cell and ellipsizes there.
|
||||
for (const source of [
|
||||
kubernetesAutoscalingTableSource,
|
||||
kubernetesConfigTableSource,
|
||||
kubernetesControllersTableSource,
|
||||
kubernetesEventsTableSource,
|
||||
kubernetesNetworkingTableSource,
|
||||
kubernetesNodesTableSource,
|
||||
kubernetesPolicyTableSource,
|
||||
kubernetesServicesTableSource,
|
||||
kubernetesStorageTableSource,
|
||||
]) {
|
||||
expect(source).not.toMatch(/class="[^"]*\binline-block\b[^"]*\btruncate\b/);
|
||||
expect(source).not.toMatch(/class="[^"]*\btruncate\b[^"]*\binline-block\b/);
|
||||
expect(source).toContain('class="block truncate"');
|
||||
}
|
||||
});
|
||||
|
||||
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');
|
||||
@@ -417,8 +438,17 @@ describe('platform overview layout guardrails', () => {
|
||||
expect(dockerVolumesTableSource).toMatch(
|
||||
/getPlatformTableCellClassForKind\('text'\)[\s\S]{0,100}?platform-table-phone-hidden[\s\S]{0,140}?dockerTextValue\(resource\.docker\?\.scope\)/,
|
||||
);
|
||||
// Desired is the denominator Ready already shows (2/2), so it waits for a
|
||||
// wide table rather than returning with the tablet tier.
|
||||
expect(kubernetesDeploymentsTableSource).toMatch(
|
||||
/sortKey="desired"[\s\S]{0,120}?platform-table-phone-hidden/,
|
||||
/sortKey="desired"[\s\S]{0,120}?class="hidden md:table-cell md:w-\[8%\]"/,
|
||||
);
|
||||
// On a phone Ready only shows 2/2 while Age needs room for "18h ago".
|
||||
expect(kubernetesDeploymentsTableSource).toMatch(
|
||||
/sortKey="ready"[\s\S]{0,120}?class="platform-table-mobile-w-10 md:w-\[7%\]"/,
|
||||
);
|
||||
expect(kubernetesDeploymentsTableSource).toMatch(
|
||||
/sortKey="age"[\s\S]{0,120}?class="platform-table-mobile-w-15 md:w-\[8%\] platform-table-narrow-hidden"/,
|
||||
);
|
||||
expect(truenasVirtualMachinesTableSource).toMatch(
|
||||
/sortKey="state"[\s\S]{0,120}?platform-table-phone-hidden/,
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
summarizePlatformTableValues,
|
||||
withPlatformStatusCounts,
|
||||
type PlatformResourceStatusFilter,
|
||||
platformTableBandWidthStyle,
|
||||
} from '../sharedPlatformPage';
|
||||
|
||||
afterEach(() => {
|
||||
@@ -917,4 +918,10 @@ describe('PlatformTableMetricFallback', () => {
|
||||
expect(marker.getAttribute('title')).toBe('Update this agent for full machine telemetry.');
|
||||
expect(marker.getAttribute('aria-hidden')).toBeNull();
|
||||
});
|
||||
|
||||
it('declares tablet band widths as a custom property the shell reads', () => {
|
||||
expect(platformTableBandWidthStyle(36)).toEqual({ '--platform-table-band-width': '36%' });
|
||||
expect(platformTableBandWidthStyle(9.5)).toEqual({ '--platform-table-band-width': '9.5%' });
|
||||
expect(platformTableBandWidthStyle(undefined)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -590,12 +590,23 @@ export function createPlatformTableSortState<SortKey extends string>(options: {
|
||||
return { sortKey, sortDirection, handleSort, getAriaSort, sortRows };
|
||||
}
|
||||
|
||||
// The tablet band between the phone projection and the md column widths (a
|
||||
// 34rem container below a 768px viewport) applies no md:w-[...] width, so a
|
||||
// table whose columns need unequal room there declares each header's percent
|
||||
// width through this style, which the shared shell reads only in that band.
|
||||
export const platformTableBandWidthStyle = (
|
||||
percent: number | undefined,
|
||||
): JSX.CSSProperties | undefined =>
|
||||
percent === undefined ? undefined : { '--platform-table-band-width': `${percent}%` };
|
||||
|
||||
export function PlatformSortableTableHead<SortKey extends string>(props: {
|
||||
kind: PlatformTableColumnKind;
|
||||
sort: PlatformTableSortState<SortKey>;
|
||||
// Omit to render a non-sortable header with the same canonical alignment.
|
||||
sortKey?: SortKey;
|
||||
class?: string;
|
||||
// Percent width for the tablet band (see platformTableBandWidthStyle).
|
||||
bandWidth?: number;
|
||||
children: JSX.Element;
|
||||
}) {
|
||||
const isSorted = () => props.sortKey !== undefined && props.sort.sortKey() === props.sortKey;
|
||||
@@ -611,6 +622,7 @@ export function PlatformSortableTableHead<SortKey extends string>(props: {
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()}
|
||||
aria-sort={props.sortKey !== undefined ? props.sort.getAriaSort(props.sortKey) : undefined}
|
||||
style={platformTableBandWidthStyle(props.bandWidth)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{props.children}
|
||||
|
||||
@@ -529,6 +529,15 @@
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* The Deployments Desired column returns with the 72rem tier (hidden
|
||||
md:table-cell), so Ready drops its own /desired total there and never
|
||||
before: one of the two always carries the desired count. */
|
||||
@container (min-width: 72rem) {
|
||||
.k8s-deployment-ready-total {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@container (min-width: 50rem) {
|
||||
.table-scroll-shell col.alert-history-context-track {
|
||||
visibility: visible;
|
||||
@@ -568,10 +577,6 @@
|
||||
.table-scroll-shell :is(th, td).storage-dataset-referenced-column {
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
.k8s-deployment-ready-total {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@container (min-width: 64rem) {
|
||||
@@ -1446,6 +1451,13 @@
|
||||
.table-scroll-shell > .table-fixed.platform-table th.platform-table-name-column {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
/* A table whose columns need unequal room in this band (short counts
|
||||
beside a long name) declares each width through the header's
|
||||
bandWidth, which the shell applies only here. */
|
||||
.table-scroll-shell > .table-fixed.platform-table th[style*='--platform-table-band-width'] {
|
||||
width: var(--platform-table-band-width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user