mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-17 11:52:40 +00:00
Restore user column sorting on Kubernetes platform tables
v5 parity follow-up to 20cccc1a9 (v5 KubernetesClusters.tsx persisted
k8s-sort-key/k8s-sort-dir sorting; v6 shipped fixed ordering). Adopts the
shared platform-table sort fabric (createPlatformTableSortState +
PlatformSortableTableHead) across the clusters, nodes, pods, deployments,
controllers, services, storage, config, autoscaling, and networking tables:
per-table storage keys, descendingFirst on metric/count columns (pods Ready
stays ascending-first so the least-ready pods surface), the status-first
compare preserved as the base order until the user sorts, and canonical
kind-based alignment via the sortable head's kind prop. Composite summary
columns (ports, selectors, labels, access/policy, bounds, detail) stay
non-sortable. TrueNAS/vSphere adoption is landing separately.
This commit is contained in:
parent
20cccc1a91
commit
f66c4b46b3
12 changed files with 1113 additions and 255 deletions
|
|
@ -1,19 +1,21 @@
|
|||
import { For, Show, type Component, type JSX } from 'solid-js';
|
||||
import { For, Show, createMemo, type Component, type JSX } from 'solid-js';
|
||||
import { StatusDot } from '@/components/shared/StatusDot';
|
||||
import { TableCell, TableHead, TableRow } from '@/components/shared/Table';
|
||||
import { TableCell, TableRow } from '@/components/shared/Table';
|
||||
import { getSimpleStatusIndicator } from '@/utils/status';
|
||||
import { asTrimmedString } from '@/utils/stringUtils';
|
||||
import {
|
||||
PLATFORM_HEALTH_FILTER_OPTIONS,
|
||||
PlatformSortableTableHead,
|
||||
PlatformTableEmptyState,
|
||||
PlatformTableNumberValue,
|
||||
PlatformTableToolbar,
|
||||
createPlatformTableFilterState,
|
||||
createPlatformTableSortState,
|
||||
formatPlatformTableTextValue,
|
||||
getPlatformTableCellClassForKind,
|
||||
getPlatformTableHeadClassForKind,
|
||||
summarizePlatformTableValues,
|
||||
PlatformTableShell,
|
||||
type PlatformTableSortValue,
|
||||
} from '@/features/platformPage/sharedPlatformPage';
|
||||
import {
|
||||
PlatformResourceDetailToggleButton,
|
||||
|
|
@ -55,6 +57,41 @@ const bounds = (resource: Resource): string => {
|
|||
return `${min ?? '—'}-${max ?? '—'}`;
|
||||
};
|
||||
|
||||
// Bounds ("1-10"), Metrics, and Labels are composite summaries with no single
|
||||
// scalar to order on, so they stay non-sortable.
|
||||
const KUBERNETES_AUTOSCALING_SORT_KEYS = [
|
||||
'autoscaler',
|
||||
'scope',
|
||||
'target',
|
||||
'current',
|
||||
'desired',
|
||||
] as const;
|
||||
|
||||
type KubernetesAutoscalingSortKey = (typeof KUBERNETES_AUTOSCALING_SORT_KEYS)[number];
|
||||
|
||||
const getKubernetesAutoscalingSortValue = (
|
||||
resource: Resource,
|
||||
key: KubernetesAutoscalingSortKey,
|
||||
): PlatformTableSortValue => {
|
||||
switch (key) {
|
||||
case 'autoscaler':
|
||||
return autoscalerName(resource);
|
||||
case 'scope':
|
||||
return kubernetesScopeLabel(resource);
|
||||
case 'target': {
|
||||
const target = targetRef(resource);
|
||||
return target === '—' ? null : target;
|
||||
}
|
||||
case 'current':
|
||||
return resource.kubernetes?.currentReplicas ?? null;
|
||||
case 'desired':
|
||||
return resource.kubernetes?.desiredReplicas ?? null;
|
||||
default:
|
||||
key satisfies never;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const KubernetesAutoscalingTable: Component<{
|
||||
resources: Resource[];
|
||||
emptyIcon: JSX.Element;
|
||||
|
|
@ -74,6 +111,14 @@ export const KubernetesAutoscalingTable: Component<{
|
|||
});
|
||||
const drawer = createPlatformResourceDetailState({ idPrefix: 'kubernetes-autoscaling-drawer' });
|
||||
const resolveResourceLabel = createPlatformResourceLabelResolver(() => props.resources);
|
||||
const sort = createPlatformTableSortState({
|
||||
storageKey: 'kubernetesAutoscaling',
|
||||
sortKeys: KUBERNETES_AUTOSCALING_SORT_KEYS,
|
||||
descendingFirst: ['current', 'desired'],
|
||||
});
|
||||
const sortedRows = createMemo(() =>
|
||||
sort.sortRows(tableState.filtered(), getKubernetesAutoscalingSortValue),
|
||||
);
|
||||
|
||||
return (
|
||||
<Show
|
||||
|
|
@ -116,39 +161,60 @@ export const KubernetesAutoscalingTable: Component<{
|
|||
tableClass="min-w-full table-fixed text-xs md:min-w-[1080px]"
|
||||
header={
|
||||
<>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('name')} md:w-[20%]`}>
|
||||
<PlatformSortableTableHead
|
||||
kind="name"
|
||||
sort={sort}
|
||||
sortKey="autoscaler"
|
||||
class="md:w-[20%]"
|
||||
>
|
||||
Autoscaler
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[16%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="scope"
|
||||
class="hidden md:table-cell md:w-[16%]"
|
||||
>
|
||||
Scope
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[18%]`}>
|
||||
Scale target
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[10%]`}>
|
||||
Bounds
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[9%]`}>
|
||||
Current
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[9%]`}>
|
||||
Desired
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[11%]`}>
|
||||
Metrics
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[7%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="target"
|
||||
class="md:w-[18%]"
|
||||
>
|
||||
Scale target
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead kind="text" sort={sort} class="md:w-[10%]">
|
||||
Bounds
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="numeric-value"
|
||||
sort={sort}
|
||||
sortKey="current"
|
||||
class="md:w-[9%]"
|
||||
>
|
||||
Current
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="numeric-value"
|
||||
sort={sort}
|
||||
sortKey="desired"
|
||||
class="md:w-[9%]"
|
||||
>
|
||||
Desired
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead kind="text" sort={sort} class="md:w-[11%]">
|
||||
Metrics
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead kind="text" sort={sort} class="hidden md:table-cell md:w-[7%]">
|
||||
Labels
|
||||
</TableHead>
|
||||
</PlatformSortableTableHead>
|
||||
</>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
<For each={tableState.filtered()}>
|
||||
<For each={sortedRows()}>
|
||||
{(resource) => {
|
||||
const indicator = () => getSimpleStatusIndicator(resource.status);
|
||||
const name = () => autoscalerName(resource);
|
||||
|
|
|
|||
|
|
@ -2,22 +2,24 @@ import { For, Show, createMemo, type Component, type JSX } from 'solid-js';
|
|||
import { StatusDot } from '@/components/shared/StatusDot';
|
||||
import { ResponsiveMetricCell } from '@/components/shared/responsive';
|
||||
import { StackedMemoryBar } from '@/components/Workloads/StackedMemoryBar';
|
||||
import { TableCell, TableHead, TableRow } from '@/components/shared/Table';
|
||||
import { TableCell, TableRow } from '@/components/shared/Table';
|
||||
import { buildMetricKeyForUnifiedResource } from '@/utils/metricsKeys';
|
||||
import { getSimpleStatusIndicator } from '@/utils/status';
|
||||
import { asTrimmedString } from '@/utils/stringUtils';
|
||||
import {
|
||||
PLATFORM_HEALTH_FILTER_OPTIONS,
|
||||
PlatformSortableTableHead,
|
||||
PlatformTableCountRatioValue,
|
||||
PlatformTableMetricFallback,
|
||||
PlatformTableToolbar,
|
||||
PlatformTableEmptyState,
|
||||
createPlatformTableFilterState,
|
||||
createPlatformTableSortState,
|
||||
formatPlatformTableTextValue,
|
||||
getPlatformTableFiniteMetric,
|
||||
getPlatformTableCellClassForKind,
|
||||
getPlatformTableHeadClassForKind,
|
||||
PlatformTableShell,
|
||||
type PlatformTableSortValue,
|
||||
} from '@/features/platformPage/sharedPlatformPage';
|
||||
import {
|
||||
PlatformResourceDetailToggleButton,
|
||||
|
|
@ -44,6 +46,19 @@ import {
|
|||
// CPU/Memory utilisation. It reuses the same shared primitives every
|
||||
// other platform-page table uses.
|
||||
|
||||
const KUBERNETES_CLUSTER_SORT_KEYS = [
|
||||
'cluster',
|
||||
'context',
|
||||
'version',
|
||||
'nodes',
|
||||
'pods',
|
||||
'deployments',
|
||||
'cpu',
|
||||
'memory',
|
||||
] as const;
|
||||
|
||||
type KubernetesClusterSortKey = (typeof KUBERNETES_CLUSTER_SORT_KEYS)[number];
|
||||
|
||||
export const KubernetesClustersTable: Component<{
|
||||
clusters: Resource[];
|
||||
// All Kubernetes-tagged resources from the same query, so the table can
|
||||
|
|
@ -68,6 +83,50 @@ export const KubernetesClustersTable: Component<{
|
|||
buildKubernetesClusterChildCounts(props.scope, props.clusters),
|
||||
);
|
||||
|
||||
const sort = createPlatformTableSortState({
|
||||
storageKey: 'kubernetesClusters',
|
||||
sortKeys: KUBERNETES_CLUSTER_SORT_KEYS,
|
||||
descendingFirst: ['nodes', 'pods', 'deployments', 'cpu', 'memory'],
|
||||
});
|
||||
// Closure rather than a module-level accessor: the count columns derive
|
||||
// from the countsByCluster memo over props.scope.
|
||||
const getClusterSortValue = (
|
||||
cluster: Resource,
|
||||
key: KubernetesClusterSortKey,
|
||||
): PlatformTableSortValue => {
|
||||
switch (key) {
|
||||
case 'cluster':
|
||||
return (
|
||||
asTrimmedString(cluster.kubernetes?.clusterName) ||
|
||||
asTrimmedString(cluster.name) ||
|
||||
cluster.id
|
||||
);
|
||||
case 'context':
|
||||
return asTrimmedString(cluster.kubernetes?.context) || null;
|
||||
case 'version':
|
||||
return asTrimmedString(cluster.kubernetes?.version) || null;
|
||||
case 'nodes':
|
||||
return countsByCluster().get(cluster.id)?.nodes.total ?? null;
|
||||
case 'pods':
|
||||
return countsByCluster().get(cluster.id)?.pods.total ?? null;
|
||||
case 'deployments':
|
||||
return countsByCluster().get(cluster.id)?.deployments.total ?? null;
|
||||
case 'cpu':
|
||||
return getPlatformTableFiniteMetric(cluster.cpu?.current) ?? null;
|
||||
case 'memory': {
|
||||
const total = getPlatformTableFiniteMetric(cluster.memory?.total) ?? 0;
|
||||
if (total > 0) {
|
||||
return ((getPlatformTableFiniteMetric(cluster.memory?.used) ?? 0) / total) * 100;
|
||||
}
|
||||
return getPlatformTableFiniteMetric(cluster.memory?.current) ?? null;
|
||||
}
|
||||
default:
|
||||
key satisfies never;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
const sortedRows = createMemo(() => sort.sortRows(tableState.filtered(), getClusterSortValue));
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={props.clusters.length > 0}
|
||||
|
|
@ -109,43 +168,75 @@ export const KubernetesClustersTable: Component<{
|
|||
tableClass="min-w-full table-fixed text-xs md:min-w-[1040px]"
|
||||
header={
|
||||
<>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('name')} md:w-[17%]`}>
|
||||
<PlatformSortableTableHead
|
||||
kind="name"
|
||||
sort={sort}
|
||||
sortKey="cluster"
|
||||
class="md:w-[17%]"
|
||||
>
|
||||
Cluster
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[15%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="context"
|
||||
class="hidden md:table-cell md:w-[15%]"
|
||||
>
|
||||
Context
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[10%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="version"
|
||||
class="hidden md:table-cell md:w-[10%]"
|
||||
>
|
||||
Version
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[8%]`}>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="numeric-value"
|
||||
sort={sort}
|
||||
sortKey="nodes"
|
||||
class="md:w-[8%]"
|
||||
>
|
||||
Nodes
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[8%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="numeric-value"
|
||||
sort={sort}
|
||||
sortKey="pods"
|
||||
class="hidden md:table-cell md:w-[8%]"
|
||||
>
|
||||
Pods
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[12%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="numeric-value"
|
||||
sort={sort}
|
||||
sortKey="deployments"
|
||||
class="hidden md:table-cell md:w-[12%]"
|
||||
>
|
||||
Deployments
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('metric-bar')} md:w-[15%]`}>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="metric-bar"
|
||||
sort={sort}
|
||||
sortKey="cpu"
|
||||
class="md:w-[15%]"
|
||||
>
|
||||
CPU
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('metric-bar')} md:w-[15%]`}>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="metric-bar"
|
||||
sort={sort}
|
||||
sortKey="memory"
|
||||
class="md:w-[15%]"
|
||||
>
|
||||
Memory
|
||||
</TableHead>
|
||||
</PlatformSortableTableHead>
|
||||
</>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
<For each={tableState.filtered()}>
|
||||
<For each={sortedRows()}>
|
||||
{(cluster) => {
|
||||
const name = () =>
|
||||
asTrimmedString(cluster.kubernetes?.clusterName) ||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,20 @@
|
|||
import { For, Show, type Component, type JSX } from 'solid-js';
|
||||
import { For, Show, createMemo, type Component, type JSX } from 'solid-js';
|
||||
import { StatusDot } from '@/components/shared/StatusDot';
|
||||
import { TableCell, TableHead, TableRow } from '@/components/shared/Table';
|
||||
import { TableCell, TableRow } from '@/components/shared/Table';
|
||||
import { getSimpleStatusIndicator } from '@/utils/status';
|
||||
import { asTrimmedString } from '@/utils/stringUtils';
|
||||
import {
|
||||
PLATFORM_HEALTH_FILTER_OPTIONS,
|
||||
PlatformSortableTableHead,
|
||||
PlatformTableEmptyState,
|
||||
PlatformTableToolbar,
|
||||
createPlatformTableFilterState,
|
||||
createPlatformTableSortState,
|
||||
formatPlatformTableTextValue,
|
||||
getPlatformTableCellClassForKind,
|
||||
getPlatformTableHeadClassForKind,
|
||||
summarizePlatformTableValues,
|
||||
PlatformTableShell,
|
||||
type PlatformTableSortValue,
|
||||
} from '@/features/platformPage/sharedPlatformPage';
|
||||
import {
|
||||
PlatformResourceDetailToggleButton,
|
||||
|
|
@ -155,6 +157,34 @@ const serviceAccountRefs = (resource: Resource): { label: string; title: string
|
|||
const labelSummary = (resource: Resource): { label: string; title: string } =>
|
||||
summarizePlatformTableValues(resource.tags);
|
||||
|
||||
// Data shape, Token refs, and Labels are composite summaries with no single
|
||||
// scalar to order on, so they stay non-sortable. Lifecycle / trust is a
|
||||
// single categorical string per row, which makes sorting group like rows.
|
||||
const KUBERNETES_CONFIG_SORT_KEYS = ['resource', 'kind', 'scope', 'lifecycle'] as const;
|
||||
|
||||
type KubernetesConfigSortKey = (typeof KUBERNETES_CONFIG_SORT_KEYS)[number];
|
||||
|
||||
const getKubernetesConfigSortValue = (
|
||||
resource: Resource,
|
||||
key: KubernetesConfigSortKey,
|
||||
): PlatformTableSortValue => {
|
||||
switch (key) {
|
||||
case 'resource':
|
||||
return configName(resource);
|
||||
case 'kind':
|
||||
return configKind(resource);
|
||||
case 'scope':
|
||||
return kubernetesScopeLabel(resource);
|
||||
case 'lifecycle': {
|
||||
const state = lifecycleOrTrust(resource);
|
||||
return state === '—' ? null : state;
|
||||
}
|
||||
default:
|
||||
key satisfies never;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const KubernetesConfigTable: Component<{
|
||||
resources: Resource[];
|
||||
emptyIcon: JSX.Element;
|
||||
|
|
@ -174,6 +204,13 @@ export const KubernetesConfigTable: Component<{
|
|||
});
|
||||
const drawer = createPlatformResourceDetailState({ idPrefix: 'kubernetes-config-drawer' });
|
||||
const resolveResourceLabel = createPlatformResourceLabelResolver(() => props.resources);
|
||||
const sort = createPlatformTableSortState({
|
||||
storageKey: 'kubernetesConfig',
|
||||
sortKeys: KUBERNETES_CONFIG_SORT_KEYS,
|
||||
});
|
||||
const sortedRows = createMemo(() =>
|
||||
sort.sortRows(tableState.filtered(), getKubernetesConfigSortValue),
|
||||
);
|
||||
|
||||
return (
|
||||
<Show
|
||||
|
|
@ -216,38 +253,47 @@ export const KubernetesConfigTable: Component<{
|
|||
tableClass="min-w-full table-fixed text-xs md:min-w-[1160px]"
|
||||
header={
|
||||
<>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('name')} md:w-[18%]`}>
|
||||
<PlatformSortableTableHead
|
||||
kind="name"
|
||||
sort={sort}
|
||||
sortKey="resource"
|
||||
class="md:w-[18%]"
|
||||
>
|
||||
Resource
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[13%]`}>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead kind="text" sort={sort} sortKey="kind" class="md:w-[13%]">
|
||||
Kind
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[16%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="scope"
|
||||
class="hidden md:table-cell md:w-[16%]"
|
||||
>
|
||||
Scope
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[16%]`}>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="lifecycle"
|
||||
class="md:w-[16%]"
|
||||
>
|
||||
Lifecycle / trust
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[14%]`}>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead kind="text" sort={sort} class="md:w-[14%]">
|
||||
Data shape
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[13%]`}
|
||||
>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead kind="text" sort={sort} class="hidden md:table-cell md:w-[13%]">
|
||||
Token refs
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[10%]`}
|
||||
>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead kind="text" sort={sort} class="hidden md:table-cell md:w-[10%]">
|
||||
Labels
|
||||
</TableHead>
|
||||
</PlatformSortableTableHead>
|
||||
</>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
<For each={tableState.filtered()}>
|
||||
<For each={sortedRows()}>
|
||||
{(resource) => {
|
||||
const indicator = () => getSimpleStatusIndicator(resource.status);
|
||||
const name = () => configName(resource);
|
||||
|
|
|
|||
|
|
@ -1,18 +1,20 @@
|
|||
import { For, Show, createMemo, type Component, type JSX } from 'solid-js';
|
||||
import { StatusDot } from '@/components/shared/StatusDot';
|
||||
import { TableCell, TableHead, TableRow } from '@/components/shared/Table';
|
||||
import { TableCell, TableRow } from '@/components/shared/Table';
|
||||
import { getResourceTypeLabel } from '@/utils/resourceTypePresentation';
|
||||
import { asTrimmedString } from '@/utils/stringUtils';
|
||||
import {
|
||||
PLATFORM_HEALTH_FILTER_OPTIONS,
|
||||
PlatformSortableTableHead,
|
||||
PlatformTableEmptyState,
|
||||
PlatformTableNumberValue,
|
||||
PlatformTableToolbar,
|
||||
createPlatformTableFilterState,
|
||||
createPlatformTableSortState,
|
||||
formatPlatformTableTextValue,
|
||||
getPlatformTableCellClassForKind,
|
||||
getPlatformTableHeadClassForKind,
|
||||
PlatformTableShell,
|
||||
type PlatformTableSortValue,
|
||||
} from '@/features/platformPage/sharedPlatformPage';
|
||||
import {
|
||||
PlatformResourceDetailToggleButton,
|
||||
|
|
@ -148,6 +150,53 @@ const apiDetail = (resource: Resource): string => {
|
|||
}
|
||||
};
|
||||
|
||||
// The Detail column is a per-kind grab-bag (service names, timestamps,
|
||||
// generation counters) with no single scalar meaning across rows, so it
|
||||
// stays non-sortable.
|
||||
const KUBERNETES_CONTROLLER_SORT_KEYS = [
|
||||
'controller',
|
||||
'kind',
|
||||
'scope',
|
||||
'target',
|
||||
'current',
|
||||
'ready',
|
||||
'available',
|
||||
'exceptions',
|
||||
] as const;
|
||||
|
||||
type KubernetesControllerSortKey = (typeof KUBERNETES_CONTROLLER_SORT_KEYS)[number];
|
||||
|
||||
const getKubernetesControllerSortValue = (
|
||||
resource: Resource,
|
||||
key: KubernetesControllerSortKey,
|
||||
): PlatformTableSortValue => {
|
||||
switch (key) {
|
||||
case 'controller':
|
||||
return controllerName(resource);
|
||||
case 'kind':
|
||||
return controllerKind(resource);
|
||||
case 'scope':
|
||||
return kubernetesScopeLabel(resource);
|
||||
case 'target': {
|
||||
const target = targetValue(resource);
|
||||
return target === '—' ? null : target;
|
||||
}
|
||||
case 'current':
|
||||
return currentValue(resource) ?? null;
|
||||
case 'ready':
|
||||
return readyOrDoneValue(resource) ?? null;
|
||||
case 'available':
|
||||
return availableValue(resource) ?? null;
|
||||
case 'exceptions': {
|
||||
const exceptions = exceptionSummary(resource);
|
||||
return exceptions === '—' ? null : exceptions;
|
||||
}
|
||||
default:
|
||||
key satisfies never;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const KubernetesControllersTable: Component<{
|
||||
resources: Resource[];
|
||||
emptyIcon: JSX.Element;
|
||||
|
|
@ -165,8 +214,19 @@ export const KubernetesControllersTable: Component<{
|
|||
externalSearch: props.externalSearch,
|
||||
externalStatus: props.externalStatus,
|
||||
});
|
||||
// User-controlled sorting layered over the attention-first default: rows
|
||||
// are pre-sorted by the status compare, so a user sort keeps that order
|
||||
// for ties and the table falls straight back to it when the sort clears.
|
||||
const sort = createPlatformTableSortState({
|
||||
storageKey: 'kubernetesControllers',
|
||||
sortKeys: KUBERNETES_CONTROLLER_SORT_KEYS,
|
||||
descendingFirst: ['current', 'ready', 'available'],
|
||||
});
|
||||
const sortedRows = createMemo(() =>
|
||||
[...tableState.filtered()].sort(compareKubernetesControllers),
|
||||
sort.sortRows(
|
||||
[...tableState.filtered()].sort(compareKubernetesControllers),
|
||||
getKubernetesControllerSortValue,
|
||||
),
|
||||
);
|
||||
const drawer = createPlatformResourceDetailState({ idPrefix: 'kubernetes-controller-drawer' });
|
||||
const resolveResourceLabel = createPlatformResourceLabelResolver(() => props.resources);
|
||||
|
|
@ -212,41 +272,68 @@ export const KubernetesControllersTable: Component<{
|
|||
tableClass="min-w-full table-fixed text-xs md:min-w-[1120px]"
|
||||
header={
|
||||
<>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('name')} md:w-[18%]`}>
|
||||
<PlatformSortableTableHead
|
||||
kind="name"
|
||||
sort={sort}
|
||||
sortKey="controller"
|
||||
class="md:w-[18%]"
|
||||
>
|
||||
Controller
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[9%]`}>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead kind="text" sort={sort} sortKey="kind" class="md:w-[9%]">
|
||||
Kind
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[14%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="scope"
|
||||
class="hidden md:table-cell md:w-[14%]"
|
||||
>
|
||||
Scope
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[12%]`}>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="target"
|
||||
class="md:w-[12%]"
|
||||
>
|
||||
Target
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[8%]`}>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="numeric-value"
|
||||
sort={sort}
|
||||
sortKey="current"
|
||||
class="md:w-[8%]"
|
||||
>
|
||||
Current
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[10%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="numeric-value"
|
||||
sort={sort}
|
||||
sortKey="ready"
|
||||
class="md:w-[10%]"
|
||||
>
|
||||
Ready/Done
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[9%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="numeric-value"
|
||||
sort={sort}
|
||||
sortKey="available"
|
||||
class="hidden md:table-cell md:w-[9%]"
|
||||
>
|
||||
Available
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[11%]`}>
|
||||
Exceptions
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[9%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="exceptions"
|
||||
class="md:w-[11%]"
|
||||
>
|
||||
Exceptions
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead kind="text" sort={sort} class="hidden md:table-cell md:w-[9%]">
|
||||
Detail
|
||||
</TableHead>
|
||||
</PlatformSortableTableHead>
|
||||
</>
|
||||
}
|
||||
body={
|
||||
|
|
|
|||
|
|
@ -1,18 +1,21 @@
|
|||
import { For, Show, createMemo, type Component, type JSX } from 'solid-js';
|
||||
import { StatusDot } from '@/components/shared/StatusDot';
|
||||
import { TableCell, TableHead, TableRow } from '@/components/shared/Table';
|
||||
import { TableCell, TableRow } from '@/components/shared/Table';
|
||||
import { asTrimmedString } from '@/utils/stringUtils';
|
||||
import {
|
||||
PLATFORM_HEALTH_FILTER_OPTIONS,
|
||||
PlatformSortableTableHead,
|
||||
PlatformTableNumberValue,
|
||||
PlatformTableRelativeTimeValue,
|
||||
PlatformTableToolbar,
|
||||
PlatformTableEmptyState,
|
||||
createPlatformTableFilterState,
|
||||
createPlatformTableSortState,
|
||||
formatPlatformTableTextValue,
|
||||
getPlatformTableCellClassForKind,
|
||||
getPlatformTableHeadClassForKind,
|
||||
getPlatformTableDateTimeSortValue,
|
||||
PlatformTableShell,
|
||||
type PlatformTableSortValue,
|
||||
} from '@/features/platformPage/sharedPlatformPage';
|
||||
import {
|
||||
PlatformResourceDetailToggleButton,
|
||||
|
|
@ -40,6 +43,48 @@ import {
|
|||
// observedGeneration is deliberately not a column: without the spec
|
||||
// generation beside it the raw number is unactionable.
|
||||
|
||||
const KUBERNETES_DEPLOYMENT_SORT_KEYS = [
|
||||
'deployment',
|
||||
'namespace',
|
||||
'cluster',
|
||||
'desired',
|
||||
'updated',
|
||||
'ready',
|
||||
'available',
|
||||
'age',
|
||||
] as const;
|
||||
|
||||
type KubernetesDeploymentSortKey = (typeof KUBERNETES_DEPLOYMENT_SORT_KEYS)[number];
|
||||
|
||||
// Replica counts sort on the same `?? 0` fallback the cells render, so a
|
||||
// deployment displaying 0 orders as 0 instead of sinking as missing.
|
||||
const getKubernetesDeploymentSortValue = (
|
||||
deployment: Resource,
|
||||
key: KubernetesDeploymentSortKey,
|
||||
): PlatformTableSortValue => {
|
||||
switch (key) {
|
||||
case 'deployment':
|
||||
return asTrimmedString(deployment.name) || deployment.id;
|
||||
case 'namespace':
|
||||
return asTrimmedString(deployment.kubernetes?.namespace) || null;
|
||||
case 'cluster':
|
||||
return kubernetesClusterLabel(deployment) || null;
|
||||
case 'desired':
|
||||
return deployment.kubernetes?.desiredReplicas ?? 0;
|
||||
case 'updated':
|
||||
return deployment.kubernetes?.updatedReplicas ?? 0;
|
||||
case 'ready':
|
||||
return deployment.kubernetes?.readyReplicas ?? 0;
|
||||
case 'available':
|
||||
return deployment.kubernetes?.availableReplicas ?? 0;
|
||||
case 'age':
|
||||
return getPlatformTableDateTimeSortValue(deployment.kubernetes?.createdAt);
|
||||
default:
|
||||
key satisfies never;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const KubernetesDeploymentsTable: Component<{
|
||||
resources: Resource[];
|
||||
emptyIcon: JSX.Element;
|
||||
|
|
@ -57,8 +102,19 @@ export const KubernetesDeploymentsTable: Component<{
|
|||
externalSearch: props.externalSearch,
|
||||
externalStatus: props.externalStatus,
|
||||
});
|
||||
// User-controlled sorting layered over the attention-first default: rows
|
||||
// are pre-sorted by the status compare, so a user sort keeps that order
|
||||
// for ties and the table falls straight back to it when the sort clears.
|
||||
const sort = createPlatformTableSortState({
|
||||
storageKey: 'kubernetesDeployments',
|
||||
sortKeys: KUBERNETES_DEPLOYMENT_SORT_KEYS,
|
||||
descendingFirst: ['desired', 'updated', 'ready', 'available', 'age'],
|
||||
});
|
||||
const sortedRows = createMemo(() =>
|
||||
[...tableState.filtered()].sort(compareKubernetesDeployments),
|
||||
sort.sortRows(
|
||||
[...tableState.filtered()].sort(compareKubernetesDeployments),
|
||||
getKubernetesDeploymentSortValue,
|
||||
),
|
||||
);
|
||||
const drawer = createPlatformResourceDetailState({ idPrefix: 'kubernetes-deployment-drawer' });
|
||||
const resolveResourceLabel = createPlatformResourceLabelResolver(() => props.resources);
|
||||
|
|
@ -111,38 +167,70 @@ export const KubernetesDeploymentsTable: Component<{
|
|||
Available) trim to what their headers plus 1-2 digit
|
||||
values need. Mobile widths are unchanged.
|
||||
*/}
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('name')} md:w-[25%]`}>
|
||||
<PlatformSortableTableHead
|
||||
kind="name"
|
||||
sort={sort}
|
||||
sortKey="deployment"
|
||||
class="md:w-[25%]"
|
||||
>
|
||||
Deployment
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[20%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="namespace"
|
||||
class="hidden md:table-cell md:w-[20%]"
|
||||
>
|
||||
Namespace
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[17%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="cluster"
|
||||
class="hidden md:table-cell md:w-[17%]"
|
||||
>
|
||||
Cluster
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[8%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="numeric-value"
|
||||
sort={sort}
|
||||
sortKey="desired"
|
||||
class="hidden md:table-cell md:w-[8%]"
|
||||
>
|
||||
Desired
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[8%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="numeric-value"
|
||||
sort={sort}
|
||||
sortKey="updated"
|
||||
class="hidden md:table-cell md:w-[8%]"
|
||||
>
|
||||
Updated
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[7%]`}>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="numeric-value"
|
||||
sort={sort}
|
||||
sortKey="ready"
|
||||
class="md:w-[7%]"
|
||||
>
|
||||
Ready
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[9%]`}>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="numeric-value"
|
||||
sort={sort}
|
||||
sortKey="available"
|
||||
class="md:w-[9%]"
|
||||
>
|
||||
Available
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[6%]`}>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="numeric-value"
|
||||
sort={sort}
|
||||
sortKey="age"
|
||||
class="md:w-[6%]"
|
||||
>
|
||||
Age
|
||||
</TableHead>
|
||||
</PlatformSortableTableHead>
|
||||
</>
|
||||
}
|
||||
body={
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
import { For, Show, type Component, type JSX } from 'solid-js';
|
||||
import { For, Show, createMemo, type Component, type JSX } from 'solid-js';
|
||||
import { StatusDot } from '@/components/shared/StatusDot';
|
||||
import { TableCell, TableHead, TableRow } from '@/components/shared/Table';
|
||||
import { TableCell, TableRow } from '@/components/shared/Table';
|
||||
import { getSimpleStatusIndicator } from '@/utils/status';
|
||||
import { asTrimmedString } from '@/utils/stringUtils';
|
||||
import {
|
||||
PLATFORM_HEALTH_FILTER_OPTIONS,
|
||||
PlatformSortableTableHead,
|
||||
PlatformTableEmptyState,
|
||||
PlatformTableToolbar,
|
||||
createPlatformTableFilterState,
|
||||
createPlatformTableSortState,
|
||||
formatPlatformTableCountRatioValue,
|
||||
formatPlatformTableTextValue,
|
||||
getPlatformTableCellClassForKind,
|
||||
getPlatformTableHeadClassForKind,
|
||||
summarizePlatformTableValues,
|
||||
PlatformTableShell,
|
||||
type PlatformTableSortValue,
|
||||
} from '@/features/platformPage/sharedPlatformPage';
|
||||
import {
|
||||
PlatformResourceDetailToggleButton,
|
||||
|
|
@ -100,6 +102,33 @@ const targetSummary = (resource: Resource): { label: string; title: string } =>
|
|||
};
|
||||
};
|
||||
|
||||
// Address / hosts, Ports, and Targets are composite summaries with no single
|
||||
// scalar to order on, so they stay non-sortable.
|
||||
const KUBERNETES_NETWORKING_SORT_KEYS = ['resource', 'kind', 'scope', 'typeClass'] as const;
|
||||
|
||||
type KubernetesNetworkingSortKey = (typeof KUBERNETES_NETWORKING_SORT_KEYS)[number];
|
||||
|
||||
const getKubernetesNetworkingSortValue = (
|
||||
resource: Resource,
|
||||
key: KubernetesNetworkingSortKey,
|
||||
): PlatformTableSortValue => {
|
||||
switch (key) {
|
||||
case 'resource':
|
||||
return resourceName(resource);
|
||||
case 'kind':
|
||||
return networkKind(resource);
|
||||
case 'scope':
|
||||
return kubernetesScopeLabel(resource);
|
||||
case 'typeClass': {
|
||||
const typeClass = typeOrClass(resource);
|
||||
return typeClass === '—' ? null : typeClass;
|
||||
}
|
||||
default:
|
||||
key satisfies never;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const KubernetesNetworkingTable: Component<{
|
||||
resources: Resource[];
|
||||
emptyIcon: JSX.Element;
|
||||
|
|
@ -119,6 +148,13 @@ export const KubernetesNetworkingTable: Component<{
|
|||
});
|
||||
const drawer = createPlatformResourceDetailState({ idPrefix: 'kubernetes-networking-drawer' });
|
||||
const resolveResourceLabel = createPlatformResourceLabelResolver(() => props.resources);
|
||||
const sort = createPlatformTableSortState({
|
||||
storageKey: 'kubernetesNetworking',
|
||||
sortKeys: KUBERNETES_NETWORKING_SORT_KEYS,
|
||||
});
|
||||
const sortedRows = createMemo(() =>
|
||||
sort.sortRows(tableState.filtered(), getKubernetesNetworkingSortValue),
|
||||
);
|
||||
|
||||
return (
|
||||
<Show
|
||||
|
|
@ -161,38 +197,47 @@ export const KubernetesNetworkingTable: Component<{
|
|||
tableClass="min-w-full table-fixed text-xs md:min-w-[1180px]"
|
||||
header={
|
||||
<>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('name')} md:w-[19%]`}>
|
||||
<PlatformSortableTableHead
|
||||
kind="name"
|
||||
sort={sort}
|
||||
sortKey="resource"
|
||||
class="md:w-[19%]"
|
||||
>
|
||||
Resource
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[12%]`}>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead kind="text" sort={sort} sortKey="kind" class="md:w-[12%]">
|
||||
Kind
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[14%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="scope"
|
||||
class="hidden md:table-cell md:w-[14%]"
|
||||
>
|
||||
Scope
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[12%]`}>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="typeClass"
|
||||
class="md:w-[12%]"
|
||||
>
|
||||
Type / class
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[18%]`}
|
||||
>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead kind="text" sort={sort} class="hidden md:table-cell md:w-[18%]">
|
||||
Address / hosts
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[12%]`}>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead kind="text" sort={sort} class="md:w-[12%]">
|
||||
Ports
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[13%]`}
|
||||
>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead kind="text" sort={sort} class="hidden md:table-cell md:w-[13%]">
|
||||
Targets
|
||||
</TableHead>
|
||||
</PlatformSortableTableHead>
|
||||
</>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
<For each={tableState.filtered()}>
|
||||
<For each={sortedRows()}>
|
||||
{(resource) => {
|
||||
const indicator = () => getSimpleStatusIndicator(resource.status);
|
||||
const name = () => resourceName(resource);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { For, Show, createMemo, type Component, type JSX } from 'solid-js';
|
|||
import { StatusDot } from '@/components/shared/StatusDot';
|
||||
import { ResponsiveMetricCell } from '@/components/shared/responsive';
|
||||
import { StackedMemoryBar } from '@/components/Workloads/StackedMemoryBar';
|
||||
import { TableCell, TableHead, TableRow } from '@/components/shared/Table';
|
||||
import { TableCell, TableRow } from '@/components/shared/Table';
|
||||
import { asTrimmedString } from '@/utils/stringUtils';
|
||||
import { getAlertStyles } from '@/utils/alerts';
|
||||
import { useWebSocket } from '@/contexts/appRuntime';
|
||||
|
|
@ -10,17 +10,19 @@ import { useAlertsActivation } from '@/stores/alertsActivation';
|
|||
import { buildMetricKeyForUnifiedResource } from '@/utils/metricsKeys';
|
||||
import {
|
||||
PLATFORM_HEALTH_FILTER_OPTIONS,
|
||||
PlatformSortableTableHead,
|
||||
PlatformTableMetricFallback,
|
||||
PlatformTableEmptyState,
|
||||
PlatformTableShell,
|
||||
PlatformTableToolbar,
|
||||
createPlatformTableFilterState,
|
||||
createPlatformTableSortState,
|
||||
formatPlatformTableBytesValue,
|
||||
formatPlatformTableTextValue,
|
||||
formatPlatformTableUptimeValue,
|
||||
getPlatformTableFiniteMetric,
|
||||
getPlatformTableCellClassForKind,
|
||||
getPlatformTableHeadClassForKind,
|
||||
type PlatformTableSortValue,
|
||||
} from '@/features/platformPage/sharedPlatformPage';
|
||||
import {
|
||||
PlatformResourceDetailToggleButton,
|
||||
|
|
@ -55,6 +57,61 @@ const formatRoles = (roles: string[] | undefined): string => {
|
|||
return roles.map((role) => role.replace('node-role.kubernetes.io/', '')).join(', ');
|
||||
};
|
||||
|
||||
const KUBERNETES_NODE_SORT_KEYS = [
|
||||
'node',
|
||||
'cluster',
|
||||
'roles',
|
||||
'kubelet',
|
||||
'runtime',
|
||||
'cpu',
|
||||
'memory',
|
||||
'uptime',
|
||||
'capacity',
|
||||
] as const;
|
||||
|
||||
type KubernetesNodeSortKey = (typeof KUBERNETES_NODE_SORT_KEYS)[number];
|
||||
|
||||
// Scalar per column that user-controlled sorting orders on. Capacity is a
|
||||
// composite label (cores / bytes / pods); CPU core count is its dominant
|
||||
// scalar, so that is what the column sorts by.
|
||||
const getKubernetesNodeSortValue = (
|
||||
node: Resource,
|
||||
key: KubernetesNodeSortKey,
|
||||
): PlatformTableSortValue => {
|
||||
switch (key) {
|
||||
case 'node':
|
||||
return asTrimmedString(node.name) || node.id;
|
||||
case 'cluster':
|
||||
return kubernetesClusterLabel(node) || null;
|
||||
case 'roles': {
|
||||
const roles = formatRoles(node.kubernetes?.roles);
|
||||
return roles === '—' ? null : roles;
|
||||
}
|
||||
case 'kubelet':
|
||||
return asTrimmedString(node.kubernetes?.kubeletVersion) || null;
|
||||
case 'runtime':
|
||||
return asTrimmedString(node.kubernetes?.containerRuntimeVersion) || null;
|
||||
case 'cpu':
|
||||
return getPlatformTableFiniteMetric(node.cpu?.current) ?? null;
|
||||
case 'memory': {
|
||||
const total = getPlatformTableFiniteMetric(node.memory?.total) ?? 0;
|
||||
if (total > 0) {
|
||||
return ((getPlatformTableFiniteMetric(node.memory?.used) ?? 0) / total) * 100;
|
||||
}
|
||||
return getPlatformTableFiniteMetric(node.memory?.current) ?? null;
|
||||
}
|
||||
case 'uptime':
|
||||
return getPlatformTableFiniteMetric(node.uptime) ?? null;
|
||||
case 'capacity': {
|
||||
const cores = node.kubernetes?.capacityCpuCores;
|
||||
return typeof cores === 'number' && cores > 0 ? cores : null;
|
||||
}
|
||||
default:
|
||||
key satisfies never;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const KubernetesNodesTable: Component<{
|
||||
resources: Resource[];
|
||||
emptyIcon: JSX.Element;
|
||||
|
|
@ -73,7 +130,20 @@ export const KubernetesNodesTable: Component<{
|
|||
});
|
||||
const drawer = createPlatformResourceDetailState({ idPrefix: 'kubernetes-node-drawer' });
|
||||
const resolveResourceLabel = createPlatformResourceLabelResolver(() => props.resources);
|
||||
const sortedRows = createMemo(() => [...tableState.filtered()].sort(compareKubernetesNodes));
|
||||
// User-controlled sorting layered over the attention-first default: rows
|
||||
// are pre-sorted by the status compare, so a user sort keeps that order
|
||||
// for ties and the table falls straight back to it when the sort clears.
|
||||
const sort = createPlatformTableSortState({
|
||||
storageKey: 'kubernetesNodes',
|
||||
sortKeys: KUBERNETES_NODE_SORT_KEYS,
|
||||
descendingFirst: ['cpu', 'memory', 'uptime', 'capacity'],
|
||||
});
|
||||
const sortedRows = createMemo(() =>
|
||||
sort.sortRows(
|
||||
[...tableState.filtered()].sort(compareKubernetesNodes),
|
||||
getKubernetesNodeSortValue,
|
||||
),
|
||||
);
|
||||
|
||||
return (
|
||||
<Show
|
||||
|
|
@ -126,45 +196,73 @@ export const KubernetesNodesTable: Component<{
|
|||
desktop gets extra room without forcing normal desktop
|
||||
viewports to hide Capacity behind horizontal scroll.
|
||||
*/}
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('name')} md:w-[15%]`}>
|
||||
<PlatformSortableTableHead kind="name" sort={sort} sortKey="node" class="md:w-[15%]">
|
||||
Node
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[10%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="cluster"
|
||||
class="hidden md:table-cell md:w-[10%]"
|
||||
>
|
||||
Cluster
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[10%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="roles"
|
||||
class="hidden md:table-cell md:w-[10%]"
|
||||
>
|
||||
Roles
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[8%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="kubelet"
|
||||
class="hidden md:table-cell md:w-[8%]"
|
||||
>
|
||||
Kubelet
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[15%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="runtime"
|
||||
class="hidden md:table-cell md:w-[15%]"
|
||||
>
|
||||
Runtime
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('metric-bar')} md:w-[11%]`}>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="metric-bar"
|
||||
sort={sort}
|
||||
sortKey="cpu"
|
||||
class="md:w-[11%]"
|
||||
>
|
||||
CPU
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('metric-bar')} md:w-[11%]`}>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="metric-bar"
|
||||
sort={sort}
|
||||
sortKey="memory"
|
||||
class="md:w-[11%]"
|
||||
>
|
||||
Memory
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[6%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="numeric-value"
|
||||
sort={sort}
|
||||
sortKey="uptime"
|
||||
class="hidden md:table-cell md:w-[6%]"
|
||||
>
|
||||
Uptime
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[14%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="numeric-value"
|
||||
sort={sort}
|
||||
sortKey="capacity"
|
||||
class="md:w-[14%]"
|
||||
>
|
||||
Capacity
|
||||
</TableHead>
|
||||
</PlatformSortableTableHead>
|
||||
</>
|
||||
}
|
||||
body={
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
import { For, Show, createMemo, type Component, type JSX } from 'solid-js';
|
||||
import { StatusDot } from '@/components/shared/StatusDot';
|
||||
import { TableCell, TableHead, TableRow } from '@/components/shared/Table';
|
||||
import { TableCell, TableRow } from '@/components/shared/Table';
|
||||
import { asTrimmedString } from '@/utils/stringUtils';
|
||||
import {
|
||||
PLATFORM_HEALTH_FILTER_OPTIONS,
|
||||
PlatformSortableTableHead,
|
||||
PlatformTableEmptyState,
|
||||
PlatformTableNumberValue,
|
||||
PlatformTableToolbar,
|
||||
createPlatformTableFilterState,
|
||||
createPlatformTableSortState,
|
||||
formatPlatformTableTextValue,
|
||||
getPlatformTableCellClassForKind,
|
||||
getPlatformTableHeadClassForKind,
|
||||
PlatformTableShell,
|
||||
type PlatformTableSortValue,
|
||||
} from '@/features/platformPage/sharedPlatformPage';
|
||||
import {
|
||||
PlatformResourceDetailToggleButton,
|
||||
|
|
@ -71,6 +73,62 @@ const ageValue = (resource: Resource): string => {
|
|||
return `${Math.floor(seconds / 86_400)}d`;
|
||||
};
|
||||
|
||||
const KUBERNETES_POD_SORT_KEYS = [
|
||||
'pod',
|
||||
'scope',
|
||||
'node',
|
||||
'status',
|
||||
'ready',
|
||||
'restarts',
|
||||
'owner',
|
||||
'image',
|
||||
'age',
|
||||
] as const;
|
||||
|
||||
type KubernetesPodSortKey = (typeof KUBERNETES_POD_SORT_KEYS)[number];
|
||||
|
||||
// Scalar per column that user-controlled sorting orders on. Ready sorts by
|
||||
// the ready fraction (ascending-first, so the least-ready pods surface on
|
||||
// the first click); '—' style empty renders map to null so rows without
|
||||
// the datum sink to the bottom.
|
||||
const getKubernetesPodSortValue = (
|
||||
resource: Resource,
|
||||
key: KubernetesPodSortKey,
|
||||
): PlatformTableSortValue => {
|
||||
switch (key) {
|
||||
case 'pod':
|
||||
return podName(resource);
|
||||
case 'scope':
|
||||
return kubernetesScopeLabel(resource);
|
||||
case 'node':
|
||||
return asTrimmedString(resource.kubernetes?.nodeName) || null;
|
||||
case 'status':
|
||||
return mapKubernetesPodStatus(resource).label;
|
||||
case 'ready': {
|
||||
const containers = podContainers(resource);
|
||||
if (containers.length === 0) return null;
|
||||
return containers.filter((container) => container.ready).length / containers.length;
|
||||
}
|
||||
case 'restarts':
|
||||
return restartCount(resource) ?? null;
|
||||
case 'owner': {
|
||||
const owner = ownerValue(resource);
|
||||
return owner === '—' ? null : owner;
|
||||
}
|
||||
case 'image': {
|
||||
const image = imageValue(resource);
|
||||
return image === '—' ? null : image;
|
||||
}
|
||||
case 'age': {
|
||||
const seconds = resource.kubernetes?.uptimeSeconds ?? resource.uptime;
|
||||
return typeof seconds === 'number' && seconds >= 0 ? seconds : null;
|
||||
}
|
||||
default:
|
||||
key satisfies never;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const KubernetesPodsTable: Component<{
|
||||
resources: Resource[];
|
||||
emptyIcon: JSX.Element;
|
||||
|
|
@ -88,7 +146,17 @@ export const KubernetesPodsTable: Component<{
|
|||
externalSearch: props.externalSearch,
|
||||
externalStatus: props.externalStatus,
|
||||
});
|
||||
const sortedRows = createMemo(() => [...tableState.filtered()].sort(compareKubernetesPods));
|
||||
// User-controlled sorting layered over the attention-first default: rows
|
||||
// are pre-sorted by the status compare, so a user sort keeps that order
|
||||
// for ties and the table falls straight back to it when the sort clears.
|
||||
const sort = createPlatformTableSortState({
|
||||
storageKey: 'kubernetesPods',
|
||||
sortKeys: KUBERNETES_POD_SORT_KEYS,
|
||||
descendingFirst: ['restarts', 'age'],
|
||||
});
|
||||
const sortedRows = createMemo(() =>
|
||||
sort.sortRows([...tableState.filtered()].sort(compareKubernetesPods), getKubernetesPodSortValue),
|
||||
);
|
||||
const drawer = createPlatformResourceDetailState({ idPrefix: 'kubernetes-pod-drawer' });
|
||||
const resolveResourceLabel = createPlatformResourceLabelResolver(() => props.resources);
|
||||
|
||||
|
|
@ -133,43 +201,68 @@ export const KubernetesPodsTable: Component<{
|
|||
tableClass="min-w-full table-fixed text-xs md:min-w-[1240px]"
|
||||
header={
|
||||
<>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('name')} md:w-[20%]`}>
|
||||
<PlatformSortableTableHead kind="name" sort={sort} sortKey="pod" class="md:w-[20%]">
|
||||
Pod
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[13%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="scope"
|
||||
class="hidden md:table-cell md:w-[13%]"
|
||||
>
|
||||
Scope
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[13%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="node"
|
||||
class="hidden md:table-cell md:w-[13%]"
|
||||
>
|
||||
Node
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[8%]`}>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead kind="text" sort={sort} sortKey="status" class="md:w-[8%]">
|
||||
Status
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[7%]`}>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="numeric-value"
|
||||
sort={sort}
|
||||
sortKey="ready"
|
||||
class="md:w-[7%]"
|
||||
>
|
||||
Ready
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[8%]`}>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="numeric-value"
|
||||
sort={sort}
|
||||
sortKey="restarts"
|
||||
class="md:w-[8%]"
|
||||
>
|
||||
Restarts
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[13%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="owner"
|
||||
class="hidden md:table-cell md:w-[13%]"
|
||||
>
|
||||
Owner
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[14%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="image"
|
||||
class="hidden md:table-cell md:w-[14%]"
|
||||
>
|
||||
Image
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[4%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="age"
|
||||
class="hidden md:table-cell md:w-[4%]"
|
||||
>
|
||||
Age
|
||||
</TableHead>
|
||||
</PlatformSortableTableHead>
|
||||
</>
|
||||
}
|
||||
body={
|
||||
|
|
|
|||
|
|
@ -1,18 +1,20 @@
|
|||
import { For, Show, type Component, type JSX } from 'solid-js';
|
||||
import { For, Show, createMemo, type Component, type JSX } from 'solid-js';
|
||||
import { StatusDot } from '@/components/shared/StatusDot';
|
||||
import { TableCell, TableHead, TableRow } from '@/components/shared/Table';
|
||||
import { TableCell, TableRow } from '@/components/shared/Table';
|
||||
import { getSimpleStatusIndicator } from '@/utils/status';
|
||||
import { asTrimmedString } from '@/utils/stringUtils';
|
||||
import {
|
||||
PLATFORM_HEALTH_FILTER_OPTIONS,
|
||||
PlatformSortableTableHead,
|
||||
PlatformTableEmptyState,
|
||||
PlatformTableToolbar,
|
||||
createPlatformTableFilterState,
|
||||
createPlatformTableSortState,
|
||||
formatPlatformTableTextValue,
|
||||
getPlatformTableCellClassForKind,
|
||||
getPlatformTableHeadClassForKind,
|
||||
summarizePlatformTableValues,
|
||||
PlatformTableShell,
|
||||
type PlatformTableSortValue,
|
||||
} from '@/features/platformPage/sharedPlatformPage';
|
||||
import {
|
||||
PlatformResourceDetailToggleButton,
|
||||
|
|
@ -59,6 +61,31 @@ const selectorSummary = (resource: Resource): { label: string; title: string } =
|
|||
const externalIpSummary = (resource: Resource): { label: string; title: string } =>
|
||||
summarizePlatformTableValues(resource.kubernetes?.externalIps);
|
||||
|
||||
// The multi-value summary columns (External IPs, Ports, Selector) carry no
|
||||
// single scalar to order on, so they stay non-sortable.
|
||||
const KUBERNETES_SERVICE_SORT_KEYS = ['service', 'scope', 'type', 'clusterIp'] as const;
|
||||
|
||||
type KubernetesServiceSortKey = (typeof KUBERNETES_SERVICE_SORT_KEYS)[number];
|
||||
|
||||
const getKubernetesServiceSortValue = (
|
||||
resource: Resource,
|
||||
key: KubernetesServiceSortKey,
|
||||
): PlatformTableSortValue => {
|
||||
switch (key) {
|
||||
case 'service':
|
||||
return serviceName(resource);
|
||||
case 'scope':
|
||||
return kubernetesScopeLabel(resource);
|
||||
case 'type':
|
||||
return asTrimmedString(resource.kubernetes?.serviceType) || null;
|
||||
case 'clusterIp':
|
||||
return asTrimmedString(resource.kubernetes?.clusterIp) || null;
|
||||
default:
|
||||
key satisfies never;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const KubernetesServicesTable: Component<{
|
||||
resources: Resource[];
|
||||
emptyIcon: JSX.Element;
|
||||
|
|
@ -78,6 +105,13 @@ export const KubernetesServicesTable: Component<{
|
|||
});
|
||||
const drawer = createPlatformResourceDetailState({ idPrefix: 'kubernetes-service-drawer' });
|
||||
const resolveResourceLabel = createPlatformResourceLabelResolver(() => props.resources);
|
||||
const sort = createPlatformTableSortState({
|
||||
storageKey: 'kubernetesServices',
|
||||
sortKeys: KUBERNETES_SERVICE_SORT_KEYS,
|
||||
});
|
||||
const sortedRows = createMemo(() =>
|
||||
sort.sortRows(tableState.filtered(), getKubernetesServiceSortValue),
|
||||
);
|
||||
|
||||
return (
|
||||
<Show
|
||||
|
|
@ -120,38 +154,47 @@ export const KubernetesServicesTable: Component<{
|
|||
tableClass="min-w-full table-fixed text-xs md:min-w-[1120px]"
|
||||
header={
|
||||
<>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('name')} md:w-[19%]`}>
|
||||
<PlatformSortableTableHead
|
||||
kind="name"
|
||||
sort={sort}
|
||||
sortKey="service"
|
||||
class="md:w-[19%]"
|
||||
>
|
||||
Service
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[15%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="scope"
|
||||
class="hidden md:table-cell md:w-[15%]"
|
||||
>
|
||||
Scope
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[11%]`}>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead kind="text" sort={sort} sortKey="type" class="md:w-[11%]">
|
||||
Type
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[13%]`}>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="clusterIp"
|
||||
class="md:w-[13%]"
|
||||
>
|
||||
Cluster IP
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[13%]`}
|
||||
>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead kind="text" sort={sort} class="hidden md:table-cell md:w-[13%]">
|
||||
External IPs
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[16%]`}>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead kind="text" sort={sort} class="md:w-[16%]">
|
||||
Ports
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[13%]`}
|
||||
>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead kind="text" sort={sort} class="hidden md:table-cell md:w-[13%]">
|
||||
Selector
|
||||
</TableHead>
|
||||
</PlatformSortableTableHead>
|
||||
</>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
<For each={tableState.filtered()}>
|
||||
<For each={sortedRows()}>
|
||||
{(resource) => {
|
||||
const indicator = () => getSimpleStatusIndicator(resource.status);
|
||||
const name = () => serviceName(resource);
|
||||
|
|
|
|||
|
|
@ -1,18 +1,20 @@
|
|||
import { For, Show, type Component, type JSX } from 'solid-js';
|
||||
import { For, Show, createMemo, type Component, type JSX } from 'solid-js';
|
||||
import { StatusDot } from '@/components/shared/StatusDot';
|
||||
import { TableCell, TableHead, TableRow } from '@/components/shared/Table';
|
||||
import { TableCell, TableRow } from '@/components/shared/Table';
|
||||
import { getSimpleStatusIndicator } from '@/utils/status';
|
||||
import { asTrimmedString } from '@/utils/stringUtils';
|
||||
import {
|
||||
PLATFORM_HEALTH_FILTER_OPTIONS,
|
||||
PlatformSortableTableHead,
|
||||
PlatformTableEmptyState,
|
||||
PlatformTableToolbar,
|
||||
createPlatformTableFilterState,
|
||||
createPlatformTableSortState,
|
||||
formatPlatformTableBytesValue,
|
||||
formatPlatformTableTextValue,
|
||||
getPlatformTableCellClassForKind,
|
||||
getPlatformTableHeadClassForKind,
|
||||
PlatformTableShell,
|
||||
type PlatformTableSortValue,
|
||||
} from '@/features/platformPage/sharedPlatformPage';
|
||||
import {
|
||||
PlatformResourceDetailToggleButton,
|
||||
|
|
@ -143,6 +145,63 @@ const bindingTarget = (resource: Resource): { label: string; title: string } =>
|
|||
return { label: volumeName || 'Unbound', title: volumeName || 'Unbound' };
|
||||
};
|
||||
|
||||
// Access / policy is a composite label (access modes + reclaim policy +
|
||||
// expansion), so it stays non-sortable. Size sorts on the same bytes the
|
||||
// capacity label renders (capacity, falling back to the PVC request).
|
||||
const KUBERNETES_STORAGE_SORT_KEYS = [
|
||||
'resource',
|
||||
'kind',
|
||||
'scope',
|
||||
'phase',
|
||||
'class',
|
||||
'size',
|
||||
'provider',
|
||||
] as const;
|
||||
|
||||
type KubernetesStorageSortKey = (typeof KUBERNETES_STORAGE_SORT_KEYS)[number];
|
||||
|
||||
const getKubernetesStorageSortValue = (
|
||||
resource: Resource,
|
||||
key: KubernetesStorageSortKey,
|
||||
): PlatformTableSortValue => {
|
||||
switch (key) {
|
||||
case 'resource':
|
||||
return storageName(resource);
|
||||
case 'kind':
|
||||
return storageKind(resource);
|
||||
case 'scope':
|
||||
return kubernetesScopeLabel(resource);
|
||||
case 'phase': {
|
||||
const phase = bindingOrPhase(resource);
|
||||
return phase === '—' ? null : phase;
|
||||
}
|
||||
case 'class': {
|
||||
const klass = storageClass(resource);
|
||||
return klass === '—' ? null : klass;
|
||||
}
|
||||
case 'size': {
|
||||
const capacity = resource.kubernetes?.capacityBytes;
|
||||
if (typeof capacity === 'number' && capacity > 0) return capacity;
|
||||
const requested = resource.kubernetes?.requestedBytes;
|
||||
if (
|
||||
resource.type === 'k8s-persistent-volume-claim' &&
|
||||
typeof requested === 'number' &&
|
||||
requested > 0
|
||||
) {
|
||||
return requested;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case 'provider': {
|
||||
const target = bindingTarget(resource).label;
|
||||
return target === '—' ? null : target;
|
||||
}
|
||||
default:
|
||||
key satisfies never;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const KubernetesStorageTable: Component<{
|
||||
resources: Resource[];
|
||||
emptyIcon: JSX.Element;
|
||||
|
|
@ -158,6 +217,14 @@ export const KubernetesStorageTable: Component<{
|
|||
});
|
||||
const drawer = createPlatformResourceDetailState({ idPrefix: 'kubernetes-storage-drawer' });
|
||||
const resolveResourceLabel = createPlatformResourceLabelResolver(() => props.resources);
|
||||
const sort = createPlatformTableSortState({
|
||||
storageKey: 'kubernetesStorage',
|
||||
sortKeys: KUBERNETES_STORAGE_SORT_KEYS,
|
||||
descendingFirst: ['size'],
|
||||
});
|
||||
const sortedRows = createMemo(() =>
|
||||
sort.sortRows(tableState.filtered(), getKubernetesStorageSortValue),
|
||||
);
|
||||
|
||||
return (
|
||||
<Show
|
||||
|
|
@ -200,43 +267,65 @@ export const KubernetesStorageTable: Component<{
|
|||
tableClass="min-w-full table-fixed text-xs md:min-w-[1120px]"
|
||||
header={
|
||||
<>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('name')} md:w-[19%]`}>
|
||||
<PlatformSortableTableHead
|
||||
kind="name"
|
||||
sort={sort}
|
||||
sortKey="resource"
|
||||
class="md:w-[19%]"
|
||||
>
|
||||
Resource
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[10%]`}>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead kind="text" sort={sort} sortKey="kind" class="md:w-[10%]">
|
||||
Kind
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[13%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="scope"
|
||||
class="hidden md:table-cell md:w-[13%]"
|
||||
>
|
||||
Scope
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[14%]`}>
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="phase"
|
||||
class="md:w-[14%]"
|
||||
>
|
||||
Binding / phase
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[10%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="class"
|
||||
class="hidden md:table-cell md:w-[10%]"
|
||||
>
|
||||
Class
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[8%]`}>
|
||||
Size
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[12%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="numeric-value"
|
||||
sort={sort}
|
||||
sortKey="size"
|
||||
class="md:w-[8%]"
|
||||
>
|
||||
Size
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead kind="text" sort={sort} class="hidden md:table-cell md:w-[12%]">
|
||||
Access / policy
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[14%]`}
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="provider"
|
||||
class="hidden md:table-cell md:w-[14%]"
|
||||
>
|
||||
Provider / binding
|
||||
</TableHead>
|
||||
</PlatformSortableTableHead>
|
||||
</>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
<For each={tableState.filtered()}>
|
||||
<For each={sortedRows()}>
|
||||
{(resource) => {
|
||||
const indicator = () => getSimpleStatusIndicator(resource.status);
|
||||
const name = () => storageName(resource);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import type { Resource } from '@/types/resource';
|
||||
import { KubernetesPodsTable } from '../KubernetesPodsTable';
|
||||
|
||||
const makePod = ({ id, ...overrides }: Partial<Resource> & Pick<Resource, 'id'>): Resource => ({
|
||||
id,
|
||||
name: id,
|
||||
displayName: id,
|
||||
platformId: 'cluster-1',
|
||||
platformType: 'kubernetes',
|
||||
sourceType: 'agent',
|
||||
sources: ['kubernetes'],
|
||||
status: 'online',
|
||||
type: 'pod',
|
||||
lastSeen: 1_700_000_000_000,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
// Mixed statuses so both the attention-first default order and the user
|
||||
// sort are observable: zulu is the only danger row and floats by default.
|
||||
const FIXTURE: Resource[] = [
|
||||
makePod({
|
||||
id: 'alpha',
|
||||
kubernetes: {
|
||||
podPhase: 'Running',
|
||||
podContainers: [{ ready: true, state: 'running', restartCount: 9 }],
|
||||
},
|
||||
}),
|
||||
makePod({
|
||||
id: 'zulu',
|
||||
kubernetes: {
|
||||
podPhase: 'Running',
|
||||
podContainers: [{ ready: false, state: 'waiting', reason: 'CrashLoopBackOff', restartCount: 3 }],
|
||||
},
|
||||
}),
|
||||
makePod({
|
||||
id: 'mike',
|
||||
kubernetes: {
|
||||
podPhase: 'Running',
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
const renderTable = () =>
|
||||
render(() => (
|
||||
<KubernetesPodsTable
|
||||
resources={FIXTURE}
|
||||
emptyIcon={<span />}
|
||||
emptyTitle="No pods"
|
||||
emptyDescription="No pods"
|
||||
showToolbar={false}
|
||||
/>
|
||||
));
|
||||
|
||||
const visibleRowOrder = (): string[] =>
|
||||
Array.from(document.querySelectorAll('tr[data-kubernetes-pod-row]')).map(
|
||||
(row) => row.getAttribute('data-kubernetes-pod-row') ?? '',
|
||||
);
|
||||
|
||||
const headerFor = (label: string): HTMLElement => {
|
||||
const header = screen
|
||||
.getAllByRole('columnheader')
|
||||
.find((th) => th.textContent?.trim().startsWith(label));
|
||||
if (!header) throw new Error(`No column header labelled ${label}`);
|
||||
return header;
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
window.localStorage.clear();
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe('KubernetesPodsTable user sorting', () => {
|
||||
it('cycles a name sort and falls back to the attention-first order', () => {
|
||||
renderTable();
|
||||
|
||||
// Built-in order: attention first (zulu is CrashLoopBackOff), then names.
|
||||
expect(visibleRowOrder()).toEqual(['zulu', 'alpha', 'mike']);
|
||||
|
||||
fireEvent.click(headerFor('Pod'));
|
||||
expect(headerFor('Pod')).toHaveAttribute('aria-sort', 'ascending');
|
||||
expect(visibleRowOrder()).toEqual(['alpha', 'mike', 'zulu']);
|
||||
|
||||
fireEvent.click(headerFor('Pod'));
|
||||
expect(headerFor('Pod')).toHaveAttribute('aria-sort', 'descending');
|
||||
expect(visibleRowOrder()).toEqual(['zulu', 'mike', 'alpha']);
|
||||
|
||||
// Third click clears back to the built-in attention-first order.
|
||||
fireEvent.click(headerFor('Pod'));
|
||||
expect(headerFor('Pod')).not.toHaveAttribute('aria-sort');
|
||||
expect(visibleRowOrder()).toEqual(['zulu', 'alpha', 'mike']);
|
||||
});
|
||||
|
||||
it('sorts restarts descending first with missing values last', () => {
|
||||
renderTable();
|
||||
|
||||
fireEvent.click(headerFor('Restarts'));
|
||||
expect(headerFor('Restarts')).toHaveAttribute('aria-sort', 'descending');
|
||||
// mike reports no containers and no restart count, so it sinks.
|
||||
expect(visibleRowOrder()).toEqual(['alpha', 'zulu', 'mike']);
|
||||
});
|
||||
|
||||
it('persists the chosen sort across a remount', () => {
|
||||
renderTable();
|
||||
fireEvent.click(headerFor('Pod'));
|
||||
expect(visibleRowOrder()).toEqual(['alpha', 'mike', 'zulu']);
|
||||
expect(window.localStorage.getItem('kubernetesPodsSortKey')).toBe('pod');
|
||||
expect(window.localStorage.getItem('kubernetesPodsSortDirection')).toBe('asc');
|
||||
|
||||
cleanup();
|
||||
|
||||
renderTable();
|
||||
expect(headerFor('Pod')).toHaveAttribute('aria-sort', 'ascending');
|
||||
expect(visibleRowOrder()).toEqual(['alpha', 'mike', 'zulu']);
|
||||
});
|
||||
});
|
||||
|
|
@ -410,18 +410,12 @@ describe('platform overview layout guardrails', () => {
|
|||
'<span class="md:hidden">{compactCapacityLabel()}</span>',
|
||||
);
|
||||
|
||||
expect(truenasSystemsTableSource).toMatch(
|
||||
/getPlatformTableHeadClassForKind\('name'\)[\s\S]{0,200}?System/,
|
||||
);
|
||||
expect(truenasSystemsTableSource).toMatch(/kind="name"[\s\S]{0,200}?System/);
|
||||
expect(truenasSystemsTableSource).toMatch(
|
||||
/<span class="md:hidden">\s*<PlatformTablePercentValue value={storagePercent\(\)} \/>\s*<\/span>/,
|
||||
);
|
||||
expect(vsphereHostsTableSource).toMatch(
|
||||
/getPlatformTableHeadClassForKind\('name'\)[\s\S]{0,200}?Host/,
|
||||
);
|
||||
expect(vsphereHostsTableSource).toMatch(
|
||||
/getPlatformTableHeadClassForKind\('numeric-value'\)[\s\S]{0,200}?VMs/,
|
||||
);
|
||||
expect(vsphereHostsTableSource).toMatch(/kind="name"[\s\S]{0,200}?Host/);
|
||||
expect(vsphereHostsTableSource).toMatch(/kind="numeric-value"[\s\S]{0,200}?VMs/);
|
||||
expect(vsphereHostsTableSource).toContain('hidden md:table-cell');
|
||||
// AgentsMachinesTable uses a column-config pattern: kind helpers are
|
||||
// applied dynamically in the table render, with labels living in the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue