mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-22 19:56:01 +00:00
kubernetes(overview): restore v5 healthy/total fractions on cluster counts
The v5 clusters table read "2/3 ready" the moment a node went down; the v6 rewrite flattened Nodes / Pods / Deployments to raw totals, so a cluster with a NotReady node still showed a plain "3". Roll attention counts (danger/warning indicator variants) into the per-cluster child rollup and render healthy/total with the amber tone on shortfall. Muted states (Unknown, deployments scaled to zero) deliberately do not count as attention.
This commit is contained in:
@@ -35,7 +35,9 @@ import {
|
||||
import type { Resource } from '@/types/resource';
|
||||
import {
|
||||
buildKubernetesClusterChildCounts,
|
||||
emptyKubernetesClusterChildCounts,
|
||||
filterKubernetesResources,
|
||||
type KubernetesClusterChildCount,
|
||||
type KubernetesResourceStatusFilter,
|
||||
} from './kubernetesPageModel';
|
||||
|
||||
@@ -60,6 +62,18 @@ const metricFallback = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
// v5-style healthy/total fraction: the healthy share turns amber as soon as
|
||||
// any child needs attention, so a cluster with a NotReady node reads "2/3"
|
||||
// instead of a flat "3".
|
||||
const childCountCell = (count: KubernetesClusterChildCount) => (
|
||||
<>
|
||||
<span class={count.attention > 0 ? 'text-amber-700 dark:text-amber-300' : ''}>
|
||||
{count.total - count.attention}
|
||||
</span>
|
||||
<span class="text-muted">/{count.total}</span>
|
||||
</>
|
||||
);
|
||||
|
||||
export const KubernetesClustersTable: Component<{
|
||||
clusters: Resource[];
|
||||
// All Kubernetes-tagged resources from the same query, so the table can
|
||||
@@ -169,11 +183,7 @@ export const KubernetesClustersTable: Component<{
|
||||
const hasMemoryMetric = () =>
|
||||
memoryTotal() > 0 || memoryPercentOnly() !== undefined;
|
||||
const counts = () =>
|
||||
countsByCluster().get(cluster.id) ?? {
|
||||
nodes: 0,
|
||||
pods: 0,
|
||||
deployments: 0,
|
||||
};
|
||||
countsByCluster().get(cluster.id) ?? emptyKubernetesClusterChildCounts();
|
||||
const indicator = () => getSimpleStatusIndicator(cluster.status);
|
||||
const canRenderMetrics = () => indicator().variant !== 'muted';
|
||||
const detailRowId = () => drawer.detailRowId(cluster);
|
||||
@@ -222,17 +232,17 @@ export const KubernetesClustersTable: Component<{
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content tabular-nums`}
|
||||
>
|
||||
{counts().nodes}
|
||||
{childCountCell(counts().nodes)}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums md:table-cell`}
|
||||
>
|
||||
{counts().pods}
|
||||
{childCountCell(counts().pods)}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums md:table-cell`}
|
||||
>
|
||||
{counts().deployments}
|
||||
{childCountCell(counts().deployments)}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('metric-bar')} w-[20%] md:w-auto`}
|
||||
|
||||
@@ -674,8 +674,71 @@ describe('kubernetesPageModel', () => {
|
||||
];
|
||||
|
||||
const counts = buildKubernetesClusterChildCounts(resources, [prod, stg]);
|
||||
expect(counts.get('cluster-prod')).toEqual({ nodes: 2, pods: 3, deployments: 1 });
|
||||
expect(counts.get('cluster-stg')).toEqual({ nodes: 1, pods: 1, deployments: 0 });
|
||||
expect(counts.get('cluster-prod')).toEqual({
|
||||
nodes: { total: 2, attention: 0 },
|
||||
pods: { total: 3, attention: 0 },
|
||||
deployments: { total: 1, attention: 0 },
|
||||
});
|
||||
expect(counts.get('cluster-stg')).toEqual({
|
||||
nodes: { total: 1, attention: 0 },
|
||||
pods: { total: 1, attention: 0 },
|
||||
deployments: { total: 0, attention: 0 },
|
||||
});
|
||||
});
|
||||
|
||||
it('tallies attention per bucket from the status mappers', () => {
|
||||
const prod = cluster('cluster-prod', { clusterId: 'prod' });
|
||||
const counts = buildKubernetesClusterChildCounts(
|
||||
[
|
||||
prod,
|
||||
makeResource({
|
||||
id: 'n-ready',
|
||||
type: 'k8s-node',
|
||||
kubernetes: { clusterId: 'prod', ready: true },
|
||||
}),
|
||||
makeResource({
|
||||
id: 'n-cordoned',
|
||||
type: 'k8s-node',
|
||||
kubernetes: { clusterId: 'prod', ready: true, unschedulable: true },
|
||||
}),
|
||||
makeResource({
|
||||
id: 'p-running',
|
||||
type: 'pod',
|
||||
kubernetes: {
|
||||
clusterId: 'prod',
|
||||
podPhase: 'Running',
|
||||
podContainers: [{ ready: true, state: 'running' }],
|
||||
},
|
||||
}),
|
||||
makeResource({
|
||||
id: 'p-crash',
|
||||
type: 'pod',
|
||||
kubernetes: {
|
||||
clusterId: 'prod',
|
||||
podPhase: 'Running',
|
||||
podContainers: [{ ready: false, state: 'waiting', reason: 'CrashLoopBackOff' }],
|
||||
},
|
||||
}),
|
||||
makeResource({
|
||||
id: 'd-scaled-to-zero',
|
||||
type: 'k8s-deployment',
|
||||
kubernetes: { clusterId: 'prod', desiredReplicas: 0 },
|
||||
}),
|
||||
makeResource({
|
||||
id: 'd-partial',
|
||||
type: 'k8s-deployment',
|
||||
kubernetes: { clusterId: 'prod', desiredReplicas: 3, readyReplicas: 1 },
|
||||
}),
|
||||
],
|
||||
[prod],
|
||||
);
|
||||
expect(counts.get('cluster-prod')).toEqual({
|
||||
nodes: { total: 2, attention: 1 },
|
||||
pods: { total: 2, attention: 1 },
|
||||
// Scaled-to-zero is muted, not degraded; only the partial rollout
|
||||
// counts as attention.
|
||||
deployments: { total: 2, attention: 1 },
|
||||
});
|
||||
});
|
||||
|
||||
it('counts agent rows that report a kubernetes source as cluster nodes', () => {
|
||||
@@ -698,7 +761,11 @@ describe('kubernetesPageModel', () => {
|
||||
],
|
||||
[prod],
|
||||
);
|
||||
expect(counts.get('cluster-prod')).toEqual({ nodes: 1, pods: 0, deployments: 0 });
|
||||
expect(counts.get('cluster-prod')).toEqual({
|
||||
nodes: { total: 1, attention: 0 },
|
||||
pods: { total: 0, attention: 0 },
|
||||
deployments: { total: 0, attention: 0 },
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores resources whose kubernetes.clusterId / clusterName matches no cluster row', () => {
|
||||
@@ -711,7 +778,11 @@ describe('kubernetesPageModel', () => {
|
||||
],
|
||||
[prod],
|
||||
);
|
||||
expect(counts.get('cluster-prod')).toEqual({ nodes: 0, pods: 0, deployments: 0 });
|
||||
expect(counts.get('cluster-prod')).toEqual({
|
||||
nodes: { total: 0, attention: 0 },
|
||||
pods: { total: 0, attention: 0 },
|
||||
deployments: { total: 0, attention: 0 },
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an empty map when there are no clusters', () => {
|
||||
|
||||
@@ -704,18 +704,36 @@ export type KubernetesPageModel = {
|
||||
incidents: KubernetesIncidentRow[];
|
||||
};
|
||||
|
||||
export type KubernetesClusterChildCounts = {
|
||||
nodes: number;
|
||||
pods: number;
|
||||
deployments: number;
|
||||
export type KubernetesClusterChildCount = {
|
||||
total: number;
|
||||
// Rows whose status indicator is danger or warning. Healthy-for-display is
|
||||
// total - attention, so muted states (Unknown, Scaled to 0) never read as
|
||||
// degraded.
|
||||
attention: number;
|
||||
};
|
||||
|
||||
const emptyKubernetesClusterChildCounts = (): KubernetesClusterChildCounts => ({
|
||||
nodes: 0,
|
||||
pods: 0,
|
||||
deployments: 0,
|
||||
export type KubernetesClusterChildCounts = {
|
||||
nodes: KubernetesClusterChildCount;
|
||||
pods: KubernetesClusterChildCount;
|
||||
deployments: KubernetesClusterChildCount;
|
||||
};
|
||||
|
||||
export const emptyKubernetesClusterChildCounts = (): KubernetesClusterChildCounts => ({
|
||||
nodes: { total: 0, attention: 0 },
|
||||
pods: { total: 0, attention: 0 },
|
||||
deployments: { total: 0, attention: 0 },
|
||||
});
|
||||
|
||||
const tallyKubernetesChild = (
|
||||
count: KubernetesClusterChildCount,
|
||||
indicator: StatusIndicator,
|
||||
): void => {
|
||||
count.total += 1;
|
||||
if (indicator.variant === 'danger' || indicator.variant === 'warning') {
|
||||
count.attention += 1;
|
||||
}
|
||||
};
|
||||
|
||||
const matchClusterFor = (
|
||||
resource: Resource,
|
||||
clusters: readonly Resource[],
|
||||
@@ -738,10 +756,12 @@ const matchClusterFor = (
|
||||
};
|
||||
|
||||
// Walks the resource snapshot once and rolls per-cluster Nodes / Pods /
|
||||
// Deployments into a Map keyed by cluster row id. The Overview clusters
|
||||
// table calls this instead of computing counts inline, so the same
|
||||
// rollup is testable and reusable by other consumers (incident rollups,
|
||||
// future Overview "active issues" cards).
|
||||
// Deployments into a Map keyed by cluster row id, carrying both the total
|
||||
// and how many rows need attention so the Overview can render the v5-style
|
||||
// healthy/total fractions. The Overview clusters table calls this instead
|
||||
// of computing counts inline, so the same rollup is testable and reusable
|
||||
// by other consumers (incident rollups, future Overview "active issues"
|
||||
// cards).
|
||||
export function buildKubernetesClusterChildCounts(
|
||||
resources: readonly Resource[],
|
||||
clusters: readonly Resource[],
|
||||
@@ -758,13 +778,13 @@ export function buildKubernetesClusterChildCounts(
|
||||
const bucket = counts.get(cluster.id);
|
||||
if (!bucket) continue;
|
||||
if (resource.type === 'k8s-node') {
|
||||
bucket.nodes += 1;
|
||||
tallyKubernetesChild(bucket.nodes, mapKubernetesNodeStatus(resource));
|
||||
} else if (resource.type === 'agent' && resource.sources?.includes('kubernetes')) {
|
||||
bucket.nodes += 1;
|
||||
tallyKubernetesChild(bucket.nodes, mapKubernetesNodeStatus(resource));
|
||||
} else if (resource.type === 'pod') {
|
||||
bucket.pods += 1;
|
||||
tallyKubernetesChild(bucket.pods, mapKubernetesPodStatus(resource));
|
||||
} else if (resource.type === 'k8s-deployment') {
|
||||
bucket.deployments += 1;
|
||||
tallyKubernetesChild(bucket.deployments, mapKubernetesDeploymentStatus(resource));
|
||||
}
|
||||
}
|
||||
return counts;
|
||||
|
||||
Reference in New Issue
Block a user