Promote Kubernetes nodes to platform tab

This commit is contained in:
rcourtman
2026-05-24 12:52:54 +01:00
parent 49c9ca7656
commit 7c58ca0f79
10 changed files with 139 additions and 12 deletions
@@ -177,7 +177,9 @@ represented until the collector has a libpod-native source. Kubernetes config
tables must preserve the same trust boundary for metadata-only ConfigMap and
Secret rows: the shared table wording may indicate metadata-only inventory, but
must not imply payload fields were read when the agent used Kubernetes
metadata-only API responses.
metadata-only API responses. Kubernetes Node inventory must also be reachable
through a dedicated native tab, not only the overview stack, while retaining the
shared `PlatformSectionTabs` shell.
1. `frontend-modern/src/components/Settings/APIAccessPanel.tsx` shared with `security-privacy`: the API Access settings intro is both a security/privacy token-management trust surface and a canonical settings-shell presentation boundary.
The panel may own shell placement and local action layout, but
@@ -5066,7 +5066,12 @@
"frontend-modern/src/features/infrastructure/InfrastructurePageSurface.tsx",
"frontend-modern/src/features/infrastructure/useInfrastructurePageRouteState.ts",
"frontend-modern/src/features/infrastructure/useInfrastructurePageState.ts",
"frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx",
"frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx",
"frontend-modern/src/features/kubernetes/KubernetesInventoryTable.tsx",
"frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx",
"frontend-modern/src/features/kubernetes/kubernetesPageModel.ts",
"frontend-modern/src/features/kubernetes/KubernetesPageSurface.tsx",
"frontend-modern/src/hooks/useUnifiedResources.ts",
"frontend-modern/src/routing/resourceLinks.ts",
"frontend-modern/src/types/resource.ts",
@@ -5194,7 +5199,12 @@
"frontend-modern/src/features/infrastructure/InfrastructurePageSurface.tsx",
"frontend-modern/src/features/infrastructure/useInfrastructurePageRouteState.ts",
"frontend-modern/src/features/infrastructure/useInfrastructurePageState.ts",
"frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx",
"frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx",
"frontend-modern/src/features/kubernetes/KubernetesInventoryTable.tsx",
"frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx",
"frontend-modern/src/features/kubernetes/kubernetesPageModel.ts",
"frontend-modern/src/features/kubernetes/KubernetesPageSurface.tsx",
"frontend-modern/src/hooks/useUnifiedResources.ts",
"frontend-modern/src/routing/resourceLinks.ts",
"frontend-modern/src/types/resource.ts",
@@ -5222,7 +5232,10 @@
"frontend-modern/src/features/docker/__tests__/DockerPageSurface.test.tsx",
"frontend-modern/src/features/infrastructure/__tests__/infrastructurePageModel.test.ts",
"frontend-modern/src/features/infrastructure/__tests__/InfrastructurePageSurface.guardrails.test.ts",
"frontend-modern/src/features/kubernetes/__tests__/KubernetesClustersTable.test.tsx",
"frontend-modern/src/features/kubernetes/__tests__/KubernetesInventoryTable.test.tsx",
"frontend-modern/src/features/kubernetes/__tests__/kubernetesPageModel.test.ts",
"frontend-modern/src/features/kubernetes/__tests__/KubernetesPageSurface.contract.test.tsx",
"frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts",
"frontend-modern/src/routing/__tests__/resourceLinks.test.ts",
"frontend-modern/src/stores/__tests__/websocket-unified.test.ts",
@@ -124,6 +124,11 @@ cross-source deduplication.
100. `frontend-modern/src/features/agents/AgentsMachinesTable.tsx`
101. `internal/platformsupport/manifest_generated.go`
102. `frontend-modern/src/features/kubernetes/KubernetesInventoryTable.tsx`
103. `frontend-modern/src/features/kubernetes/KubernetesPageSurface.tsx`
104. `frontend-modern/src/features/kubernetes/kubernetesPageModel.ts`
105. `frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx`
106. `frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx`
107. `frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx`
## Shared Boundaries
@@ -250,7 +255,7 @@ reintroduce false Swarm capability surfaces.
libpod-native source can populate a native contract instead of deriving pods
from Docker-compatible container labels.
Kubernetes inventory likewise projects native API objects as first-class
unified resources: namespaces, Services, ReplicaSets, StatefulSets,
unified resources: Nodes, namespaces, Services, ReplicaSets, StatefulSets,
DaemonSets, Jobs, CronJobs, Ingresses, EndpointSlices, NetworkPolicies,
PersistentVolumes, PersistentVolumeClaims, StorageClasses, ConfigMaps,
Secrets, ServiceAccounts, ResourceQuotas, LimitRanges,
@@ -263,6 +268,9 @@ reintroduce false Swarm capability surfaces.
carry key names, but Secret values are outside the unified resource contract.
Because Secret names and key names can disclose intent, `k8s-secret` policy
metadata must classify them as `restricted` with `local-only` routing.
Kubernetes Node inventory is an API-native resource surface, not merely
overview chrome: `/kubernetes/nodes` must expose the bespoke node table while
the overview may still include the same rows for cluster orientation.
2. Add typed accessors and views in `internal/unifiedresources/views.go`
3. Add source ingestion/adaptation in the adapter layer only
Frontend resource platform contracts in
@@ -140,7 +140,7 @@ export const KubernetesNodesTable: Component<{
>
<TableCard class={PLATFORM_TABLE_CARD_CLASS}>
<TableCardHeader title={props.title ?? 'Nodes'} />
<Table class="min-w-full table-fixed text-xs md:min-w-[1280px]">
<Table class="min-w-full table-fixed text-xs md:min-w-[1100px] 2xl:min-w-[1280px]">
<TableHeader>
<TableRow class={PLATFORM_TABLE_HEADER_ROW_CLASS}>
{/*
@@ -149,22 +149,31 @@ export const KubernetesNodesTable: Component<{
-style values, Capacity gets room for "6 cores /
51.0 GB" strings, CPU and Memory bars share an
equal slice, and the short-text columns (Cluster,
Roles, Kubelet, Uptime) trim accordingly. Mobile
widths are unchanged.
Roles, Kubelet, Uptime) trim accordingly. Wide
desktop gets extra room without forcing normal desktop
viewports to hide Capacity behind horizontal scroll.
*/}
<TableHead class={`${getPlatformTableHeadClassForKind('name')} md:w-[15%]`}>
Node
</TableHead>
<TableHead class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[10%]`}>
<TableHead
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[10%]`}
>
Cluster
</TableHead>
<TableHead class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[10%]`}>
<TableHead
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[10%]`}
>
Roles
</TableHead>
<TableHead class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[8%]`}>
<TableHead
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[8%]`}
>
Kubelet
</TableHead>
<TableHead class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[15%]`}>
<TableHead
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[15%]`}
>
Runtime
</TableHead>
<TableHead class={`${getPlatformTableHeadClassForKind('metric-bar')} md:w-[11%]`}>
@@ -173,10 +182,14 @@ export const KubernetesNodesTable: Component<{
<TableHead class={`${getPlatformTableHeadClassForKind('metric-bar')} md:w-[11%]`}>
Memory
</TableHead>
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[6%]`}>
<TableHead
class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[6%]`}
>
Uptime
</TableHead>
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[14%]`}>
<TableHead
class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[14%]`}
>
Capacity
</TableHead>
</TableRow>
@@ -98,6 +98,14 @@ export function KubernetesPageSurface() {
<Show when={activeTab() === 'overview'}>
<KubernetesOverview model={model} />
</Show>
<Show when={activeTab() === 'nodes'}>
<KubernetesNodesTable
resources={model().nodes}
emptyIcon={k8sIcon()}
emptyTitle="No nodes reported"
emptyDescription="Kubernetes nodes appear here as soon as the agent enumerates Node resources."
/>
</Show>
<Show when={activeTab() === 'workloads'}>
<KubernetesWorkloads model={model} />
</Show>
@@ -1,4 +1,4 @@
import { describe, expect, it, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { render, screen } from '@solidjs/testing-library';
import { Route, Router } from '@solidjs/router';
@@ -7,6 +7,7 @@ import { KubernetesPageSurface } from '../KubernetesPageSurface';
const mockUseUnifiedResources = vi.fn();
const mockUseWorkloadsState = vi.fn();
const mockPathname = vi.hoisted(() => vi.fn(() => '/'));
const mockWorkloadsSurface = vi.fn((props: WorkloadsSurfaceProps) => (
<div
data-testid="kubernetes-workloads-surface"
@@ -22,6 +23,14 @@ vi.mock('@/hooks/useUnifiedResources', () => ({
useUnifiedResources: (...args: unknown[]) => mockUseUnifiedResources(...args),
}));
vi.mock('@solidjs/router', async () => {
const actual = await vi.importActual<typeof import('@solidjs/router')>('@solidjs/router');
return {
...actual,
useLocation: () => ({ pathname: mockPathname() }),
};
});
vi.mock('@/components/Workloads/useWorkloadsState', () => ({
useWorkloadsState: (...args: unknown[]) => mockUseWorkloadsState(...args),
}));
@@ -50,6 +59,14 @@ vi.mock('../KubernetesDeploymentsTable', () => ({
}));
describe('KubernetesPageSurface contract', () => {
beforeEach(() => {
mockPathname.mockReturnValue('/');
});
afterEach(() => {
vi.clearAllMocks();
});
it('pins the embedded workloads surface to the pod-native Kubernetes columns', () => {
mockUseUnifiedResources.mockReturnValue({
resources: () => [
@@ -129,4 +146,61 @@ describe('KubernetesPageSurface contract', () => {
}),
);
});
it('promotes Kubernetes node inventory to a dedicated API-native tab', () => {
mockPathname.mockReturnValue('/kubernetes/nodes');
mockUseUnifiedResources.mockReturnValue({
resources: () => [
{
id: 'node-1',
type: 'k8s-node',
name: 'worker-1',
displayName: 'worker-1',
platformId: 'cluster-1',
platformType: 'kubernetes',
sourceType: 'agent',
sources: ['kubernetes'],
status: 'online',
lastSeen: 1_700_000_000_000,
},
],
loading: () => false,
error: () => null,
refetch: vi.fn(),
});
mockUseWorkloadsState.mockReturnValue({
allGuests: () => [],
clearPinnedSummaryScope: vi.fn(),
containerRuntimeFilterConfig: () => undefined,
focusedSummaryWorkloadGroupId: () => null,
groupingMode: () => 'grouped',
handleBeforeAutoFocus: vi.fn(),
search: () => '',
selectedGuestId: () => null,
setGroupingMode: vi.fn(),
setMetricDisplayMode: vi.fn(),
setSearch: vi.fn(),
setSortDirection: vi.fn(),
setSortKey: vi.fn(),
setStatusMode: vi.fn(),
setViewMode: vi.fn(),
setWorkloadMetricHistoryRange: vi.fn(),
statusMode: () => 'all',
surfaceConnected: () => false,
surfaceInitialDataReceived: () => false,
viewMode: () => 'pod',
workloadMetricDisplayMode: () => 'bars',
workloadMetricHistoryRange: () => '1h',
workloadsFilterColumnVisibility: () => undefined,
});
render(() => (
<Router>
<Route path="/" component={KubernetesPageSurface} />
</Router>
));
expect(screen.getByTestId('nodes-table')).toBeInTheDocument();
expect(screen.queryByTestId('kubernetes-workloads-surface')).not.toBeInTheDocument();
});
});
@@ -18,6 +18,7 @@ describe('kubernetesPageModel', () => {
it('declares API-native Kubernetes sections', () => {
expect(KUBERNETES_TAB_SPECS.map((tab) => tab.id)).toEqual([
'overview',
'nodes',
'workloads',
'services',
'storage',
@@ -3,6 +3,7 @@ import { resolveResourcePlatformType } from '@/utils/sourcePlatforms';
export type KubernetesPageTabId =
| 'overview'
| 'nodes'
| 'workloads'
| 'services'
| 'storage'
@@ -20,6 +21,7 @@ export type KubernetesTabSpec = {
export const KUBERNETES_TAB_SPECS: readonly KubernetesTabSpec[] = [
{ id: 'overview', label: 'Overview', path: '/kubernetes/overview' },
{ id: 'nodes', label: 'Nodes', path: '/kubernetes/nodes' },
{ id: 'workloads', label: 'Workloads', path: '/kubernetes/workloads' },
{ id: 'services', label: 'Services', path: '/kubernetes/services' },
{ id: 'storage', label: 'Storage', path: '/kubernetes/storage' },
@@ -772,7 +772,10 @@ class CanonicalCompletionGuardTest(unittest.TestCase):
"frontend-modern/src/features/docker/__tests__/dockerPageModel.test.ts",
"frontend-modern/src/features/infrastructure/__tests__/InfrastructurePageSurface.guardrails.test.ts",
"frontend-modern/src/features/infrastructure/__tests__/infrastructurePageModel.test.ts",
"frontend-modern/src/features/kubernetes/__tests__/KubernetesClustersTable.test.tsx",
"frontend-modern/src/features/kubernetes/__tests__/KubernetesInventoryTable.test.tsx",
"frontend-modern/src/features/kubernetes/__tests__/KubernetesPageSurface.contract.test.tsx",
"frontend-modern/src/features/kubernetes/__tests__/kubernetesPageModel.test.ts",
"frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts",
"frontend-modern/src/routing/__tests__/resourceLinks.test.ts",
"frontend-modern/src/stores/__tests__/websocket-unified.test.ts",
@@ -4810,7 +4810,10 @@ class SubsystemLookupTest(unittest.TestCase):
"frontend-modern/src/features/docker/__tests__/dockerPageModel.test.ts",
"frontend-modern/src/features/infrastructure/__tests__/InfrastructurePageSurface.guardrails.test.ts",
"frontend-modern/src/features/infrastructure/__tests__/infrastructurePageModel.test.ts",
"frontend-modern/src/features/kubernetes/__tests__/KubernetesClustersTable.test.tsx",
"frontend-modern/src/features/kubernetes/__tests__/KubernetesInventoryTable.test.tsx",
"frontend-modern/src/features/kubernetes/__tests__/KubernetesPageSurface.contract.test.tsx",
"frontend-modern/src/features/kubernetes/__tests__/kubernetesPageModel.test.ts",
"frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts",
"frontend-modern/src/routing/__tests__/resourceLinks.test.ts",
"frontend-modern/src/stores/__tests__/websocket-unified.test.ts",