@skyhook-io/k8s-ui 1.8.6 → 1.8.8
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 +3 -3
- package/src/components/applications/ApplicationsList.tsx +5 -2
- package/src/components/applications/ApplicationsView.tsx +6 -1
- package/src/components/audit/AuditAlerts.tsx +4 -0
- package/src/components/audit/AuditBadgeTooltip.test.tsx +30 -0
- package/src/components/audit/AuditBadgeTooltip.tsx +47 -0
- package/src/components/audit/AuditFindingsTable.tsx +4 -0
- package/src/components/audit/index.ts +1 -0
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +27 -9
- package/src/components/gitops/GitOpsDetailLayout.tsx +3 -3
- package/src/components/gitops/GitOpsStatusBadge.tsx +9 -3
- package/src/components/gitops/GitOpsTableView.tsx +49 -46
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +12 -5
- package/src/components/issues/IssuesView.tsx +49 -40
- package/src/components/issues/ResourceIssuesSection.tsx +145 -0
- package/src/components/issues/diagnostic.ts +86 -0
- package/src/components/issues/index.ts +2 -1
- package/src/components/issues/issues.test.ts +21 -0
- package/src/components/issues/severity.ts +10 -9
- package/src/components/issues/types.ts +23 -0
- package/src/components/namespace-switcher/NamespacePicker.tsx +381 -0
- package/src/components/namespace-switcher/index.ts +6 -0
- package/src/components/resources/ResourcesView.tsx +58 -83
- package/src/components/resources/cron-to-human.test.ts +41 -0
- package/src/components/resources/get-pod-problems.test.ts +18 -0
- package/src/components/resources/health-golden.test.ts +66 -0
- package/src/components/resources/renderers/JobRenderer.tsx +6 -2
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -2
- package/src/components/resources/renderers/NodeRenderer.tsx +17 -8
- package/src/components/resources/renderers/PVCRenderer.tsx +7 -7
- package/src/components/resources/renderers/PodRenderer.tsx +28 -9
- package/src/components/resources/renderers/ServiceRenderer.tsx +23 -9
- package/src/components/resources/renderers/WorkloadRenderer.tsx +6 -3
- package/src/components/resources/resource-utils-argo.test.ts +23 -0
- package/src/components/resources/resource-utils-argo.ts +5 -1
- package/src/components/resources/resource-utils-keda.ts +12 -8
- package/src/components/resources/resource-utils.ts +34 -14
- package/src/components/scope-pill/ScopePill.tsx +35 -0
- package/src/components/scope-pill/index.ts +2 -0
- package/src/components/timeline/TimelineList.tsx +27 -1
- package/src/components/timeline/TimelineSwimlanes.tsx +1 -0
- package/src/components/timeline/shared.tsx +15 -4
- package/src/components/topology/K8sResourceNode.tsx +28 -1
- package/src/components/topology/TopologyControls.tsx +90 -14
- package/src/components/topology/layout.ts +11 -5
- package/src/components/ui/FreshnessControl.tsx +153 -0
- package/src/components/ui/PaneLoader.tsx +24 -6
- package/src/components/ui/SortableTh.tsx +16 -10
- package/src/components/ui/Toast.tsx +1 -1
- package/src/components/ui/drawer-components.test.tsx +35 -0
- package/src/components/ui/drawer-components.tsx +13 -1
- package/src/components/ui/index.ts +2 -0
- package/src/components/workload/ResourceDetailDrawer.tsx +215 -32
- package/src/components/workload/WorkloadView.tsx +61 -13
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useKeyboardShortcuts.tsx +23 -2
- package/src/hooks/useRefreshAnimation.ts +15 -2
- package/src/index.ts +8 -0
- package/src/types/core.ts +142 -3
- package/src/types/gitops-insights.ts +4 -0
- package/src/utils/animation.ts +10 -0
- package/src/utils/applications.test.ts +55 -1
- package/src/utils/applications.ts +28 -7
- package/src/utils/badge-colors.ts +7 -0
- package/src/utils/format-freshness.test.ts +34 -0
- package/src/utils/format.ts +32 -0
- package/src/utils/resource-hierarchy.test.ts +51 -0
- package/src/utils/resource-hierarchy.ts +7 -4
|
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState, type ComponentType, type ReactNode } from
|
|
|
2
2
|
import { ChevronRight, CircleCheck, Clock, ExternalLink } from 'lucide-react';
|
|
3
3
|
import { ClusterName, EmptyState } from '../ui';
|
|
4
4
|
import { formatCompactAge, formatRelativeAgeTime } from '../../utils/format';
|
|
5
|
+
import { diagnosticRoleLabel, diagnosticFactLabel, confidenceTitle, incidentParentLabel } from './diagnostic';
|
|
5
6
|
import {
|
|
6
7
|
ISSUE_SEVERITY_BADGE_CLASS,
|
|
7
8
|
ISSUE_SEVERITY_LABEL,
|
|
@@ -208,6 +209,17 @@ export function IssueRow({
|
|
|
208
209
|
<span className="shrink-0 tabular-nums">{affected}</span>
|
|
209
210
|
</>
|
|
210
211
|
) : null}
|
|
212
|
+
{issue.incident_parent ? (
|
|
213
|
+
<>
|
|
214
|
+
<span aria-hidden>·</span>
|
|
215
|
+
{/* Non-interactive signal (the header is the toggle — a nested
|
|
216
|
+
button would be invalid); the clickable link lives in the body. */}
|
|
217
|
+
<span className="min-w-0 truncate text-theme-text-tertiary" title={confidenceTitle(issue.incident_parent.confidence ?? '')}>
|
|
218
|
+
↳ {incidentParentLabel(issue.incident_parent.fact_type, issue.incident_parent.confidence)}{' '}
|
|
219
|
+
<span className="font-medium text-theme-text-secondary">{issue.incident_parent.ref.kind} / {issue.incident_parent.ref.name}</span>
|
|
220
|
+
</span>
|
|
221
|
+
</>
|
|
222
|
+
) : null}
|
|
211
223
|
{renderMeta?.(slotCtx)}
|
|
212
224
|
</div>
|
|
213
225
|
</div>
|
|
@@ -248,6 +260,21 @@ export function IssueRow({
|
|
|
248
260
|
<div className="border-t border-theme-border bg-theme-base/40 px-4 py-4 pl-11">
|
|
249
261
|
<div className="flex flex-col gap-4">
|
|
250
262
|
<Diagnosis issue={issue} />
|
|
263
|
+
{issue.incident_parent ? (
|
|
264
|
+
<section className="flex flex-col gap-1">
|
|
265
|
+
<h4 className="text-[11px] font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
266
|
+
{incidentParentLabel(issue.incident_parent.fact_type, issue.incident_parent.confidence)}
|
|
267
|
+
{issue.incident_parent.confidence ? (
|
|
268
|
+
<span className="ml-2 badge-sm text-[10px] font-normal text-theme-text-tertiary" title={confidenceTitle(issue.incident_parent.confidence)}>
|
|
269
|
+
{issue.incident_parent.confidence} confidence
|
|
270
|
+
</span>
|
|
271
|
+
) : null}
|
|
272
|
+
</h4>
|
|
273
|
+
<ul className="flex flex-col gap-px">
|
|
274
|
+
<ResourceLine refForLink={memberRef(issue, issue.incident_parent.ref)} resourceHref={resourceHref} onResourceClick={onResourceClick} ResourceLinkIcon={ResourceLinkIcon} />
|
|
275
|
+
</ul>
|
|
276
|
+
</section>
|
|
277
|
+
) : null}
|
|
251
278
|
<DiagnosticContext issue={issue} resourceHref={resourceHref} onResourceClick={onResourceClick} ResourceLinkIcon={ResourceLinkIcon} />
|
|
252
279
|
<div className="border-t border-theme-border/70 pt-3">
|
|
253
280
|
<AffectedResources issue={issue} resourceHref={resourceHref} onResourceClick={onResourceClick} ResourceLinkIcon={ResourceLinkIcon} />
|
|
@@ -273,7 +300,11 @@ function Diagnosis({ issue }: { issue: Issue }) {
|
|
|
273
300
|
const { headline, detail } = issueMessageParts(issue);
|
|
274
301
|
// When the issue carries a parsed plain-English cause, lead with it. The raw
|
|
275
302
|
// detector message is kept below as de-emphasized detail.
|
|
276
|
-
const
|
|
303
|
+
const visibleMessage = [headline, detail].filter(Boolean).join(' ');
|
|
304
|
+
const rawMessage = issue.raw_message ?? (issue.cause ? issue.message ?? '' : '');
|
|
305
|
+
const shouldShowRawMessage = issue.cause
|
|
306
|
+
? Boolean(rawMessage)
|
|
307
|
+
: Boolean(issue.raw_message && issue.raw_message !== visibleMessage);
|
|
277
308
|
return (
|
|
278
309
|
<section className="flex flex-col gap-1">
|
|
279
310
|
<h4 className="text-[11px] font-semibold uppercase tracking-wide text-theme-text-tertiary">What's wrong</h4>
|
|
@@ -308,9 +339,9 @@ function Diagnosis({ issue }: { issue: Issue }) {
|
|
|
308
339
|
{issue.operation_retry_count ? ` · retried ${issue.operation_retry_count}×` : ''}
|
|
309
340
|
</p>
|
|
310
341
|
) : null}
|
|
311
|
-
{/* Raw detector message, de-emphasized
|
|
312
|
-
|
|
313
|
-
{
|
|
342
|
+
{/* Raw detector message, de-emphasized so precise controller/kubelet text
|
|
343
|
+
remains available without leading the diagnosis. */}
|
|
344
|
+
{shouldShowRawMessage ? (
|
|
314
345
|
<p className="break-words font-mono text-[11px] leading-relaxed text-theme-text-tertiary">{rawMessage}</p>
|
|
315
346
|
) : null}
|
|
316
347
|
{crash ? <p className="text-xs text-theme-text-tertiary tabular-nums">{crash}</p> : null}
|
|
@@ -366,6 +397,14 @@ function DiagnosticContext({
|
|
|
366
397
|
<li key={`${fact.type}-${idx}`} className="flex flex-col gap-1.5 rounded-md border border-theme-border/70 px-2.5 py-2">
|
|
367
398
|
<div className="flex min-w-0 items-baseline gap-2">
|
|
368
399
|
<span className="shrink-0 text-xs font-medium text-theme-text-secondary">{diagnosticFactLabel(fact.type)}</span>
|
|
400
|
+
{fact.confidence ? (
|
|
401
|
+
<span
|
|
402
|
+
className="shrink-0 badge-sm text-[10px] text-theme-text-tertiary"
|
|
403
|
+
title={confidenceTitle(fact.confidence)}
|
|
404
|
+
>
|
|
405
|
+
{fact.confidence} confidence
|
|
406
|
+
</span>
|
|
407
|
+
) : null}
|
|
369
408
|
{fact.message ? <span className="min-w-0 break-words text-xs leading-relaxed text-theme-text-tertiary">{fact.message}</span> : null}
|
|
370
409
|
</div>
|
|
371
410
|
{fact.related_issues?.length ? (
|
|
@@ -375,6 +414,7 @@ function DiagnosticContext({
|
|
|
375
414
|
key={`${related.ref.group ?? ''}/${related.ref.kind}/${related.ref.namespace ?? ''}/${related.ref.name}#${relIdx}`}
|
|
376
415
|
label="Related"
|
|
377
416
|
refForLink={memberRef(issue, related.ref)}
|
|
417
|
+
count={related.count}
|
|
378
418
|
resourceHref={resourceHref}
|
|
379
419
|
onResourceClick={onResourceClick}
|
|
380
420
|
ResourceLinkIcon={ResourceLinkIcon}
|
|
@@ -402,42 +442,6 @@ function DiagnosticContext({
|
|
|
402
442
|
);
|
|
403
443
|
}
|
|
404
444
|
|
|
405
|
-
function diagnosticRoleLabel(role: string): string {
|
|
406
|
-
switch (role) {
|
|
407
|
-
case 'candidate':
|
|
408
|
-
return 'Candidate signal';
|
|
409
|
-
case 'affected':
|
|
410
|
-
return 'Affected signal';
|
|
411
|
-
case 'rollup':
|
|
412
|
-
return 'Rollup';
|
|
413
|
-
default:
|
|
414
|
-
return 'Context';
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function diagnosticFactLabel(type: string): string {
|
|
419
|
-
switch (type) {
|
|
420
|
-
case 'explicit_reference':
|
|
421
|
-
return 'Explicit reference';
|
|
422
|
-
case 'owner_rollup':
|
|
423
|
-
return 'Owner rollup';
|
|
424
|
-
case 'selected_backend_issue':
|
|
425
|
-
return 'Selected backend';
|
|
426
|
-
case 'service_config_mismatch':
|
|
427
|
-
return 'Service config';
|
|
428
|
-
case 'service_env_reference':
|
|
429
|
-
return 'Service env';
|
|
430
|
-
case 'probe_target_mismatch':
|
|
431
|
-
return 'Probe target';
|
|
432
|
-
case 'blocked_init_container':
|
|
433
|
-
return 'Init container';
|
|
434
|
-
case 'restart_cause':
|
|
435
|
-
return 'Restart cause';
|
|
436
|
-
default:
|
|
437
|
-
return type.replace(/_/g, ' ');
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
445
|
// Native-tooltip detail for the collapsed-row age chip: absolute first-seen + last-seen
|
|
442
446
|
// freshness, the two facts the compact "2h" hides.
|
|
443
447
|
function ageTitle(issue: Issue): string {
|
|
@@ -505,12 +509,14 @@ function AffectedResources({
|
|
|
505
509
|
function ResourceLine({
|
|
506
510
|
label,
|
|
507
511
|
refForLink,
|
|
512
|
+
count,
|
|
508
513
|
resourceHref,
|
|
509
514
|
onResourceClick,
|
|
510
515
|
ResourceLinkIcon,
|
|
511
516
|
}: {
|
|
512
517
|
label?: string;
|
|
513
518
|
refForLink: IssueResourceRef;
|
|
519
|
+
count?: number;
|
|
514
520
|
resourceHref?: (ref: IssueResourceRef) => string;
|
|
515
521
|
onResourceClick?: (ref: IssueResourceRef) => void;
|
|
516
522
|
ResourceLinkIcon: ComponentType<{ className?: string }>;
|
|
@@ -525,6 +531,9 @@ function ResourceLine({
|
|
|
525
531
|
{r.namespace ? `${r.namespace} / ` : ''}
|
|
526
532
|
{r.name}
|
|
527
533
|
</span>
|
|
534
|
+
{count && count > 1 ? (
|
|
535
|
+
<span className="shrink-0 text-[10px] text-theme-text-tertiary tabular-nums" title={`${count} affected resources grouped under this issue`}>{count} affected</span>
|
|
536
|
+
) : null}
|
|
528
537
|
{linkable && <ResourceLinkIcon className="h-3 w-3 shrink-0 text-theme-text-tertiary opacity-0 transition-opacity group-hover/r:opacity-100" />}
|
|
529
538
|
</>
|
|
530
539
|
);
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { AlertTriangle, ExternalLink } from 'lucide-react'
|
|
2
|
+
import { Section } from '../ui/drawer-components'
|
|
3
|
+
import { Badge } from '../ui/Badge'
|
|
4
|
+
import type { Issue, IssueResourceRef } from './types'
|
|
5
|
+
import { categoryLabel } from './severity'
|
|
6
|
+
import { diagnosticRoleLabel, diagnosticFactLabel, confidenceTitle } from './diagnostic'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* ResourceIssuesSection — the compact "Operational Issues" block for the resource
|
|
10
|
+
* detail. Renders the live, diagnosed Issues that touch one resource (its own +,
|
|
11
|
+
* for a workload, its owned pods' issues, server-rolled-up via RelatedIssues).
|
|
12
|
+
* The diagnosis sibling of IssuesView's queue row — same Cause/Action vocabulary,
|
|
13
|
+
* stripped of the queue chrome (accordion, scope copy, fan-out expansion).
|
|
14
|
+
*
|
|
15
|
+
* Header mirrors the queue: the plain `categoryLabel` is the operator-facing
|
|
16
|
+
* headline and the raw `reason` rides alongside as a muted signal (so the K8s
|
|
17
|
+
* jargon is available but not the lead). Body is intentionally just the plain
|
|
18
|
+
* `cause` (which names the offending object) + the `Next step` — the diagnosis
|
|
19
|
+
* and the fix. The raw `message`/evidence stays in the queue + MCP where the
|
|
20
|
+
* locator detail is wanted; inline it mostly restated the cause or the category,
|
|
21
|
+
* so it's omitted to keep the card scannable. (`message` is the body fallback
|
|
22
|
+
* only for categories that don't yet emit a `cause`.)
|
|
23
|
+
*/
|
|
24
|
+
export function ResourceIssuesSection({
|
|
25
|
+
issues,
|
|
26
|
+
onResourceClick,
|
|
27
|
+
}: {
|
|
28
|
+
issues: Issue[] | undefined
|
|
29
|
+
/** When provided, related resources in a causal link become clickable. */
|
|
30
|
+
onResourceClick?: (ref: IssueResourceRef) => void
|
|
31
|
+
}) {
|
|
32
|
+
if (!issues || issues.length === 0) return null
|
|
33
|
+
return (
|
|
34
|
+
<Section title={`Operational Issues (${issues.length})`} icon={AlertTriangle} defaultExpanded>
|
|
35
|
+
<div className="space-y-3">
|
|
36
|
+
{issues.map((issue) => {
|
|
37
|
+
return (
|
|
38
|
+
<div key={issue.id} className="card-inner">
|
|
39
|
+
<div className="mb-1 flex min-w-0 items-baseline gap-2">
|
|
40
|
+
<Badge severity={issue.severity === 'critical' ? 'error' : 'warning'} size="sm">
|
|
41
|
+
{issue.severity}
|
|
42
|
+
</Badge>
|
|
43
|
+
<span className="shrink-0 text-sm font-medium text-theme-text-primary">{categoryLabel(issue.category)}</span>
|
|
44
|
+
{issue.reason ? (
|
|
45
|
+
<span className="min-w-0 flex-1 truncate text-xs text-theme-text-tertiary">{issue.reason}</span>
|
|
46
|
+
) : null}
|
|
47
|
+
{issue.count ? (
|
|
48
|
+
<span className="shrink-0 text-xs text-theme-text-tertiary tabular-nums">· {issue.count} affected</span>
|
|
49
|
+
) : null}
|
|
50
|
+
</div>
|
|
51
|
+
{issue.cause ? (
|
|
52
|
+
<p className="text-sm leading-relaxed text-theme-text-secondary">{issue.cause}</p>
|
|
53
|
+
) : issue.message ? (
|
|
54
|
+
<p className="text-sm leading-relaxed text-theme-text-secondary">{issue.message}</p>
|
|
55
|
+
) : null}
|
|
56
|
+
{issue.action ? (
|
|
57
|
+
<p className="mt-1 text-sm leading-relaxed text-theme-text-secondary">
|
|
58
|
+
<span className="font-medium text-theme-text-primary">Next step: </span>
|
|
59
|
+
{issue.action}
|
|
60
|
+
</p>
|
|
61
|
+
) : null}
|
|
62
|
+
{issue.remediation_kind === 'create-namespace' && issue.remediation_target ? (
|
|
63
|
+
<p className="mt-1 text-xs text-theme-text-tertiary">
|
|
64
|
+
Suggested fix: create namespace{' '}
|
|
65
|
+
<code className="rounded bg-theme-elevated px-1 font-mono">{issue.remediation_target}</code> — apply it from the GitOps detail page.
|
|
66
|
+
</p>
|
|
67
|
+
) : null}
|
|
68
|
+
<CausalContext issue={issue} onResourceClick={onResourceClick} />
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
})}
|
|
72
|
+
</div>
|
|
73
|
+
</Section>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* CausalContext — the compact, drawer-density rendering of an issue's
|
|
79
|
+
* cross-subject causal links (DiagnosticContext). Shows only the linking facts
|
|
80
|
+
* (those carrying a confidence tier or related issues) — the queue's IssuesView
|
|
81
|
+
* renders the fuller context with clickable resource navigation; here the related
|
|
82
|
+
* resources are shown as plain identifiers to keep the resource panel scannable.
|
|
83
|
+
*/
|
|
84
|
+
function CausalContext({ issue, onResourceClick }: { issue: Issue; onResourceClick?: (ref: IssueResourceRef) => void }) {
|
|
85
|
+
const ctx = issue.diagnostic_context
|
|
86
|
+
const links = ctx?.facts?.filter((f) => f.confidence || (f.related_issues && f.related_issues.length > 0)) ?? []
|
|
87
|
+
if (!ctx || links.length === 0) return null
|
|
88
|
+
return (
|
|
89
|
+
<div className="mt-2 border-t border-theme-border/60 pt-2">
|
|
90
|
+
<div className="mb-1 flex items-center gap-2">
|
|
91
|
+
<span className="text-[10px] font-semibold uppercase tracking-wide text-theme-text-tertiary">Context</span>
|
|
92
|
+
{ctx.role ? <span className="badge-sm text-[10px] text-theme-text-secondary">{diagnosticRoleLabel(ctx.role)}</span> : null}
|
|
93
|
+
</div>
|
|
94
|
+
<ul className="space-y-1.5">
|
|
95
|
+
{links.map((fact, idx) => (
|
|
96
|
+
<li key={`${fact.type}-${idx}`} className="text-xs">
|
|
97
|
+
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
|
98
|
+
<span className="font-medium text-theme-text-secondary">{diagnosticFactLabel(fact.type)}</span>
|
|
99
|
+
{fact.confidence ? (
|
|
100
|
+
<span className="badge-sm text-[10px] text-theme-text-tertiary" title={confidenceTitle(fact.confidence)}>
|
|
101
|
+
{fact.confidence} confidence
|
|
102
|
+
</span>
|
|
103
|
+
) : null}
|
|
104
|
+
{fact.message ? <span className="text-theme-text-tertiary">{fact.message}</span> : null}
|
|
105
|
+
</div>
|
|
106
|
+
{fact.related_issues && fact.related_issues.length > 0 ? (
|
|
107
|
+
<ul className="mt-0.5 space-y-0.5 pl-3">
|
|
108
|
+
{fact.related_issues.map((rel, ri) => {
|
|
109
|
+
const label = (
|
|
110
|
+
<>
|
|
111
|
+
<span className="text-[10px] uppercase tracking-wide text-theme-text-tertiary">{rel.ref.kind}</span>{' '}
|
|
112
|
+
<span className="font-mono">
|
|
113
|
+
{rel.ref.namespace ? `${rel.ref.namespace} / ` : ''}
|
|
114
|
+
{rel.ref.name}
|
|
115
|
+
</span>
|
|
116
|
+
{rel.count && rel.count > 1 ? (
|
|
117
|
+
<span className="ml-1 tabular-nums" title={`${rel.count} affected resources grouped under this issue`}>· {rel.count} affected</span>
|
|
118
|
+
) : null}
|
|
119
|
+
</>
|
|
120
|
+
)
|
|
121
|
+
return (
|
|
122
|
+
<li key={ri} className="text-theme-text-tertiary">
|
|
123
|
+
{onResourceClick ? (
|
|
124
|
+
<button
|
|
125
|
+
type="button"
|
|
126
|
+
onClick={() => onResourceClick(rel.ref)}
|
|
127
|
+
className="group inline-flex items-center gap-1 text-left hover:text-theme-text-secondary"
|
|
128
|
+
>
|
|
129
|
+
{label}
|
|
130
|
+
<ExternalLink className="h-3 w-3 opacity-0 group-hover:opacity-100" />
|
|
131
|
+
</button>
|
|
132
|
+
) : (
|
|
133
|
+
label
|
|
134
|
+
)}
|
|
135
|
+
</li>
|
|
136
|
+
)
|
|
137
|
+
})}
|
|
138
|
+
</ul>
|
|
139
|
+
) : null}
|
|
140
|
+
</li>
|
|
141
|
+
))}
|
|
142
|
+
</ul>
|
|
143
|
+
</div>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Shared labels for rendering an issue's DiagnosticContext (the causal-link
|
|
2
|
+
// surface). Used by both the cluster Issues queue (IssuesView) and the
|
|
3
|
+
// per-resource Operational Issues block (ResourceIssuesSection) so the two
|
|
4
|
+
// can't drift on wording.
|
|
5
|
+
|
|
6
|
+
// Operator-facing — describes the issue's place in the causal picture in plain
|
|
7
|
+
// language, not Radar's internal role taxonomy.
|
|
8
|
+
export function diagnosticRoleLabel(role: string): string {
|
|
9
|
+
switch (role) {
|
|
10
|
+
case 'candidate':
|
|
11
|
+
return 'Possible cause';
|
|
12
|
+
case 'affected':
|
|
13
|
+
return 'Affected';
|
|
14
|
+
case 'rollup':
|
|
15
|
+
return 'Grouped';
|
|
16
|
+
default:
|
|
17
|
+
return 'Context';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Operator-facing fact labels — plain language over the implementation-shaped
|
|
22
|
+
// internal type names.
|
|
23
|
+
export function diagnosticFactLabel(type: string): string {
|
|
24
|
+
switch (type) {
|
|
25
|
+
case 'explicit_reference':
|
|
26
|
+
return 'Missing reference';
|
|
27
|
+
case 'owner_rollup':
|
|
28
|
+
return 'Grouped from pods';
|
|
29
|
+
case 'selected_backend_issue':
|
|
30
|
+
return 'Backend pods';
|
|
31
|
+
case 'service_config_mismatch':
|
|
32
|
+
return 'Service config';
|
|
33
|
+
case 'service_env_reference':
|
|
34
|
+
return 'Referenced service';
|
|
35
|
+
case 'probe_target_mismatch':
|
|
36
|
+
return 'Probe target';
|
|
37
|
+
case 'blocked_init_container':
|
|
38
|
+
return 'Init container';
|
|
39
|
+
case 'restart_cause':
|
|
40
|
+
return 'Restart evidence';
|
|
41
|
+
case 'node_blast_radius':
|
|
42
|
+
return 'Affected workloads';
|
|
43
|
+
case 'pvc_blast_radius':
|
|
44
|
+
return 'Blocked pods';
|
|
45
|
+
case 'apiservice_hpa':
|
|
46
|
+
return 'Stalled autoscalers';
|
|
47
|
+
case 'secret_not_ready':
|
|
48
|
+
return 'Dependent pods';
|
|
49
|
+
default:
|
|
50
|
+
return type.replace(/_/g, ' ');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Operator-facing lead-in for the symptom→root pointer chip. Honest per fact
|
|
55
|
+
// type: a declared PVC/Secret edge is a cause; a co-located node is only
|
|
56
|
+
// "related" (node pressure can be a shared victim, not the root), so it must not
|
|
57
|
+
// claim "caused by".
|
|
58
|
+
export function incidentParentLabel(factType?: string, confidence?: string): string {
|
|
59
|
+
switch (factType) {
|
|
60
|
+
case 'pvc_blast_radius':
|
|
61
|
+
case 'secret_not_ready':
|
|
62
|
+
return 'Caused by';
|
|
63
|
+
case 'apiservice_hpa':
|
|
64
|
+
return 'Likely cause';
|
|
65
|
+
case 'node_blast_radius':
|
|
66
|
+
return 'Related';
|
|
67
|
+
default:
|
|
68
|
+
return confidence === 'high' ? 'Caused by' : 'Possible cause';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Plain-language gloss for the confidence chip's tooltip — the operator should
|
|
73
|
+
// know a medium link is "these are co-located, the node may be the cause", not a
|
|
74
|
+
// proven fact.
|
|
75
|
+
export function confidenceTitle(confidence: string): string {
|
|
76
|
+
switch (confidence) {
|
|
77
|
+
case 'high':
|
|
78
|
+
return 'High confidence: a declared structural link (selector, owner, or claim reference).';
|
|
79
|
+
case 'medium':
|
|
80
|
+
return 'Medium confidence: these resources are related, but causation is inferred — verify before acting.';
|
|
81
|
+
case 'low':
|
|
82
|
+
return 'Low confidence: a heuristic match.';
|
|
83
|
+
default:
|
|
84
|
+
return '';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// names are safe to surface.
|
|
5
5
|
export { IssueRow, IssuesView } from './IssuesView';
|
|
6
6
|
export type { IssueRowProps, IssueRowSlotContext, IssuesViewProps } from './IssuesView';
|
|
7
|
+
export { ResourceIssuesSection } from './ResourceIssuesSection';
|
|
7
8
|
export {
|
|
8
9
|
ISSUE_SEVERITIES,
|
|
9
10
|
ISSUE_SEVERITY_RANK,
|
|
@@ -11,7 +12,7 @@ export {
|
|
|
11
12
|
subjectRef,
|
|
12
13
|
memberRef,
|
|
13
14
|
} from './types';
|
|
14
|
-
export type { Issue, IssueSeverity, IssueAffected, IssueResourceRef, IssueDiagnosticContext, IssueDiagnosticFact, IssueDiagnosticIssueRef, IssueDiagnosticRole, IssueChangeContext, IssueRecentChange, IssueRecentChangeField } from './types';
|
|
15
|
+
export type { Issue, IssueSeverity, IssueAffected, IssueResourceRef, IssueDiagnosticContext, IssueDiagnosticFact, IssueDiagnosticConfidence, IssueDiagnosticIssueRef, IssueDiagnosticRole, IssueIncidentParent, IssueChangeContext, IssueRecentChange, IssueRecentChangeField } from './types';
|
|
15
16
|
export {
|
|
16
17
|
ISSUE_SEVERITY_LABEL,
|
|
17
18
|
ISSUE_SEVERITY_BADGE_CLASS,
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { createElement } from 'react'
|
|
3
|
+
import { renderToString } from 'react-dom/server'
|
|
2
4
|
import { compareIssues, subjectRef, memberRef, normalizeImagePullMessage, issueMessageParts, type Issue } from './types'
|
|
3
5
|
import { categoryLabel, groupLabel, groupBadgeClass } from './severity'
|
|
6
|
+
import { IssueRow } from './IssuesView'
|
|
4
7
|
|
|
5
8
|
const base: Issue = {
|
|
6
9
|
id: 'id-0',
|
|
@@ -105,3 +108,21 @@ describe('image-pull message normalization', () => {
|
|
|
105
108
|
expect(parts.detail).toBe('')
|
|
106
109
|
})
|
|
107
110
|
})
|
|
111
|
+
|
|
112
|
+
describe('IssueRow diagnosis raw messages', () => {
|
|
113
|
+
it('shows raw_message when cleaned issue copy has no parsed cause', () => {
|
|
114
|
+
const issue = mk({
|
|
115
|
+
category: 'gitops_operation_failed',
|
|
116
|
+
category_group: 'configuration',
|
|
117
|
+
severity: 'critical',
|
|
118
|
+
reason: 'OperationFailed',
|
|
119
|
+
message: 'app path does not exist',
|
|
120
|
+
raw_message: 'rpc error: code = Unknown desc = app path does not exist',
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const html = renderToString(createElement(IssueRow, { issue, open: true, onToggle: () => undefined, as: 'div' }))
|
|
124
|
+
|
|
125
|
+
expect(html).toContain('app path does not exist')
|
|
126
|
+
expect(html).toContain('rpc error: code = Unknown desc = app path does not exist')
|
|
127
|
+
})
|
|
128
|
+
})
|
|
@@ -57,7 +57,7 @@ export function groupBadgeClass(group: string): string {
|
|
|
57
57
|
// new category needs no frontend deploy to APPEAR); the UI humanizes for
|
|
58
58
|
// display, falling back to title-cased snake_case for anything unmapped.
|
|
59
59
|
const CATEGORY_LABEL: Record<string, string> = {
|
|
60
|
-
unschedulable: '
|
|
60
|
+
unschedulable: "Can't be scheduled",
|
|
61
61
|
quota_exceeded: 'Quota exceeded',
|
|
62
62
|
admission_webhook_blocking: 'Admission blocked',
|
|
63
63
|
image_pull_failed: 'Image pull failed',
|
|
@@ -65,12 +65,12 @@ const CATEGORY_LABEL: Record<string, string> = {
|
|
|
65
65
|
init_container_failed: 'Init container failed',
|
|
66
66
|
crashloop: 'Crash loop',
|
|
67
67
|
oom_killed: 'OOM killed',
|
|
68
|
-
liveness_probe_failed: '
|
|
69
|
-
readiness_failed: '
|
|
68
|
+
liveness_probe_failed: 'Health check failing',
|
|
69
|
+
readiness_failed: 'Not ready for traffic',
|
|
70
70
|
workload_degraded: 'Workload degraded',
|
|
71
71
|
high_restart: 'High restart count',
|
|
72
72
|
missing_config_ref: 'Missing reference',
|
|
73
|
-
pdb_blocks_evictions: '
|
|
73
|
+
pdb_blocks_evictions: 'Evictions blocked',
|
|
74
74
|
secret_sync_failed: 'Secret sync failed',
|
|
75
75
|
service_no_endpoints: 'No endpoints',
|
|
76
76
|
ingress_backend_missing: 'Ingress backend missing',
|
|
@@ -88,22 +88,23 @@ const CATEGORY_LABEL: Record<string, string> = {
|
|
|
88
88
|
job_failed: 'Job failed',
|
|
89
89
|
cronjob_failed: 'CronJob failed',
|
|
90
90
|
rollout_stalled: 'Rollout stalled',
|
|
91
|
-
hpa_limited_or_failed: '
|
|
92
|
-
rbac_forbidden: '
|
|
91
|
+
hpa_limited_or_failed: 'Autoscaling limited',
|
|
92
|
+
rbac_forbidden: 'Permission denied',
|
|
93
93
|
certificate_not_ready: 'Certificate not ready',
|
|
94
|
-
pod_security_violation: 'Pod Security
|
|
94
|
+
pod_security_violation: 'Pod Security blocked',
|
|
95
95
|
node_not_ready: 'Node not ready',
|
|
96
96
|
node_provisioning_failed: 'Node provisioning failed',
|
|
97
|
-
apiservice_unavailable: '
|
|
97
|
+
apiservice_unavailable: 'API extension unavailable',
|
|
98
98
|
crossplane_reconcile_failed: 'Crossplane reconcile failed',
|
|
99
99
|
termination_stuck: 'Stuck terminating',
|
|
100
|
-
operator_condition_failed: 'Controller
|
|
100
|
+
operator_condition_failed: 'Controller reports a problem',
|
|
101
101
|
gitops_sync_failed: 'GitOps sync failed',
|
|
102
102
|
gitops_render_failed: 'GitOps render failed',
|
|
103
103
|
gitops_spec_invalid: 'GitOps spec invalid',
|
|
104
104
|
gitops_operation_failed: 'GitOps operation failed',
|
|
105
105
|
gitops_out_of_sync: 'GitOps out of sync',
|
|
106
106
|
gitops_health_degraded: 'GitOps health degraded',
|
|
107
|
+
helm_release_failed: 'Helm release failed',
|
|
107
108
|
webhook_backend_down: 'Webhook backend down',
|
|
108
109
|
control_plane_not_ready: 'Control plane not ready',
|
|
109
110
|
machine_not_ready: 'Machine not ready',
|
|
@@ -71,11 +71,31 @@ export interface IssueDiagnosticIssueRef {
|
|
|
71
71
|
reason?: string;
|
|
72
72
|
category?: string;
|
|
73
73
|
severity?: IssueSeverity;
|
|
74
|
+
/** How many affected resources fold into this linked issue from the root's
|
|
75
|
+
* perspective (e.g. 5 of a PVC's mounting pods under one Deployment issue).
|
|
76
|
+
* Absent when the link covers a single resource. */
|
|
77
|
+
count?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type IssueDiagnosticConfidence = 'high' | 'medium' | 'low';
|
|
81
|
+
|
|
82
|
+
/** Reverse pointer from a symptom issue to the root issue that explains it
|
|
83
|
+
* (the inverse of diagnostic_context's root→symptom facts). Set only when a
|
|
84
|
+
* single root is unambiguous. `ref` is the parent subject for display + deep
|
|
85
|
+
* navigation (thread the issue's cluster_id onto it via memberRef). */
|
|
86
|
+
export interface IssueIncidentParent {
|
|
87
|
+
id: string;
|
|
88
|
+
ref: IssueResourceRef;
|
|
89
|
+
category?: string;
|
|
90
|
+
confidence?: IssueDiagnosticConfidence;
|
|
91
|
+
fact_type?: string;
|
|
74
92
|
}
|
|
75
93
|
|
|
76
94
|
export interface IssueDiagnosticFact {
|
|
77
95
|
type: string;
|
|
78
96
|
message?: string;
|
|
97
|
+
/** How certain a cross-subject causal link is. Absent for non-causal facts. */
|
|
98
|
+
confidence?: IssueDiagnosticConfidence;
|
|
79
99
|
refs?: IssueResourceRef[];
|
|
80
100
|
related_issues?: IssueDiagnosticIssueRef[];
|
|
81
101
|
}
|
|
@@ -99,6 +119,7 @@ export interface IssueRecentChangeField {
|
|
|
99
119
|
}
|
|
100
120
|
|
|
101
121
|
export interface IssueRecentChange {
|
|
122
|
+
source?: string;
|
|
102
123
|
kind: string;
|
|
103
124
|
namespace?: string;
|
|
104
125
|
name: string;
|
|
@@ -153,6 +174,7 @@ export interface Issue {
|
|
|
153
174
|
|
|
154
175
|
reason: string;
|
|
155
176
|
message?: string;
|
|
177
|
+
raw_message?: string;
|
|
156
178
|
/** Parsed domain diagnosis: plain-English cause, suggested next step, and
|
|
157
179
|
* an optional structured one-click fix.
|
|
158
180
|
* Server-emitted (omitempty); empty for issues without a parser. */
|
|
@@ -177,6 +199,7 @@ export interface Issue {
|
|
|
177
199
|
members?: IssueResourceRef[];
|
|
178
200
|
members_truncated?: boolean;
|
|
179
201
|
diagnostic_context?: IssueDiagnosticContext;
|
|
202
|
+
incident_parent?: IssueIncidentParent;
|
|
180
203
|
change_context?: IssueChangeContext;
|
|
181
204
|
|
|
182
205
|
// Pod crash context carried from the representative member.
|