@skyhook-io/k8s-ui 1.14.4 → 1.14.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/charts/AreaChart.test.tsx +160 -0
- package/src/components/charts/AreaChart.tsx +191 -39
- package/src/components/charts/PrometheusChartsView.test.tsx +36 -0
- package/src/components/charts/PrometheusChartsView.tsx +193 -76
- package/src/components/charts/SeriesLegend.tsx +8 -3
- package/src/components/charts/annotations.ts +59 -0
- package/src/components/charts/axis.test.ts +32 -0
- package/src/components/charts/axis.ts +60 -0
- package/src/components/charts/colors.test.ts +27 -0
- package/src/components/charts/colors.ts +23 -0
- package/src/components/charts/format.test.ts +19 -0
- package/src/components/charts/format.ts +17 -1
- package/src/components/charts/index.ts +3 -1
- package/src/components/charts/types.ts +12 -0
- package/src/components/resources/HPADiagnosisSummary.test.tsx +178 -0
- package/src/components/resources/HPADiagnosisSummary.tsx +218 -0
- package/src/components/resources/index.ts +7 -0
- package/src/components/resources/renderers/HPARenderer.tsx +4 -57
- package/src/components/resources/renderers/WorkloadRenderer.tsx +4 -20
- package/src/components/topology/K8sResourceNode.test.ts +9 -0
- package/src/components/topology/K8sResourceNode.tsx +56 -5
- package/src/components/ui/Badge.tsx +5 -1
- package/src/components/ui/index.ts +5 -0
- package/src/components/ui/monacoRuntime.ts +14 -0
- package/src/monaco-deep.d.ts +10 -0
- package/src/types/core.ts +23 -10
|
@@ -6,6 +6,7 @@ export {
|
|
|
6
6
|
WORKLOAD_METRIC_CATEGORIES,
|
|
7
7
|
NODE_METRIC_CATEGORIES,
|
|
8
8
|
METRIC_TIME_RANGES,
|
|
9
|
+
describePodCoverage,
|
|
9
10
|
} from './PrometheusChartsView'
|
|
10
11
|
export type {
|
|
11
12
|
PrometheusChartsViewProps,
|
|
@@ -14,13 +15,14 @@ export type {
|
|
|
14
15
|
MetricCategoryDef,
|
|
15
16
|
PrometheusResourceMetricsResult,
|
|
16
17
|
} from './PrometheusChartsView'
|
|
17
|
-
export { SERIES_COLORS, seriesColor, seriesFill, computeShortLabels } from './colors'
|
|
18
|
+
export { SERIES_COLORS, seriesColor, seriesFill, computeShortLabels, seriesDisplayLabels } from './colors'
|
|
18
19
|
export { formatMetricValue, formatTimestamp } from './format'
|
|
19
20
|
export { computeSaturation } from './saturation'
|
|
20
21
|
export type {
|
|
21
22
|
TimeSeriesPoint,
|
|
22
23
|
TimeSeries,
|
|
23
24
|
ReferenceLine,
|
|
25
|
+
ChartAnnotation,
|
|
24
26
|
// Deprecated Prom-prefixed aliases — see types.ts.
|
|
25
27
|
PrometheusDataPoint,
|
|
26
28
|
PrometheusSeries,
|
|
@@ -35,3 +35,15 @@ export interface ReferenceLine {
|
|
|
35
35
|
label: string
|
|
36
36
|
kind: 'request' | 'limit'
|
|
37
37
|
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Vertical marker on a chart at one instant. `kind` names what Radar recorded
|
|
41
|
+
* there; the chart draws the marker and its label and never implies that the
|
|
42
|
+
* marked instant explains the series around it.
|
|
43
|
+
*/
|
|
44
|
+
export interface ChartAnnotation {
|
|
45
|
+
/** Unix seconds, the same clock as {@link TimeSeriesPoint.timestamp}. */
|
|
46
|
+
timestamp: number
|
|
47
|
+
label: string
|
|
48
|
+
kind: 'change'
|
|
49
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { renderToString } from "react-dom/server";
|
|
3
|
+
import {
|
|
4
|
+
HPADiagnosisSummary,
|
|
5
|
+
hpaBadgeSeverity,
|
|
6
|
+
hpaReasonGroups,
|
|
7
|
+
isHPAReasonRedundant,
|
|
8
|
+
} from "./HPADiagnosisSummary";
|
|
9
|
+
import type { HPADiagnosisState, HPADiagnosisView } from "../../types";
|
|
10
|
+
|
|
11
|
+
// React marks interpolation boundaries with empty comments; strip them so
|
|
12
|
+
// assertions can quote the sentence a reader actually sees.
|
|
13
|
+
const render = (node: Parameters<typeof renderToString>[0]) =>
|
|
14
|
+
renderToString(node).replace(/<!-- -->/g, "");
|
|
15
|
+
|
|
16
|
+
const maxed: HPADiagnosisView = {
|
|
17
|
+
state: "limited_max",
|
|
18
|
+
summary: "HPA wants more replicas but is capped at maxReplicas=10",
|
|
19
|
+
bounds: { min: 1, max: 10, current: 10, desired: 10 },
|
|
20
|
+
reasons: [
|
|
21
|
+
{
|
|
22
|
+
id: "limited_max",
|
|
23
|
+
message: "HPA is capped at maxReplicas=10",
|
|
24
|
+
detail:
|
|
25
|
+
"the desired replica count is more than the maximum replica count",
|
|
26
|
+
conditionType: "ScalingLimited",
|
|
27
|
+
conditionReason: "TooManyReplicas",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "missing_current_metric",
|
|
31
|
+
message: "HPA is missing current metric values",
|
|
32
|
+
detail: "cpu",
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
describe("hpaBadgeSeverity", () => {
|
|
38
|
+
const expected: Record<HPADiagnosisState, string> = {
|
|
39
|
+
ok: "success",
|
|
40
|
+
scaling_up: "warning",
|
|
41
|
+
scaling_down: "warning",
|
|
42
|
+
limited_max: "warning",
|
|
43
|
+
metrics_incomplete: "warning",
|
|
44
|
+
limited_min: "info",
|
|
45
|
+
scaled_to_zero: "info",
|
|
46
|
+
disabled: "info",
|
|
47
|
+
pinned: "info",
|
|
48
|
+
stabilized: "info",
|
|
49
|
+
metrics_unavailable: "error",
|
|
50
|
+
unable_to_scale: "error",
|
|
51
|
+
stale: "neutral",
|
|
52
|
+
unknown: "neutral",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
it.each(Object.entries(expected))(
|
|
56
|
+
"maps %s to the %s badge",
|
|
57
|
+
(state, severity) => {
|
|
58
|
+
expect(hpaBadgeSeverity(state as HPADiagnosisState)).toBe(severity);
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("isHPAReasonRedundant", () => {
|
|
64
|
+
it("treats the reason that named the state as a restatement", () => {
|
|
65
|
+
expect(isHPAReasonRedundant(maxed, maxed.reasons![0])).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("treats a reason whose message repeats the summary as a restatement", () => {
|
|
69
|
+
const echo = { id: "other", message: maxed.summary };
|
|
70
|
+
expect(isHPAReasonRedundant(maxed, echo)).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("keeps a reason that adds something the headline does not", () => {
|
|
74
|
+
expect(isHPAReasonRedundant(maxed, maxed.reasons![1])).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("splits reasons into restatements and additions", () => {
|
|
78
|
+
const groups = hpaReasonGroups(maxed);
|
|
79
|
+
expect(groups.redundant.map((reason) => reason.id)).toEqual([
|
|
80
|
+
"limited_max",
|
|
81
|
+
]);
|
|
82
|
+
expect(groups.additional.map((reason) => reason.id)).toEqual([
|
|
83
|
+
"missing_current_metric",
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("HPADiagnosisSummary", () => {
|
|
89
|
+
it("shows the detail variant evidence without repeating the headline", () => {
|
|
90
|
+
const html = render(
|
|
91
|
+
<HPADiagnosisSummary diagnosis={maxed} variant="detail" />,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(html).toContain(
|
|
95
|
+
"HPA wants more replicas but is capped at maxReplicas=10",
|
|
96
|
+
);
|
|
97
|
+
expect(html).toContain("10/10 replicas, bounds 1-10");
|
|
98
|
+
expect(html).toContain("Maxed");
|
|
99
|
+
expect(html).toContain("Evidence");
|
|
100
|
+
expect(html).toContain("limited max");
|
|
101
|
+
expect(html).toContain("ScalingLimited");
|
|
102
|
+
expect(html).toContain("TooManyReplicas");
|
|
103
|
+
expect(html).not.toContain("HPA is capped at maxReplicas=10");
|
|
104
|
+
expect(html).toContain("HPA is missing current metric values");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// `detail` is the controller's sentence only on a condition-built reason.
|
|
108
|
+
// On the others it is Radar's own — a metric-name list, a note about an
|
|
109
|
+
// unobserved generation — and quoting it as Kubernetes invents a quote.
|
|
110
|
+
it("never attributes Radar's own detail to Kubernetes", () => {
|
|
111
|
+
const radarDetail: HPADiagnosisView = {
|
|
112
|
+
state: "stale_status",
|
|
113
|
+
summary: "HPA status has not observed the latest spec generation yet",
|
|
114
|
+
bounds: { min: 1, max: 10, current: 3, desired: 3 },
|
|
115
|
+
reasons: [
|
|
116
|
+
{
|
|
117
|
+
id: "stale_status",
|
|
118
|
+
message: "HPA status has not observed the latest spec generation yet",
|
|
119
|
+
detail: "the controller has recorded no status for generation 7",
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "missing_current_metric",
|
|
123
|
+
message:
|
|
124
|
+
"HPA status is missing current values for one or more configured metrics",
|
|
125
|
+
detail: "cpu, memory",
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
} as unknown as HPADiagnosisView;
|
|
129
|
+
|
|
130
|
+
for (const variant of ["detail", "inline"] as const) {
|
|
131
|
+
const html = render(
|
|
132
|
+
<HPADiagnosisSummary diagnosis={radarDetail} variant={variant} />,
|
|
133
|
+
);
|
|
134
|
+
expect(html, variant).toContain(
|
|
135
|
+
"the controller has recorded no status for generation 7",
|
|
136
|
+
);
|
|
137
|
+
expect(html, variant).toContain("cpu, memory");
|
|
138
|
+
expect(html, variant).not.toContain("Kubernetes");
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("attributes the controller condition in the inline variant and lists the rest", () => {
|
|
143
|
+
const html = render(
|
|
144
|
+
<HPADiagnosisSummary
|
|
145
|
+
diagnosis={maxed}
|
|
146
|
+
variant="inline"
|
|
147
|
+
header={
|
|
148
|
+
<span data-testid="host-header">HorizontalPodAutoscaler api-hpa</span>
|
|
149
|
+
}
|
|
150
|
+
/>,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
expect(html).toContain("HorizontalPodAutoscaler api-hpa");
|
|
154
|
+
expect(html).toContain("Maxed");
|
|
155
|
+
expect(html).toContain("10/10 replicas · bounds 1-10");
|
|
156
|
+
expect(html).toContain(
|
|
157
|
+
"HPA wants more replicas but is capped at maxReplicas=10",
|
|
158
|
+
);
|
|
159
|
+
expect(html).not.toContain("HPA is capped at maxReplicas=10");
|
|
160
|
+
expect(html).toContain(
|
|
161
|
+
"Kubernetes ScalingLimited · TooManyReplicas: “the desired replica count is more than the maximum replica count”",
|
|
162
|
+
);
|
|
163
|
+
expect(html).toContain("HPA is missing current metric values");
|
|
164
|
+
expect(html).toContain("· cpu");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("omits the bounds line when the producer withheld bounds", () => {
|
|
168
|
+
const html = render(
|
|
169
|
+
<HPADiagnosisSummary
|
|
170
|
+
diagnosis={{ state: "ok", summary: "HPA is tracking its target" }}
|
|
171
|
+
variant="inline"
|
|
172
|
+
/>,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
expect(html).toContain("HPA is tracking its target");
|
|
176
|
+
expect(html).not.toContain("replicas");
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Badge, type BadgeSeverity } from "../ui/Badge";
|
|
3
|
+
import { StatusDot } from "../ui/status-tone";
|
|
4
|
+
import { hpaStateLabel, hpaStateLevel } from "./resource-utils-hpa";
|
|
5
|
+
import type {
|
|
6
|
+
HPADiagnosisState,
|
|
7
|
+
HPADiagnosisView,
|
|
8
|
+
HPAReasonSummary,
|
|
9
|
+
} from "../../types";
|
|
10
|
+
|
|
11
|
+
export function hpaBadgeSeverity(state: HPADiagnosisState): BadgeSeverity {
|
|
12
|
+
switch (hpaStateLevel(state)) {
|
|
13
|
+
case "healthy":
|
|
14
|
+
return "success";
|
|
15
|
+
case "unhealthy":
|
|
16
|
+
return "error";
|
|
17
|
+
case "degraded":
|
|
18
|
+
return "warning";
|
|
19
|
+
case "alert":
|
|
20
|
+
return "alert";
|
|
21
|
+
case "neutral":
|
|
22
|
+
return "info";
|
|
23
|
+
default:
|
|
24
|
+
return "neutral";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A reason that named the state, or repeats the summary verbatim, says the
|
|
30
|
+
* headline over again — only the controller's own sentence behind it (the
|
|
31
|
+
* reason's detail) adds anything a reader does not already have.
|
|
32
|
+
*/
|
|
33
|
+
export function isHPAReasonRedundant(
|
|
34
|
+
diagnosis: HPADiagnosisView,
|
|
35
|
+
reason: HPAReasonSummary,
|
|
36
|
+
): boolean {
|
|
37
|
+
return reason.id === diagnosis.state || reason.message === diagnosis.summary;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface HPAReasonGroups {
|
|
41
|
+
/** Reasons whose message restates the headline; render their detail, not their message. */
|
|
42
|
+
redundant: HPAReasonSummary[];
|
|
43
|
+
/** Reasons that carry something the headline does not. */
|
|
44
|
+
additional: HPAReasonSummary[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function hpaReasonGroups(diagnosis: HPADiagnosisView): HPAReasonGroups {
|
|
48
|
+
const reasons = diagnosis.reasons ?? [];
|
|
49
|
+
const redundant = reasons.filter((reason) =>
|
|
50
|
+
isHPAReasonRedundant(diagnosis, reason),
|
|
51
|
+
);
|
|
52
|
+
return {
|
|
53
|
+
redundant,
|
|
54
|
+
additional: reasons.filter((reason) => !redundant.includes(reason)),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatReasonID(id: string): string {
|
|
59
|
+
return id.replace(/_/g, " ");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface HPADiagnosisSummaryProps {
|
|
63
|
+
diagnosis: HPADiagnosisView;
|
|
64
|
+
/**
|
|
65
|
+
* `detail` — the HPA detail page: a boxed Evidence block per reason.
|
|
66
|
+
* `inline` — an evidence card on the workload the HPA scales: the headline
|
|
67
|
+
* once, then the controller's condition quoted and attributed.
|
|
68
|
+
*/
|
|
69
|
+
variant: "detail" | "inline";
|
|
70
|
+
/**
|
|
71
|
+
* Host-rendered identity for the `inline` variant (the scaler's name, owner
|
|
72
|
+
* links) placed before the state indicator. Navigation stays with the host.
|
|
73
|
+
*/
|
|
74
|
+
header?: ReactNode;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function HPADiagnosisSummary({
|
|
78
|
+
diagnosis,
|
|
79
|
+
variant,
|
|
80
|
+
header,
|
|
81
|
+
}: HPADiagnosisSummaryProps) {
|
|
82
|
+
const { redundant, additional } = hpaReasonGroups(diagnosis);
|
|
83
|
+
const bounds = diagnosis.bounds;
|
|
84
|
+
|
|
85
|
+
if (variant === "inline") {
|
|
86
|
+
return (
|
|
87
|
+
<div className="space-y-1 text-xs">
|
|
88
|
+
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
|
|
89
|
+
{header}
|
|
90
|
+
<span className="inline-flex items-center gap-1.5 text-theme-text-secondary">
|
|
91
|
+
<StatusDot
|
|
92
|
+
tone={hpaStateLevel(diagnosis.state)}
|
|
93
|
+
className="shrink-0"
|
|
94
|
+
/>
|
|
95
|
+
{hpaStateLabel(diagnosis.state)}
|
|
96
|
+
</span>
|
|
97
|
+
{bounds ? (
|
|
98
|
+
<span className="font-mono tabular-nums text-theme-text-tertiary">
|
|
99
|
+
{bounds.current}/{bounds.desired} replicas · bounds {bounds.min}-
|
|
100
|
+
{bounds.max}
|
|
101
|
+
</span>
|
|
102
|
+
) : null}
|
|
103
|
+
</div>
|
|
104
|
+
<p className="text-theme-text-secondary">{diagnosis.summary}</p>
|
|
105
|
+
{redundant.map((reason) =>
|
|
106
|
+
reason.detail ? (
|
|
107
|
+
<p
|
|
108
|
+
key={`${reason.id}-${reason.detail}`}
|
|
109
|
+
className="text-theme-text-tertiary"
|
|
110
|
+
>
|
|
111
|
+
{/* Same rule as the detail variant: only a condition-built
|
|
112
|
+
reason carries the controller's own sentence. */}
|
|
113
|
+
{reason.conditionType ? (
|
|
114
|
+
<>
|
|
115
|
+
Kubernetes {reason.conditionType}
|
|
116
|
+
{reason.conditionReason ? ` · ${reason.conditionReason}` : ""}
|
|
117
|
+
: “{reason.detail}”
|
|
118
|
+
</>
|
|
119
|
+
) : (
|
|
120
|
+
reason.detail
|
|
121
|
+
)}
|
|
122
|
+
</p>
|
|
123
|
+
) : null,
|
|
124
|
+
)}
|
|
125
|
+
{additional.length > 0 ? (
|
|
126
|
+
<ul className="list-disc space-y-0.5 pl-4 text-theme-text-secondary marker:text-theme-text-tertiary">
|
|
127
|
+
{additional.map((reason) => (
|
|
128
|
+
<li key={`${reason.id}-${reason.message}`}>
|
|
129
|
+
{reason.message}
|
|
130
|
+
{reason.detail ? (
|
|
131
|
+
<span className="text-theme-text-tertiary">
|
|
132
|
+
{" "}
|
|
133
|
+
· {reason.detail}
|
|
134
|
+
</span>
|
|
135
|
+
) : null}
|
|
136
|
+
</li>
|
|
137
|
+
))}
|
|
138
|
+
</ul>
|
|
139
|
+
) : null}
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const reasons = diagnosis.reasons ?? [];
|
|
145
|
+
return (
|
|
146
|
+
<div className="card-inner space-y-3">
|
|
147
|
+
<div className="flex flex-wrap items-start justify-between gap-2">
|
|
148
|
+
<div className="min-w-0">
|
|
149
|
+
<div className="text-sm font-medium text-theme-text-primary">
|
|
150
|
+
{diagnosis.summary}
|
|
151
|
+
</div>
|
|
152
|
+
{bounds ? (
|
|
153
|
+
<div className="mt-1 text-xs text-theme-text-secondary">
|
|
154
|
+
{bounds.current}/{bounds.desired} replicas, bounds {bounds.min}-
|
|
155
|
+
{bounds.max}
|
|
156
|
+
</div>
|
|
157
|
+
) : null}
|
|
158
|
+
</div>
|
|
159
|
+
<Badge severity={hpaBadgeSeverity(diagnosis.state)}>
|
|
160
|
+
{hpaStateLabel(diagnosis.state)}
|
|
161
|
+
</Badge>
|
|
162
|
+
</div>
|
|
163
|
+
{reasons.length > 0 && (
|
|
164
|
+
<div className="space-y-2">
|
|
165
|
+
{reasons.map((reason) => (
|
|
166
|
+
<div
|
|
167
|
+
key={`${reason.id}-${reason.message}`}
|
|
168
|
+
className="rounded border border-theme-border bg-theme-surface p-2"
|
|
169
|
+
>
|
|
170
|
+
<div className="flex flex-wrap items-center gap-2 text-xs">
|
|
171
|
+
<span className="font-medium text-theme-text-primary">
|
|
172
|
+
Evidence
|
|
173
|
+
</span>
|
|
174
|
+
<span className="text-theme-text-tertiary">
|
|
175
|
+
{formatReasonID(reason.id)}
|
|
176
|
+
</span>
|
|
177
|
+
{reason.conditionType && (
|
|
178
|
+
<span className="text-theme-text-tertiary">
|
|
179
|
+
{reason.conditionType}
|
|
180
|
+
</span>
|
|
181
|
+
)}
|
|
182
|
+
{reason.conditionReason && (
|
|
183
|
+
<span className="text-theme-text-tertiary">
|
|
184
|
+
{reason.conditionReason}
|
|
185
|
+
</span>
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
{/* Radar's reading leads, as the reason is built to; the
|
|
189
|
+
controller's own sentence follows it, quoted and attributed
|
|
190
|
+
the same way the inline variant attributes it. Unlabelled
|
|
191
|
+
beside the condition chips, it read as Radar's words. */}
|
|
192
|
+
{!isHPAReasonRedundant(diagnosis, reason) && (
|
|
193
|
+
<div className="mt-1 text-xs text-theme-text-secondary">
|
|
194
|
+
{reason.message}
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
{reason.detail &&
|
|
198
|
+
// `detail` is the controller's own sentence only on a reason
|
|
199
|
+
// built from a condition, which is the only kind that carries
|
|
200
|
+
// a condition type. Elsewhere it is Radar's — a list of metric
|
|
201
|
+
// names, a note about an unobserved generation — and quoting
|
|
202
|
+
// that as Kubernetes would put words in its mouth.
|
|
203
|
+
(reason.conditionType ? (
|
|
204
|
+
<p className="mt-1 text-xs text-theme-text-tertiary">
|
|
205
|
+
Kubernetes: “{reason.detail}”
|
|
206
|
+
</p>
|
|
207
|
+
) : (
|
|
208
|
+
<p className="mt-1 text-xs text-theme-text-tertiary">
|
|
209
|
+
{reason.detail}
|
|
210
|
+
</p>
|
|
211
|
+
))}
|
|
212
|
+
</div>
|
|
213
|
+
))}
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
export * from './resource-utils'
|
|
2
2
|
export * from './resource-utils-hpa'
|
|
3
|
+
export {
|
|
4
|
+
HPADiagnosisSummary,
|
|
5
|
+
hpaBadgeSeverity,
|
|
6
|
+
hpaReasonGroups,
|
|
7
|
+
isHPAReasonRedundant,
|
|
8
|
+
} from './HPADiagnosisSummary'
|
|
9
|
+
export type { HPADiagnosisSummaryProps, HPAReasonGroups } from './HPADiagnosisSummary'
|
|
3
10
|
export * from './resource-utils-argo'
|
|
4
11
|
export * from './resource-utils-certmanager'
|
|
5
12
|
export * from './resource-utils-cnpg'
|
|
@@ -2,11 +2,11 @@ import type { ReactNode } from 'react'
|
|
|
2
2
|
import { Activity, Cpu } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import { Section, PropertyList, Property, ConditionsSection, ResourceLink, type ConditionTone } from '../../ui/drawer-components'
|
|
5
|
-
import { Badge
|
|
5
|
+
import { Badge } from '../../ui/Badge'
|
|
6
6
|
import { kindToPlural } from '../../../utils/navigation'
|
|
7
7
|
import { formatAge } from '../resource-utils'
|
|
8
|
-
import {
|
|
9
|
-
import type { HPADiagnosis
|
|
8
|
+
import { HPADiagnosisSummary } from '../HPADiagnosisSummary'
|
|
9
|
+
import type { HPADiagnosis } from '../../../types'
|
|
10
10
|
|
|
11
11
|
interface HPARendererProps {
|
|
12
12
|
data: any
|
|
@@ -16,23 +16,6 @@ interface HPARendererProps {
|
|
|
16
16
|
extraSections?: ReactNode
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function hpaBadgeSeverity(state: HPADiagnosisState): BadgeSeverity {
|
|
20
|
-
switch (hpaStateLevel(state)) {
|
|
21
|
-
case 'healthy':
|
|
22
|
-
return 'success'
|
|
23
|
-
case 'unhealthy':
|
|
24
|
-
return 'error'
|
|
25
|
-
case 'degraded':
|
|
26
|
-
return 'warning'
|
|
27
|
-
case 'alert':
|
|
28
|
-
return 'alert'
|
|
29
|
-
case 'neutral':
|
|
30
|
-
return 'info'
|
|
31
|
-
default:
|
|
32
|
-
return 'neutral'
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
19
|
function hpaConditionTone(condition: any): ConditionTone | undefined {
|
|
37
20
|
if (condition?.type === 'ScaledToZero') {
|
|
38
21
|
if (condition.status === 'False') return 'ok'
|
|
@@ -50,14 +33,6 @@ function hpaConditionTone(condition: any): ConditionTone | undefined {
|
|
|
50
33
|
return 'warning'
|
|
51
34
|
}
|
|
52
35
|
|
|
53
|
-
function formatReasonID(id: string): string {
|
|
54
|
-
return id.replace(/_/g, ' ')
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function isReasonMessageRedundant(state: HPADiagnosisState, reason: NonNullable<HPADiagnosis['reasons']>[number]): boolean {
|
|
58
|
-
return state === 'limited_max' && reason.id === 'limited_max'
|
|
59
|
-
}
|
|
60
|
-
|
|
61
36
|
export function HPARenderer({ data, onNavigate, hpaDiagnosis, extraSections }: HPARendererProps) {
|
|
62
37
|
const status = data.status || {}
|
|
63
38
|
const spec = data.spec || {}
|
|
@@ -67,35 +42,7 @@ export function HPARenderer({ data, onNavigate, hpaDiagnosis, extraSections }: H
|
|
|
67
42
|
<>
|
|
68
43
|
{hpaDiagnosis && (
|
|
69
44
|
<Section title="Diagnosis" icon={Activity}>
|
|
70
|
-
<
|
|
71
|
-
<div className="flex flex-wrap items-start justify-between gap-2">
|
|
72
|
-
<div className="min-w-0">
|
|
73
|
-
<div className="text-sm font-medium text-theme-text-primary">{hpaDiagnosis.summary}</div>
|
|
74
|
-
<div className="mt-1 text-xs text-theme-text-secondary">
|
|
75
|
-
{hpaDiagnosis.bounds.current}/{hpaDiagnosis.bounds.desired} replicas, bounds {hpaDiagnosis.bounds.min}-{hpaDiagnosis.bounds.max}
|
|
76
|
-
</div>
|
|
77
|
-
</div>
|
|
78
|
-
<Badge severity={hpaBadgeSeverity(hpaDiagnosis.state)}>{hpaStateLabel(hpaDiagnosis.state)}</Badge>
|
|
79
|
-
</div>
|
|
80
|
-
{hpaDiagnosis.reasons && hpaDiagnosis.reasons.length > 0 && (
|
|
81
|
-
<div className="space-y-2">
|
|
82
|
-
{hpaDiagnosis.reasons.map((reason) => (
|
|
83
|
-
<div key={`${reason.id}-${reason.message}`} className="rounded border border-theme-border bg-theme-surface p-2">
|
|
84
|
-
<div className="flex flex-wrap items-center gap-2 text-xs">
|
|
85
|
-
<span className="font-medium text-theme-text-primary">Evidence</span>
|
|
86
|
-
<span className="text-theme-text-tertiary">{formatReasonID(reason.id)}</span>
|
|
87
|
-
{reason.conditionType && <span className="text-theme-text-tertiary">{reason.conditionType}</span>}
|
|
88
|
-
{reason.conditionReason && <span className="text-theme-text-tertiary">{reason.conditionReason}</span>}
|
|
89
|
-
{reason.detail && <span className="text-theme-text-tertiary">{reason.detail}</span>}
|
|
90
|
-
</div>
|
|
91
|
-
{!isReasonMessageRedundant(hpaDiagnosis.state, reason) && (
|
|
92
|
-
<div className="mt-1 text-xs text-theme-text-secondary">{reason.message}</div>
|
|
93
|
-
)}
|
|
94
|
-
</div>
|
|
95
|
-
))}
|
|
96
|
-
</div>
|
|
97
|
-
)}
|
|
98
|
-
</div>
|
|
45
|
+
<HPADiagnosisSummary diagnosis={hpaDiagnosis} variant="detail" />
|
|
99
46
|
</Section>
|
|
100
47
|
)}
|
|
101
48
|
|
|
@@ -6,11 +6,12 @@ import type { PolicyResourceResponse } from '../../../types/policy'
|
|
|
6
6
|
import { Section, PropertyList, Property, ConditionsSection, PodTemplateSection, AlertBanner, ResourceLink, ResourceRefBadge, useOperationalIssuesShown } from '../../ui/drawer-components'
|
|
7
7
|
import { DialogPortal } from '../../ui/DialogPortal'
|
|
8
8
|
import { Tooltip } from '../../ui/Tooltip'
|
|
9
|
-
import { Badge
|
|
9
|
+
import { Badge } from '../../ui/Badge'
|
|
10
10
|
import type { RBACSubjectResponse, RBACPolicyRule, ResourceRef, HPADiagnosis, WorkloadPodInfo } from '../../../types'
|
|
11
11
|
import { detectBlastRadius, rulePermissivenessScore } from '../../../utils/rbac-blast-radius'
|
|
12
12
|
import { RBACErrorSection, isRBACUnavailable } from './RBACErrorSection'
|
|
13
|
-
import { hpaStateLabel
|
|
13
|
+
import { hpaStateLabel } from '../resource-utils-hpa'
|
|
14
|
+
import { hpaBadgeSeverity } from '../HPADiagnosisSummary'
|
|
14
15
|
import {
|
|
15
16
|
rbacVerbBadgeClass,
|
|
16
17
|
rbacResourceBadgeClass,
|
|
@@ -407,29 +408,12 @@ function ScalerDiagnosisRow({ entry }: { entry: ScalerDiagnosis }) {
|
|
|
407
408
|
if (!entry.diagnosis) return null
|
|
408
409
|
return (
|
|
409
410
|
<div className="rounded border border-theme-border bg-theme-surface px-2 py-1.5 text-xs leading-6 text-theme-text-secondary whitespace-normal break-normal">
|
|
410
|
-
<Badge severity={
|
|
411
|
+
<Badge severity={hpaBadgeSeverity(entry.diagnosis.state)} size="sm" className="mr-1.5 align-middle">{hpaStateLabel(entry.diagnosis.state)}</Badge>
|
|
411
412
|
<span className="align-middle whitespace-normal break-normal">{compactHPASummary(entry.diagnosis)}</span>
|
|
412
413
|
</div>
|
|
413
414
|
)
|
|
414
415
|
}
|
|
415
416
|
|
|
416
|
-
function badgeSeverityForHPA(diagnosis: HPADiagnosis): BadgeSeverity {
|
|
417
|
-
switch (hpaStateLevel(diagnosis.state)) {
|
|
418
|
-
case 'healthy':
|
|
419
|
-
return 'success'
|
|
420
|
-
case 'unhealthy':
|
|
421
|
-
return 'error'
|
|
422
|
-
case 'degraded':
|
|
423
|
-
return 'warning'
|
|
424
|
-
case 'alert':
|
|
425
|
-
return 'alert'
|
|
426
|
-
case 'neutral':
|
|
427
|
-
return 'info'
|
|
428
|
-
default:
|
|
429
|
-
return 'neutral'
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
417
|
// ============================================================================
|
|
434
418
|
// WORKLOAD PERMISSIONS SECTION
|
|
435
419
|
// ============================================================================
|
|
@@ -9,6 +9,15 @@ describe('baseSubtitle', () => {
|
|
|
9
9
|
expect(baseSubtitle('ServiceMonitor', { endpointCount: 1 })).toBe('1 scrape endpoint')
|
|
10
10
|
})
|
|
11
11
|
|
|
12
|
+
it('describes a Service by its first port, capping additional ports to a count', () => {
|
|
13
|
+
expect(baseSubtitle('Service', { type: 'ClusterIP', ports: [] })).toBe('ClusterIP')
|
|
14
|
+
expect(baseSubtitle('Service', { type: 'ClusterIP', ports: [{ port: 80 }] })).toBe('ClusterIP :80')
|
|
15
|
+
expect(baseSubtitle('Service', {
|
|
16
|
+
type: 'ClusterIP',
|
|
17
|
+
ports: [{ port: 80 }, { port: 443 }, { port: 9153 }],
|
|
18
|
+
})).toBe('ClusterIP :80 +2 more')
|
|
19
|
+
})
|
|
20
|
+
|
|
12
21
|
it('describes a Calico staged deletion as a removal, not as selecting everything', () => {
|
|
13
22
|
// A staged deletion carries no selector, because the Calico API forbids one.
|
|
14
23
|
// Falling through to the empty-selector default would label the broadest
|