Lift Docker / Kubernetes search haystacks into the page models

frontend-modern/src/features/platformPage/sharedPlatformPage.tsx
carried ~90 lines of resource.docker?.* and resource.kubernetes?.*
field knowledge inside matchesPlatformSearch. That coupled the
cross-platform helper to two specific platforms — every new docker.*
or kubernetes.* field anyone wanted searchable had to be edited in a
shared file, and the same fields were duplicated inside
DockerContainersTable's bespoke search-candidates helper.

dockerPageModel now exports dockerResourceSearchHaystack +
filterDockerResources, and kubernetesPageModel exports
kubernetesResourceSearchHaystack + filterKubernetesResources, each
covering the platform's own generic + meta fields and the composite
labels operators are likely to paste (e.g. "0.0.0.0:8080->80/tcp" on
container rows). All 12 Docker tables and all 12 Kubernetes tables now
import the per-platform filter; the bespoke DockerContainersTable
filter helper disappears since the page-model haystack subsumes it.

matchesPlatformSearch in sharedPlatformPage drops every docker.* and
kubernetes.* reference. The shared helper still serves the four
providers that consume it directly without a domain-specific haystack
(TrueNAS systems, Proxmox Mail Gateway, vSphere hosts, Agents), so
generic + pmg.* + vmware.* + tags stay. Existing sharedPlatformPage
test coverage of docker/k8s native metadata moves to the per-platform
page-model tests.

Verified:
- npm run type-check, lint:eslint, lint:theme, lint:canonical-platforms clean
- vitest: 138 tests pass across docker / kubernetes / platformPage suites
- browser proof on /docker/containers searching "traefik" trims 70 -> 6
  by docker.image; on /kubernetes/workloads searching "payments" trims
  42 -> 9 by kubernetes.namespace, "production-eu" trims to 14 by
  kubernetes.clusterName
This commit is contained in:
rcourtman
2026-05-25 07:47:47 +01:00
parent ec2bd4007f
commit 51efea045b
29 changed files with 567 additions and 264 deletions
@@ -17,10 +17,8 @@ import {
PlatformTableEmptyState,
PlatformTableToolbar,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import {
DockerResourceNameCell,
@@ -29,12 +27,13 @@ import {
dockerTextValue,
type DockerNativeTableProps,
} from './DockerNativeTableShared';
import { filterDockerResources, type DockerResourceStatusFilter } from './dockerPageModel';
export const DockerConfigsTable: Component<DockerNativeTableProps> = (props) => {
const tableState = createPlatformTableFilterState({
resources: () => props.resources,
initialStatus: 'all' as PlatformResourceStatusFilter,
filter: filterPlatformResources,
initialStatus: 'all' as DockerResourceStatusFilter,
filter: filterDockerResources,
});
return (
@@ -18,10 +18,8 @@ import {
PlatformTableEmptyState,
PlatformTableToolbar,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import {
DockerResourceNameCell,
@@ -31,7 +29,12 @@ import {
dockerTextValue,
type DockerNativeTableProps,
} from './DockerNativeTableShared';
import { compareDockerContainers, mapDockerContainerStatus } from './dockerPageModel';
import {
compareDockerContainers,
filterDockerResources,
mapDockerContainerStatus,
type DockerResourceStatusFilter,
} from './dockerPageModel';
import type { Resource } from '@/types/resource';
type DockerPort = NonNullable<NonNullable<Resource['docker']>['ports']>[number];
@@ -119,82 +122,11 @@ const updateStatusTitle = (resource: Resource): string => {
);
};
const dockerContainerSearchCandidates = (resource: Resource): Array<string | undefined> => [
resource.name,
resource.displayName,
resource.id,
resource.parentName,
resource.platformId,
resource.platformType,
resource.agent?.hostname,
resource.identity?.hostname,
resource.canonicalIdentity?.displayName,
resource.canonicalIdentity?.hostname,
resource.canonicalIdentity?.primaryId,
...(resource.canonicalIdentity?.aliases ?? []),
resource.docker?.containerId,
resource.docker?.displayName,
resource.docker?.hostname,
resource.docker?.runtime,
resource.docker?.runtimeVersion,
resource.docker?.dockerVersion,
resource.docker?.image,
resource.docker?.imageId,
resource.docker?.containerState,
resource.docker?.health,
numberSearchValue(resource.docker?.restartCount),
numberSearchValue(resource.docker?.exitCode),
updateStatusLabel(resource),
resource.docker?.updateStatus?.error,
resource.docker?.updateStatus?.currentDigest,
resource.docker?.updateStatus?.latestDigest,
...(resource.docker?.ports?.flatMap((port) => [
portLabel(port),
port.ip,
numberSearchValue(port.privatePort),
numberSearchValue(port.publicPort),
port.protocol,
]) ?? []),
...(resource.docker?.networks?.flatMap((network) => [
networkLabel(network),
network.name,
network.ipv4,
network.ipv6,
]) ?? []),
...(resource.docker?.mounts?.flatMap((mount) => [
mountLabel(mount),
mount.type,
mount.source,
mount.destination,
mount.mode,
mount.rw === false ? 'read-only' : mount.rw === true ? 'read-write' : undefined,
]) ?? []),
...(resource.tags ?? []),
];
const matchesDockerContainerSearch = (resource: Resource, search: string): boolean => {
const needle = search.trim().toLowerCase();
if (!needle) return true;
return dockerContainerSearchCandidates(resource)
.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
.some((value) => value.toLowerCase().includes(needle));
};
const filterDockerContainerResources = (
resources: Resource[],
search: string,
status: PlatformResourceStatusFilter,
): Resource[] =>
filterPlatformResources(resources, '', status).filter((resource) =>
matchesDockerContainerSearch(resource, search),
);
export const DockerContainersTable: Component<DockerNativeTableProps> = (props) => {
const tableState = createPlatformTableFilterState({
resources: () => props.resources,
initialStatus: 'all' as PlatformResourceStatusFilter,
filter: filterDockerContainerResources,
initialStatus: 'all' as DockerResourceStatusFilter,
filter: filterDockerResources,
});
const sortedRows = createMemo(() => [...tableState.filtered()].sort(compareDockerContainers));
@@ -26,14 +26,17 @@ import {
PlatformTableToolbar,
PlatformTableEmptyState,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import type { Disk } from '@/types/api';
import type { Resource } from '@/types/resource';
import { getDockerHostSystemBadge, hasDockerSwarmEvidence } from './dockerPageModel';
import {
filterDockerResources,
getDockerHostSystemBadge,
hasDockerSwarmEvidence,
type DockerResourceStatusFilter,
} from './dockerPageModel';
// Docker / Podman hosts are container hosts, not generic Pulse Agents.
// The operator columns that matter are runtime version, container count,
@@ -106,8 +109,8 @@ export const DockerHostsTable: Component<{
}> = (props) => {
const tableState = createPlatformTableFilterState({
resources: () => props.resources,
initialStatus: 'all' as PlatformResourceStatusFilter,
filter: filterPlatformResources,
initialStatus: 'all' as DockerResourceStatusFilter,
filter: filterDockerResources,
});
const showSwarmColumn = createMemo(() => props.resources.some(hasDockerSwarmEvidence));
const [selectedHostId, setSelectedHostId] = createSignal<string | null>(null);
@@ -17,10 +17,8 @@ import {
PlatformTableEmptyState,
PlatformTableToolbar,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import {
DockerResourceNameCell,
@@ -30,12 +28,13 @@ import {
dockerNumberValue,
type DockerNativeTableProps,
} from './DockerNativeTableShared';
import { filterDockerResources, type DockerResourceStatusFilter } from './dockerPageModel';
export const DockerImagesTable: Component<DockerNativeTableProps> = (props) => {
const tableState = createPlatformTableFilterState({
resources: () => props.resources,
initialStatus: 'all' as PlatformResourceStatusFilter,
filter: filterPlatformResources,
initialStatus: 'all' as DockerResourceStatusFilter,
filter: filterDockerResources,
});
return (
@@ -17,10 +17,8 @@ import {
PlatformTableEmptyState,
PlatformTableToolbar,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import {
DockerResourceNameCell,
@@ -29,6 +27,7 @@ import {
dockerTextValue,
type DockerNativeTableProps,
} from './DockerNativeTableShared';
import { filterDockerResources, type DockerResourceStatusFilter } from './dockerPageModel';
import type { Resource } from '@/types/resource';
const networkFlags = (resource: Resource): string =>
@@ -61,8 +60,8 @@ const networkSubnets = (resource: Resource): string =>
export const DockerNetworksTable: Component<DockerNativeTableProps> = (props) => {
const tableState = createPlatformTableFilterState({
resources: () => props.resources,
initialStatus: 'all' as PlatformResourceStatusFilter,
filter: filterPlatformResources,
initialStatus: 'all' as DockerResourceStatusFilter,
filter: filterDockerResources,
});
return (
@@ -17,10 +17,8 @@ import {
PlatformTableEmptyState,
PlatformTableToolbar,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import {
DockerResourceNameCell,
@@ -29,12 +27,13 @@ import {
dockerTextValue,
type DockerNativeTableProps,
} from './DockerNativeTableShared';
import { filterDockerResources, type DockerResourceStatusFilter } from './dockerPageModel';
export const DockerSecretsTable: Component<DockerNativeTableProps> = (props) => {
const tableState = createPlatformTableFilterState({
resources: () => props.resources,
initialStatus: 'all' as PlatformResourceStatusFilter,
filter: filterPlatformResources,
initialStatus: 'all' as DockerResourceStatusFilter,
filter: filterDockerResources,
});
return (
@@ -19,13 +19,16 @@ import {
PlatformTableToolbar,
PlatformTableEmptyState,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import type { Resource } from '@/types/resource';
import { compareDockerServices, mapDockerServiceStatus } from './dockerPageModel';
import {
compareDockerServices,
filterDockerResources,
mapDockerServiceStatus,
type DockerResourceStatusFilter,
} from './dockerPageModel';
// Docker Swarm services are cluster-scoped declarations, not running
// processes — they have no CPU / Memory / Disk / Disk I/O / Uptime /
@@ -89,8 +92,8 @@ export const DockerServicesTable: Component<{
}> = (props) => {
const tableState = createPlatformTableFilterState({
resources: () => props.resources,
initialStatus: 'all' as PlatformResourceStatusFilter,
filter: filterPlatformResources,
initialStatus: 'all' as DockerResourceStatusFilter,
filter: filterDockerResources,
});
const sortedRows = createMemo(() => [...tableState.filtered()].sort(compareDockerServices));
@@ -21,15 +21,15 @@ import {
PlatformTableEmptyState,
PlatformTableToolbar,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import type { DockerStorageUsageMeta, Resource } from '@/types/resource';
import {
filterDockerResources,
hasDockerEngineStorageUsage,
hasDockerStorageUsageBucket,
type DockerResourceStatusFilter,
} from './dockerPageModel';
const bucketValue = (bucket?: DockerStorageUsageMeta): JSX.Element => {
@@ -58,8 +58,8 @@ export const DockerStorageUsageTable: Component<{
const storageHosts = () => props.hosts.filter(hasDockerEngineStorageUsage);
const tableState = createPlatformTableFilterState({
resources: storageHosts,
initialStatus: 'all' as PlatformResourceStatusFilter,
filter: filterPlatformResources,
initialStatus: 'all' as DockerResourceStatusFilter,
filter: filterDockerResources,
});
const hasFilteredSourceRows = () => (props.sourceCount ?? props.hosts.length) > 0;
@@ -17,10 +17,8 @@ import {
PlatformTableEmptyState,
PlatformTableToolbar,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import {
DockerResourceNameCell,
@@ -29,13 +27,18 @@ import {
dockerTextValue,
type DockerNativeTableProps,
} from './DockerNativeTableShared';
import { compareDockerSwarmNodes, mapDockerSwarmNodeStatus } from './dockerPageModel';
import {
compareDockerSwarmNodes,
filterDockerResources,
mapDockerSwarmNodeStatus,
type DockerResourceStatusFilter,
} from './dockerPageModel';
export const DockerSwarmNodesTable: Component<DockerNativeTableProps> = (props) => {
const tableState = createPlatformTableFilterState({
resources: () => props.resources,
initialStatus: 'all' as PlatformResourceStatusFilter,
filter: filterPlatformResources,
initialStatus: 'all' as DockerResourceStatusFilter,
filter: filterDockerResources,
});
const sortedRows = createMemo(() => [...tableState.filtered()].sort(compareDockerSwarmNodes));
@@ -17,10 +17,8 @@ import {
PlatformTableEmptyState,
PlatformTableToolbar,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import {
DockerResourceNameCell,
@@ -28,13 +26,18 @@ import {
dockerTextValue,
type DockerNativeTableProps,
} from './DockerNativeTableShared';
import { compareDockerTasks, mapDockerTaskStatus } from './dockerPageModel';
import {
compareDockerTasks,
filterDockerResources,
mapDockerTaskStatus,
type DockerResourceStatusFilter,
} from './dockerPageModel';
export const DockerTasksTable: Component<DockerNativeTableProps> = (props) => {
const tableState = createPlatformTableFilterState({
resources: () => props.resources,
initialStatus: 'all' as PlatformResourceStatusFilter,
filter: filterPlatformResources,
initialStatus: 'all' as DockerResourceStatusFilter,
filter: filterDockerResources,
});
const sortedRows = createMemo(() => [...tableState.filtered()].sort(compareDockerTasks));
@@ -17,10 +17,8 @@ import {
PlatformTableEmptyState,
PlatformTableToolbar,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import {
DockerResourceNameCell,
@@ -29,12 +27,13 @@ import {
dockerTextValue,
type DockerNativeTableProps,
} from './DockerNativeTableShared';
import { filterDockerResources, type DockerResourceStatusFilter } from './dockerPageModel';
export const DockerVolumesTable: Component<DockerNativeTableProps> = (props) => {
const tableState = createPlatformTableFilterState({
resources: () => props.resources,
initialStatus: 'all' as PlatformResourceStatusFilter,
filter: filterPlatformResources,
initialStatus: 'all' as DockerResourceStatusFilter,
filter: filterDockerResources,
});
return (
@@ -8,6 +8,7 @@ import {
compareDockerServices,
compareDockerSwarmNodes,
compareDockerTasks,
filterDockerResources,
getDockerHostSystemBadge,
getDockerPageTabSpecs,
hasDockerEngineStorageUsage,
@@ -631,4 +632,78 @@ describe('dockerPageModel', () => {
expect(model.services.map((r) => r.id)).toEqual(['svc-down', 'svc-ok']);
});
});
describe('filterDockerResources', () => {
const rows: Resource[] = [
makeResource({ id: 'web', type: 'app-container', status: 'online' }),
makeResource({
id: 'edge-proxy',
type: 'app-container',
status: 'online',
docker: {
hostname: 'edge-01',
image: 'traefik:v3.1',
runtime: 'docker',
runtimeVersion: '27.5.1',
containerState: 'running',
ports: [{ ip: '0.0.0.0', publicPort: 8080, privatePort: 80, protocol: 'tcp' }],
},
}),
makeResource({
id: 'svc-payments',
type: 'docker-service',
status: 'degraded',
docker: {
serviceName: 'payments-worker',
swarm: { clusterName: 'prod-swarm', nodeRole: 'manager' },
},
}),
makeResource({
id: 'redis-vol',
type: 'docker-volume',
status: 'online',
docker: { volumeName: 'redis-data', driver: 'local' },
}),
];
it('matches docker.* fields the shared filter no longer carries', () => {
expect(filterDockerResources(rows, 'edge-01', 'all').map((r) => r.id)).toEqual([
'edge-proxy',
]);
expect(filterDockerResources(rows, 'traefik', 'all').map((r) => r.id)).toEqual([
'edge-proxy',
]);
expect(filterDockerResources(rows, 'payments-worker', 'all').map((r) => r.id)).toEqual([
'svc-payments',
]);
expect(filterDockerResources(rows, 'prod-swarm', 'all').map((r) => r.id)).toEqual([
'svc-payments',
]);
expect(filterDockerResources(rows, 'redis-data', 'all').map((r) => r.id)).toEqual([
'redis-vol',
]);
});
it('matches composite port tokens like host:public->private/proto', () => {
expect(filterDockerResources(rows, '0.0.0.0:8080->80/tcp', 'all').map((r) => r.id)).toEqual([
'edge-proxy',
]);
});
it('still combines status and search', () => {
expect(
filterDockerResources(rows, 'payments-worker', 'degraded').map((r) => r.id),
).toEqual(['svc-payments']);
expect(filterDockerResources(rows, 'payments-worker', 'online')).toEqual([]);
});
it('returns all rows for empty search and triad filters by status', () => {
expect(filterDockerResources(rows, '', 'all').map((r) => r.id).sort()).toEqual(
['edge-proxy', 'redis-vol', 'svc-payments', 'web'].sort(),
);
expect(filterDockerResources(rows, '', 'degraded').map((r) => r.id)).toEqual([
'svc-payments',
]);
});
});
});
@@ -209,6 +209,146 @@ export const compareDockerServices = compareByDockerStatus(mapDockerServiceStatu
export const compareDockerTasks = compareByDockerStatus(mapDockerTaskStatus);
export const compareDockerSwarmNodes = compareByDockerStatus(mapDockerSwarmNodeStatus);
export type DockerResourceStatusFilter = 'all' | 'online' | 'degraded' | 'offline';
const ONLINE_STATUSES = new Set<string>(['online', 'running']);
const DEGRADED_STATUSES = new Set<string>(['degraded', 'paused']);
const OFFLINE_STATUSES = new Set<string>(['offline', 'stopped']);
const mapResourceStatusToTriad = (
status: string | undefined,
): Exclude<DockerResourceStatusFilter, 'all'> | 'unknown' => {
if (!status) return 'unknown';
if (ONLINE_STATUSES.has(status)) return 'online';
if (DEGRADED_STATUSES.has(status)) return 'degraded';
if (OFFLINE_STATUSES.has(status)) return 'offline';
return 'unknown';
};
const numberToken = (value: number | undefined): string | undefined =>
typeof value === 'number' ? String(value) : undefined;
const dockerPortToken = (
port: NonNullable<NonNullable<Resource['docker']>['ports']>[number],
): string => {
const protocol = asTrimmedString(port.protocol)?.toLowerCase() || 'tcp';
const privatePort = numberToken(port.privatePort);
const publicPort = numberToken(port.publicPort);
const ip = asTrimmedString(port.ip);
if (privatePort && publicPort) {
return `${ip ? `${ip}:` : ''}${publicPort}->${privatePort}/${protocol}`;
}
if (privatePort) return `${privatePort}/${protocol}`;
if (publicPort) return `${ip ? `${ip}:` : ''}${publicPort}/${protocol}`;
return protocol;
};
// Builds the lowercase search haystack a Docker page table consults when
// filtering rows. The shared platformPage helper carries only generic Resource
// fields; docker.* lookups live here so the cross-platform helper does not
// have to know which platforms exist.
export function dockerResourceSearchHaystack(resource: Resource): string {
const docker = resource.docker;
return [
resource.id,
resource.name,
resource.displayName,
resource.parentName,
resource.platformId,
resource.platformType,
resource.agent?.hostname,
resource.identity?.hostname,
resource.canonicalIdentity?.displayName,
resource.canonicalIdentity?.hostname,
resource.canonicalIdentity?.primaryId,
...(resource.canonicalIdentity?.aliases ?? []),
docker?.hostname,
docker?.displayName,
docker?.runtime,
docker?.runtimeVersion,
docker?.dockerVersion,
docker?.containerId,
docker?.image,
docker?.imageId,
docker?.containerState,
docker?.health,
numberToken(docker?.restartCount),
numberToken(docker?.exitCode),
docker?.updateStatus?.error,
docker?.updateStatus?.currentDigest,
docker?.updateStatus?.latestDigest,
docker?.volumeName,
docker?.networkId,
docker?.driver,
docker?.mountpoint,
docker?.serviceName,
docker?.taskId,
docker?.nodeId,
docker?.nodeName,
docker?.nodeRole,
docker?.availability,
docker?.address,
docker?.managerReachability,
docker?.managerAddress,
docker?.engineVersion,
docker?.secretName,
docker?.configName,
docker?.mode,
docker?.currentState,
docker?.desiredState,
docker?.message,
docker?.error,
docker?.swarm?.clusterId,
docker?.swarm?.clusterName,
docker?.swarm?.nodeRole,
docker?.swarm?.localState,
...(docker?.ports?.flatMap((port) => [
dockerPortToken(port),
port.ip,
port.protocol,
numberToken(port.privatePort),
numberToken(port.publicPort),
]) ?? []),
...(docker?.networks?.flatMap((network) => [network.name, network.ipv4, network.ipv6]) ?? []),
...(docker?.mounts?.flatMap((mount) => [
mount.type,
mount.source,
mount.destination,
mount.mode,
mount.rw === false ? 'read-only' : mount.rw === true ? 'read-write' : undefined,
]) ?? []),
...(docker?.repoTags ?? []),
...(docker?.repoDigests ?? []),
...(resource.tags ?? []),
]
.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
.join(' ')
.toLowerCase();
}
export function filterDockerResources(
resources: Resource[],
search: string,
status: DockerResourceStatusFilter,
): Resource[] {
const needle = search.trim().toLowerCase();
const result: Resource[] = [];
for (const resource of resources) {
if (status !== 'all') {
const triad = mapResourceStatusToTriad(resource.status);
if (triad !== status) continue;
}
if (!needle) {
result.push(resource);
continue;
}
if (dockerResourceSearchHaystack(resource).includes(needle)) {
result.push(resource);
}
}
return result;
}
export function hasDockerSwarmEvidence(resource: Resource): boolean {
const swarm = resource.docker?.swarm;
if (!swarm) return false;
@@ -20,12 +20,14 @@ import {
PlatformTableEmptyState,
PlatformTableToolbar,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import type { Resource } from '@/types/resource';
import {
filterKubernetesResources,
type KubernetesResourceStatusFilter,
} from './kubernetesPageModel';
const textValue = (value: string | undefined): string => asTrimmedString(value) || '—';
@@ -86,8 +88,8 @@ export const KubernetesAutoscalingTable: Component<{
}> = (props) => {
const tableState = createPlatformTableFilterState({
resources: () => props.resources,
initialStatus: 'all' as PlatformResourceStatusFilter,
filter: filterPlatformResources,
initialStatus: 'all' as KubernetesResourceStatusFilter,
filter: filterKubernetesResources,
});
return (
@@ -23,10 +23,8 @@ import {
PlatformTableToolbar,
PlatformTableEmptyState,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailTableRow,
@@ -35,7 +33,11 @@ import {
getPlatformResourceDetailRowClass,
} from '@/features/platformPage/PlatformResourceDetailTableRow';
import type { Resource } from '@/types/resource';
import { buildKubernetesClusterChildCounts } from './kubernetesPageModel';
import {
buildKubernetesClusterChildCounts,
filterKubernetesResources,
type KubernetesResourceStatusFilter,
} from './kubernetesPageModel';
// Kubernetes clusters are control-plane aggregates, not single processes —
// they have no per-cluster Disk I/O / Uptime / Temperature concepts that
@@ -72,8 +74,8 @@ export const KubernetesClustersTable: Component<{
}> = (props) => {
const tableState = createPlatformTableFilterState({
resources: () => props.clusters,
initialStatus: 'all' as PlatformResourceStatusFilter,
filter: filterPlatformResources,
initialStatus: 'all' as KubernetesResourceStatusFilter,
filter: filterKubernetesResources,
});
const drawer = createPlatformResourceDetailState({ idPrefix: 'kubernetes-cluster-drawer' });
const resolveResourceLabel = createPlatformResourceLabelResolver(() => props.scope);
@@ -20,12 +20,14 @@ import {
PlatformTableEmptyState,
PlatformTableToolbar,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import type { Resource } from '@/types/resource';
import {
filterKubernetesResources,
type KubernetesResourceStatusFilter,
} from './kubernetesPageModel';
// ConfigMaps and Secrets are intentionally rendered as API metadata. Pulse may
// know that keys exist, but metadata-only collection must never imply that
@@ -157,8 +159,8 @@ export const KubernetesConfigTable: Component<{
}> = (props) => {
const tableState = createPlatformTableFilterState({
resources: () => props.resources,
initialStatus: 'all' as PlatformResourceStatusFilter,
filter: filterPlatformResources,
initialStatus: 'all' as KubernetesResourceStatusFilter,
filter: filterKubernetesResources,
});
return (
@@ -20,15 +20,15 @@ import {
PlatformTableEmptyState,
PlatformTableToolbar,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import type { Resource } from '@/types/resource';
import {
compareKubernetesControllers,
filterKubernetesResources,
mapKubernetesControllerStatus,
type KubernetesResourceStatusFilter,
} from './kubernetesPageModel';
const textValue = (value: string | undefined): string => asTrimmedString(value) || '—';
@@ -173,8 +173,8 @@ export const KubernetesControllersTable: Component<{
}> = (props) => {
const tableState = createPlatformTableFilterState({
resources: () => props.resources,
initialStatus: 'all' as PlatformResourceStatusFilter,
filter: filterPlatformResources,
initialStatus: 'all' as KubernetesResourceStatusFilter,
filter: filterKubernetesResources,
});
const sortedRows = createMemo(() =>
[...tableState.filtered()].sort(compareKubernetesControllers),
@@ -20,15 +20,15 @@ import {
PlatformTableToolbar,
PlatformTableEmptyState,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import type { Resource } from '@/types/resource';
import {
compareKubernetesDeployments,
filterKubernetesResources,
mapKubernetesDeploymentStatus,
type KubernetesResourceStatusFilter,
} from './kubernetesPageModel';
// Kubernetes Deployments are scheduling abstractions over their controlled
@@ -57,8 +57,8 @@ export const KubernetesDeploymentsTable: Component<{
}> = (props) => {
const tableState = createPlatformTableFilterState({
resources: () => props.resources,
initialStatus: 'all' as PlatformResourceStatusFilter,
filter: filterPlatformResources,
initialStatus: 'all' as KubernetesResourceStatusFilter,
filter: filterKubernetesResources,
});
const sortedRows = createMemo(() =>
[...tableState.filtered()].sort(compareKubernetesDeployments),
@@ -19,13 +19,16 @@ import {
PlatformTableEmptyState,
PlatformTableToolbar,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import type { Resource } from '@/types/resource';
import { compareKubernetesEvents, mapKubernetesEventSeverity } from './kubernetesPageModel';
import {
compareKubernetesEvents,
filterKubernetesResources,
mapKubernetesEventSeverity,
type KubernetesResourceStatusFilter,
} from './kubernetesPageModel';
const textValue = (value: string | undefined): string => asTrimmedString(value) || '—';
@@ -69,8 +72,8 @@ export const KubernetesEventsTable: Component<{
const sortedEvents = createMemo(() => [...props.resources].sort(compareKubernetesEvents));
const tableState = createPlatformTableFilterState({
resources: sortedEvents,
initialStatus: 'all' as PlatformResourceStatusFilter,
filter: filterPlatformResources,
initialStatus: 'all' as KubernetesResourceStatusFilter,
filter: filterKubernetesResources,
});
return (
@@ -20,10 +20,8 @@ import {
PlatformTableEmptyState,
PlatformTableToolbar,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailTableRow,
@@ -32,6 +30,10 @@ import {
getPlatformResourceDetailRowClass,
} from '@/features/platformPage/PlatformResourceDetailTableRow';
import type { Resource } from '@/types/resource';
import {
filterKubernetesResources,
type KubernetesResourceStatusFilter,
} from './kubernetesPageModel';
const textValue = (value: string | undefined): string => asTrimmedString(value) || '—';
@@ -139,8 +141,8 @@ export const KubernetesNetworkingTable: Component<{
}> = (props) => {
const tableState = createPlatformTableFilterState({
resources: () => props.resources,
initialStatus: 'all' as PlatformResourceStatusFilter,
filter: filterPlatformResources,
initialStatus: 'all' as KubernetesResourceStatusFilter,
filter: filterKubernetesResources,
});
const drawer = createPlatformResourceDetailState({ idPrefix: 'kubernetes-networking-drawer' });
const resolveResourceLabel = createPlatformResourceLabelResolver(() => props.resources);
@@ -22,10 +22,8 @@ import {
PlatformTableToolbar,
PlatformTableEmptyState,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailTableRow,
@@ -34,7 +32,12 @@ import {
getPlatformResourceDetailRowClass,
} from '@/features/platformPage/PlatformResourceDetailTableRow';
import type { Resource } from '@/types/resource';
import { compareKubernetesNodes, mapKubernetesNodeStatus } from './kubernetesPageModel';
import {
compareKubernetesNodes,
filterKubernetesResources,
mapKubernetesNodeStatus,
type KubernetesResourceStatusFilter,
} from './kubernetesPageModel';
// Kubernetes nodes carry richer Kubelet/runtime metadata than a generic
// Pulse Agent — kubelet version, container runtime, roles
@@ -96,8 +99,8 @@ export const KubernetesNodesTable: Component<{
}> = (props) => {
const tableState = createPlatformTableFilterState({
resources: () => props.resources,
initialStatus: 'all' as PlatformResourceStatusFilter,
filter: filterPlatformResources,
initialStatus: 'all' as KubernetesResourceStatusFilter,
filter: filterKubernetesResources,
});
const drawer = createPlatformResourceDetailState({ idPrefix: 'kubernetes-node-drawer' });
const resolveResourceLabel = createPlatformResourceLabelResolver(() => props.resources);
@@ -19,13 +19,16 @@ import {
PlatformTableEmptyState,
PlatformTableToolbar,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import type { Resource, ResourceKubernetesPodContainerStatus } from '@/types/resource';
import { compareKubernetesPods, mapKubernetesPodStatus } from './kubernetesPageModel';
import {
compareKubernetesPods,
filterKubernetesResources,
mapKubernetesPodStatus,
type KubernetesResourceStatusFilter,
} from './kubernetesPageModel';
const textValue = (value: string | undefined): string => asTrimmedString(value) || '—';
@@ -96,8 +99,8 @@ export const KubernetesPodsTable: Component<{
}> = (props) => {
const tableState = createPlatformTableFilterState({
resources: () => props.resources,
initialStatus: 'all' as PlatformResourceStatusFilter,
filter: filterPlatformResources,
initialStatus: 'all' as KubernetesResourceStatusFilter,
filter: filterKubernetesResources,
});
const sortedRows = createMemo(() => [...tableState.filtered()].sort(compareKubernetesPods));
@@ -20,12 +20,14 @@ import {
PlatformTableEmptyState,
PlatformTableToolbar,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import type { Resource } from '@/types/resource';
import {
filterKubernetesResources,
type KubernetesResourceStatusFilter,
} from './kubernetesPageModel';
// Kubernetes policy resources carry very different API fields. Keep this table
// on the documented NetworkPolicy, PDB, ResourceQuota, and LimitRange shapes
@@ -209,8 +211,8 @@ export const KubernetesPolicyTable: Component<{
}> = (props) => {
const tableState = createPlatformTableFilterState({
resources: () => props.resources,
initialStatus: 'all' as PlatformResourceStatusFilter,
filter: filterPlatformResources,
initialStatus: 'all' as KubernetesResourceStatusFilter,
filter: filterKubernetesResources,
});
return (
@@ -20,10 +20,8 @@ import {
PlatformTableEmptyState,
PlatformTableToolbar,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailTableRow,
@@ -32,6 +30,10 @@ import {
getPlatformResourceDetailRowClass,
} from '@/features/platformPage/PlatformResourceDetailTableRow';
import type { Resource } from '@/types/resource';
import {
filterKubernetesResources,
type KubernetesResourceStatusFilter,
} from './kubernetesPageModel';
// Services have a distinct API contract from Ingress and EndpointSlice:
// operators need the Service type, virtual IP, exposed ports, node ports, and
@@ -98,8 +100,8 @@ export const KubernetesServicesTable: Component<{
}> = (props) => {
const tableState = createPlatformTableFilterState({
resources: () => props.resources,
initialStatus: 'all' as PlatformResourceStatusFilter,
filter: filterPlatformResources,
initialStatus: 'all' as KubernetesResourceStatusFilter,
filter: filterKubernetesResources,
});
const drawer = createPlatformResourceDetailState({ idPrefix: 'kubernetes-service-drawer' });
const resolveResourceLabel = createPlatformResourceLabelResolver(() => props.resources);
@@ -21,10 +21,8 @@ import {
PlatformTableEmptyState,
PlatformTableToolbar,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailTableRow,
@@ -33,6 +31,10 @@ import {
getPlatformResourceDetailRowClass,
} from '@/features/platformPage/PlatformResourceDetailTableRow';
import type { Resource } from '@/types/resource';
import {
filterKubernetesResources,
type KubernetesResourceStatusFilter,
} from './kubernetesPageModel';
// Kubernetes storage is intentionally not rendered by the generic
// inventory table. StorageClass, PersistentVolume, and
@@ -168,8 +170,8 @@ export const KubernetesStorageTable: Component<{
}> = (props) => {
const tableState = createPlatformTableFilterState({
resources: () => props.resources,
initialStatus: 'all' as PlatformResourceStatusFilter,
filter: filterPlatformResources,
initialStatus: 'all' as KubernetesResourceStatusFilter,
filter: filterKubernetesResources,
});
const drawer = createPlatformResourceDetailState({ idPrefix: 'kubernetes-storage-drawer' });
const resolveResourceLabel = createPlatformResourceLabelResolver(() => props.resources);
@@ -5,6 +5,7 @@ import {
buildKubernetesClusterChildCounts,
buildKubernetesPageModel,
compareKubernetesControllers,
filterKubernetesResources,
compareKubernetesDeployments,
compareKubernetesEvents,
compareKubernetesNodes,
@@ -656,4 +657,68 @@ describe('kubernetesPageModel', () => {
).toBe(0);
});
});
describe('filterKubernetesResources', () => {
const rows: Resource[] = [
makeResource({
id: 'pod-checkout',
type: 'pod',
status: 'online',
kubernetes: {
clusterName: 'prod-cluster',
namespace: 'payments',
podName: 'checkout-api-abc',
ownerKind: 'Deployment',
ownerName: 'checkout-api',
image: 'ghcr.io/pulse-demo/checkout-api:2026.04',
},
}),
makeResource({
id: 'svc-checkout',
type: 'k8s-service',
status: 'online',
kubernetes: { serviceType: 'ClusterIP', clusterIp: '10.0.5.4', namespace: 'payments' },
}),
makeResource({
id: 'node-worker',
type: 'k8s-node',
status: 'degraded',
kubernetes: { kubeletVersion: 'v1.30.4', roles: ['worker'] },
}),
makeResource({
id: 'event-warn',
type: 'k8s-event',
status: 'online',
kubernetes: { eventType: 'Warning', reason: 'BackOff', involvedName: 'checkout-api-abc' },
}),
];
it('matches kubernetes.* fields the shared filter no longer carries', () => {
expect(filterKubernetesResources(rows, 'payments', 'all').map((r) => r.id).sort()).toEqual(
['pod-checkout', 'svc-checkout'].sort(),
);
expect(filterKubernetesResources(rows, 'prod-cluster', 'all').map((r) => r.id)).toEqual([
'pod-checkout',
]);
expect(filterKubernetesResources(rows, 'v1.30.4', 'all').map((r) => r.id)).toEqual([
'node-worker',
]);
expect(filterKubernetesResources(rows, 'BackOff', 'all').map((r) => r.id)).toEqual([
'event-warn',
]);
});
it('matches node-role labels via kubernetes.roles', () => {
expect(filterKubernetesResources(rows, 'worker', 'all').map((r) => r.id)).toEqual([
'node-worker',
]);
});
it('combines status and search', () => {
expect(filterKubernetesResources(rows, 'v1.30', 'degraded').map((r) => r.id)).toEqual([
'node-worker',
]);
expect(filterKubernetesResources(rows, 'v1.30', 'online')).toEqual([]);
});
});
});
@@ -247,6 +247,119 @@ export const compareKubernetesNodes = compareByStatus(mapKubernetesNodeStatus);
export const compareKubernetesDeployments = compareByStatus(mapKubernetesDeploymentStatus);
export const compareKubernetesControllers = compareByStatus(mapKubernetesControllerStatus);
export type KubernetesResourceStatusFilter = 'all' | 'online' | 'degraded' | 'offline';
const ONLINE_RESOURCE_STATUSES = new Set<string>(['online', 'running']);
const DEGRADED_RESOURCE_STATUSES = new Set<string>(['degraded', 'paused']);
const OFFLINE_RESOURCE_STATUSES = new Set<string>(['offline', 'stopped']);
const mapResourceStatusToTriad = (
status: string | undefined,
): Exclude<KubernetesResourceStatusFilter, 'all'> | 'unknown' => {
if (!status) return 'unknown';
if (ONLINE_RESOURCE_STATUSES.has(status)) return 'online';
if (DEGRADED_RESOURCE_STATUSES.has(status)) return 'degraded';
if (OFFLINE_RESOURCE_STATUSES.has(status)) return 'offline';
return 'unknown';
};
// Builds the lowercase search haystack a Kubernetes page table consults when
// filtering rows. The shared platformPage helper carries only generic Resource
// fields; kubernetes.* lookups live here so the cross-platform helper does not
// have to know which platforms exist.
export function kubernetesResourceSearchHaystack(resource: Resource): string {
const k = resource.kubernetes;
return [
resource.id,
resource.name,
resource.displayName,
resource.parentName,
resource.platformId,
resource.platformType,
resource.agent?.hostname,
resource.identity?.hostname,
resource.canonicalIdentity?.displayName,
resource.canonicalIdentity?.hostname,
resource.canonicalIdentity?.primaryId,
...(resource.canonicalIdentity?.aliases ?? []),
k?.clusterId,
k?.clusterName,
k?.context,
k?.namespace,
k?.podName,
k?.podPhase,
k?.podReason,
k?.podMessage,
k?.ownerKind,
k?.ownerName,
k?.image,
k?.nodeName,
k?.resourceKind,
k?.serviceName,
k?.serviceType,
k?.clusterIp,
k?.storageClass,
k?.phase,
k?.reason,
k?.message,
k?.involvedKind,
k?.involvedName,
k?.eventType,
k?.volumeName,
k?.version,
k?.kubeletVersion,
k?.containerRuntimeVersion,
k?.osImage,
k?.architecture,
k?.provisioner,
k?.addressType,
k?.secretType,
k?.targetKind,
k?.targetName,
k?.server,
...(k?.externalIps ?? []),
...(k?.hosts ?? []),
...(k?.addresses ?? []),
...(k?.accessModes ?? []),
...(k?.roles ?? []),
...(k?.policyTypes ?? []),
...(k?.metricTypes ?? []),
...(k?.podContainers?.flatMap((container) => [
container.name,
container.image,
container.state,
container.reason,
]) ?? []),
...(resource.tags ?? []),
]
.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
.join(' ')
.toLowerCase();
}
export function filterKubernetesResources(
resources: Resource[],
search: string,
status: KubernetesResourceStatusFilter,
): Resource[] {
const needle = search.trim().toLowerCase();
const result: Resource[] = [];
for (const resource of resources) {
if (status !== 'all') {
const triad = mapResourceStatusToTriad(resource.status);
if (triad !== status) continue;
}
if (!needle) {
result.push(resource);
continue;
}
if (kubernetesResourceSearchHaystack(resource).includes(needle)) {
result.push(resource);
}
}
return result;
}
const KUBERNETES_ROUTE_TAB_ALIASES: Record<string, KubernetesPageTabId> = {
autoscaling: 'workloads',
config: 'configuration',
@@ -66,34 +66,53 @@ describe('filterPlatformResources', () => {
]);
});
it('searches platform-native metadata used by bespoke tables', () => {
it('searches the platform-native metadata that bespoke tables still consume directly', () => {
// Docker / Kubernetes lookups moved to their per-platform helpers
// (filterDockerResources / filterKubernetesResources) so the shared
// helper stays platform-agnostic. The two providers that still consume
// this filter directly — Proxmox Mail Gateway and the vSphere hosts
// table — keep their native-metadata coverage here.
const nativeRows: Resource[] = [
makeResource({
id: 'docker-host',
id: 'pmg-host',
type: 'agent',
status: 'online',
docker: { runtimeVersion: '24.0.7', swarm: { nodeRole: 'manager' } },
pmg: { hostname: 'pmg-primary', version: '8.2.4' },
}),
makeResource({
id: 'k8s-deploy',
type: 'k8s-deployment',
id: 'vsphere-host',
type: 'agent',
status: 'online',
kubernetes: {
clusterName: 'prod-cluster',
namespace: 'payments',
containerRuntimeVersion: 'containerd://1.7',
},
vmware: { clusterName: 'prod-cluster', runtimeHostName: 'esxi-04' },
}),
];
expect(filterPlatformResources(nativeRows, 'manager', 'all').map((r) => r.id)).toEqual([
'docker-host',
expect(filterPlatformResources(nativeRows, 'pmg-primary', 'all').map((r) => r.id)).toEqual([
'pmg-host',
]);
expect(filterPlatformResources(nativeRows, 'payments', 'all').map((r) => r.id)).toEqual([
'k8s-deploy',
expect(filterPlatformResources(nativeRows, 'prod-cluster', 'all').map((r) => r.id)).toEqual([
'vsphere-host',
]);
});
it('no longer matches docker.* or kubernetes.* fields directly', () => {
const dockerOnlyRow = makeResource({
id: 'docker-host',
type: 'agent',
status: 'online',
docker: { runtimeVersion: '24.0.7', swarm: { nodeRole: 'manager' } },
});
const k8sOnlyRow = makeResource({
id: 'k8s-deploy',
type: 'k8s-deployment',
status: 'online',
kubernetes: { clusterName: 'prod-cluster', namespace: 'payments' },
});
expect(filterPlatformResources([dockerOnlyRow], 'manager', 'all')).toEqual([]);
expect(filterPlatformResources([k8sOnlyRow], 'payments', 'all')).toEqual([]);
});
it('combines search and status filters', () => {
const filtered = filterPlatformResources(resources, 'host', 'degraded');
expect(filtered.map((r) => r.id).sort()).toEqual(['host-charlie', 'host-foxtrot'].sort());
@@ -182,9 +182,13 @@ const mapResourceStatusToTriad = (
return 'unknown';
};
const platformSearchNumber = (value: number | undefined): string | undefined =>
typeof value === 'number' ? String(value) : undefined;
// Cross-platform fallback haystack used by tables that do not have a
// domain-specific search helper. Docker and Kubernetes provide their own
// platform-page filters (filterDockerResources / filterKubernetesResources)
// that already cover docker.* and kubernetes.* fields, so this helper stays
// platform-agnostic and only knows about the generic Resource surface plus
// the small number of provider blocks that still consume it directly
// (Proxmox Mail Gateway, vSphere hosts table).
const matchesPlatformSearch = (resource: Resource, search: string): boolean => {
if (!search) return true;
const needle = search.trim().toLowerCase();
@@ -202,51 +206,6 @@ const matchesPlatformSearch = (resource: Resource, search: string): boolean => {
resource.canonicalIdentity?.hostname,
resource.canonicalIdentity?.primaryId,
...(resource.canonicalIdentity?.aliases ?? []),
resource.docker?.hostname,
resource.docker?.runtime,
resource.docker?.runtimeVersion,
resource.docker?.dockerVersion,
resource.docker?.containerId,
resource.docker?.image,
resource.docker?.imageId,
resource.docker?.containerState,
resource.docker?.health,
platformSearchNumber(resource.docker?.restartCount),
platformSearchNumber(resource.docker?.exitCode),
resource.docker?.updateStatus?.error,
resource.docker?.updateStatus?.currentDigest,
resource.docker?.updateStatus?.latestDigest,
resource.docker?.volumeName,
resource.docker?.networkId,
resource.docker?.driver,
resource.docker?.mountpoint,
resource.docker?.serviceName,
resource.docker?.taskId,
resource.docker?.mode,
resource.docker?.swarm?.clusterName,
resource.docker?.swarm?.nodeRole,
resource.kubernetes?.clusterName,
resource.kubernetes?.clusterId,
resource.kubernetes?.context,
resource.kubernetes?.namespace,
resource.kubernetes?.podName,
resource.kubernetes?.podPhase,
resource.kubernetes?.podReason,
resource.kubernetes?.ownerKind,
resource.kubernetes?.ownerName,
resource.kubernetes?.image,
resource.kubernetes?.nodeName,
resource.kubernetes?.resourceKind,
resource.kubernetes?.serviceType,
resource.kubernetes?.clusterIp,
resource.kubernetes?.storageClass,
resource.kubernetes?.phase,
resource.kubernetes?.reason,
resource.kubernetes?.involvedName,
resource.kubernetes?.volumeName,
resource.kubernetes?.version,
resource.kubernetes?.kubeletVersion,
resource.kubernetes?.containerRuntimeVersion,
resource.pmg?.hostname,
resource.pmg?.version,
resource.vmware?.connectionName,
@@ -258,36 +217,6 @@ const matchesPlatformSearch = (resource: Resource, search: string): boolean => {
resource.vmware?.networkType,
resource.vmware?.networkHostNames?.join(' '),
resource.vmware?.networkVmNames?.join(' '),
...(resource.docker?.ports?.flatMap((port) => [
port.ip,
port.protocol,
platformSearchNumber(port.privatePort),
platformSearchNumber(port.publicPort),
]) ?? []),
...(resource.docker?.networks?.flatMap((network) => [
network.name,
network.ipv4,
network.ipv6,
]) ?? []),
...(resource.docker?.mounts?.flatMap((mount) => [
mount.type,
mount.source,
mount.destination,
mount.mode,
mount.rw === false ? 'read-only' : mount.rw === true ? 'read-write' : undefined,
]) ?? []),
...(resource.docker?.repoTags ?? []),
...(resource.docker?.repoDigests ?? []),
...(resource.kubernetes?.externalIps ?? []),
...(resource.kubernetes?.hosts ?? []),
...(resource.kubernetes?.addresses ?? []),
...(resource.kubernetes?.accessModes ?? []),
...(resource.kubernetes?.podContainers?.flatMap((container) => [
container.name,
container.image,
container.state,
container.reason,
]) ?? []),
...(resource.tags ?? []),
]
.filter((value): value is string => typeof value === 'string')