@skyhook-io/k8s-ui 1.8.6 → 1.8.7

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.
Files changed (43) hide show
  1. package/package.json +1 -1
  2. package/src/components/applications/ApplicationsView.tsx +2 -0
  3. package/src/components/audit/AuditAlerts.tsx +4 -0
  4. package/src/components/audit/AuditBadgeTooltip.test.tsx +30 -0
  5. package/src/components/audit/AuditBadgeTooltip.tsx +47 -0
  6. package/src/components/audit/AuditFindingsTable.tsx +4 -0
  7. package/src/components/audit/index.ts +1 -0
  8. package/src/components/gitops/GitOpsDetailLayout.tsx +3 -3
  9. package/src/components/gitops/GitOpsStatusBadge.tsx +9 -3
  10. package/src/components/gitops/GitOpsTableView.tsx +3 -1
  11. package/src/components/issues/IssuesView.tsx +9 -36
  12. package/src/components/issues/ResourceIssuesSection.tsx +142 -0
  13. package/src/components/issues/diagnostic.ts +64 -0
  14. package/src/components/issues/index.ts +2 -1
  15. package/src/components/issues/severity.ts +10 -9
  16. package/src/components/issues/types.ts +5 -0
  17. package/src/components/resources/ResourcesView.tsx +38 -2
  18. package/src/components/resources/cron-to-human.test.ts +41 -0
  19. package/src/components/resources/get-pod-problems.test.ts +18 -0
  20. package/src/components/resources/health-golden.test.ts +66 -0
  21. package/src/components/resources/renderers/JobRenderer.tsx +6 -2
  22. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -2
  23. package/src/components/resources/renderers/NodeRenderer.tsx +17 -8
  24. package/src/components/resources/renderers/PVCRenderer.tsx +7 -7
  25. package/src/components/resources/renderers/PodRenderer.tsx +28 -9
  26. package/src/components/resources/renderers/ServiceRenderer.tsx +23 -9
  27. package/src/components/resources/renderers/WorkloadRenderer.tsx +6 -3
  28. package/src/components/resources/resource-utils-argo.test.ts +23 -0
  29. package/src/components/resources/resource-utils-argo.ts +5 -1
  30. package/src/components/resources/resource-utils-keda.ts +12 -8
  31. package/src/components/resources/resource-utils.ts +34 -14
  32. package/src/components/timeline/TimelineSwimlanes.tsx +1 -0
  33. package/src/components/timeline/shared.tsx +15 -4
  34. package/src/components/topology/K8sResourceNode.tsx +28 -1
  35. package/src/components/topology/layout.ts +11 -5
  36. package/src/components/ui/PaneLoader.tsx +24 -6
  37. package/src/components/ui/drawer-components.test.tsx +35 -0
  38. package/src/components/ui/drawer-components.tsx +13 -1
  39. package/src/components/workload/WorkloadView.tsx +35 -5
  40. package/src/types/core.ts +100 -3
  41. package/src/utils/applications.test.ts +55 -1
  42. package/src/utils/applications.ts +28 -7
  43. package/src/utils/badge-colors.ts +7 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyhook-io/k8s-ui",
3
- "version": "1.8.6",
3
+ "version": "1.8.7",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/skyhook-io/radar",
@@ -56,6 +56,7 @@ const HEALTH_TONE: Record<AppHealth, FacetTone> = {
56
56
  unhealthy: 'error',
57
57
  degraded: 'warning',
58
58
  healthy: 'success',
59
+ neutral: 'info', // Idle — sky, calm
59
60
  unknown: 'neutral',
60
61
  }
61
62
 
@@ -297,6 +298,7 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
297
298
  {healthTile('unhealthy', 'error')}
298
299
  {healthTile('degraded', 'warning')}
299
300
  {healthTile('healthy', 'success')}
301
+ {healthTile('neutral', 'info')}
300
302
  {healthTile('unknown', 'neutral')}
301
303
  </>
302
304
  }
@@ -5,6 +5,10 @@ import { SEVERITY_TEXT, BP_CATEGORY_BADGE, DEFAULT_BADGE_COLOR } from '../../uti
5
5
 
6
6
  export interface AuditFinding {
7
7
  kind: string
8
+ /** API group, backfilled by the backend from the builtin Kind→group table
9
+ * (built-ins → e.g. "apps"/"batch"; CRDs → ""). Part of the resource key
10
+ * used to join findings onto topology nodes / list rows. */
11
+ group?: string
8
12
  namespace: string
9
13
  name: string
10
14
  checkID: string
@@ -0,0 +1,30 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { renderToString } from 'react-dom/server'
3
+ import { AuditBadgeTooltip } from './AuditBadgeTooltip'
4
+
5
+ const msgs = [
6
+ { severity: 'warning', message: 'Service selector matches no pods' },
7
+ { severity: 'danger', message: 'Ingress references missing Service' },
8
+ { severity: 'warning', message: 'Uses a deprecated API version' },
9
+ { severity: 'warning', message: 'A fourth finding' },
10
+ ]
11
+
12
+ describe('AuditBadgeTooltip', () => {
13
+ it('lists each finding message up to the cap', () => {
14
+ const html = renderToString(<AuditBadgeTooltip messages={msgs.slice(0, 2)} />)
15
+ expect(html).toContain('Service selector matches no pods')
16
+ expect(html).toContain('Ingress references missing Service')
17
+ expect(html).not.toContain('more')
18
+ })
19
+
20
+ it('caps the list and collapses the rest into "+N more"', () => {
21
+ const html = renderToString(<AuditBadgeTooltip messages={msgs} max={3} />)
22
+ expect(html).toContain('+1 more')
23
+ expect(html).not.toContain('A fourth finding')
24
+ })
25
+
26
+ it('shows the click hint by default and omits it when disabled', () => {
27
+ expect(renderToString(<AuditBadgeTooltip messages={msgs.slice(0, 1)} />)).toContain('Click to open')
28
+ expect(renderToString(<AuditBadgeTooltip messages={msgs.slice(0, 1)} clickHint={false} />)).not.toContain('Click to open')
29
+ })
30
+ })
@@ -0,0 +1,47 @@
1
+ import { ShieldAlert, AlertTriangle } from 'lucide-react'
2
+ import { clsx } from 'clsx'
3
+ import { SEVERITY_TEXT } from '../../utils/badge-colors'
4
+
5
+ export interface AuditBadgeMessage {
6
+ severity: string
7
+ message: string
8
+ }
9
+
10
+ interface AuditBadgeTooltipProps {
11
+ messages: AuditBadgeMessage[]
12
+ /** Max messages to list before collapsing the rest into "+N more". */
13
+ max?: number
14
+ /** Hint that clicking opens the resource — omitted when the badge isn't clickable. */
15
+ clickHint?: boolean
16
+ }
17
+
18
+ /**
19
+ * Inline tooltip body for an audit badge: lists the actual finding messages
20
+ * (danger-first) so the operator reads WHAT is wrong on hover, instead of a
21
+ * content-free "N findings". Shared by the resource-list and topology-node
22
+ * badges so the two can't drift.
23
+ */
24
+ export function AuditBadgeTooltip({ messages, max = 3, clickHint = true }: AuditBadgeTooltipProps) {
25
+ const shown = messages.slice(0, max)
26
+ const overflow = messages.length - shown.length
27
+ return (
28
+ <div className="flex flex-col gap-1 text-left">
29
+ {shown.map((m, i) => {
30
+ const isDanger = m.severity === 'danger'
31
+ const Icon = isDanger ? ShieldAlert : AlertTriangle
32
+ return (
33
+ <div key={i} className="flex items-start gap-1.5">
34
+ <Icon className={clsx('w-3 h-3 shrink-0 mt-0.5', isDanger ? SEVERITY_TEXT.error : SEVERITY_TEXT.warning)} />
35
+ <span>{m.message}</span>
36
+ </div>
37
+ )
38
+ })}
39
+ {overflow > 0 && (
40
+ <div className="text-theme-text-tertiary">{`+${overflow} more`}</div>
41
+ )}
42
+ {clickHint && (
43
+ <div className="text-theme-text-tertiary mt-0.5">Click to open →</div>
44
+ )}
45
+ </div>
46
+ )
47
+ }
@@ -27,6 +27,10 @@ export interface CheckMeta {
27
27
  remediation: string
28
28
  frameworks?: string[]
29
29
  references?: CheckReference[]
30
+ /** Finding means the specific resource is broken (reference-integrity /
31
+ * lifecycle), worth a per-resource topology/list badge — vs posture /
32
+ * best-practice checks that fire near-universally. Set by the backend registry. */
33
+ badgeWorthy?: boolean
30
34
  }
31
35
 
32
36
  /** An authoritative link for a check (K8s docs, CIS, NSA/CISA, …). */
@@ -1,3 +1,4 @@
1
1
  export { AuditCard, type AuditCardData } from './AuditCard'
2
2
  export { AuditAlerts, type AuditFinding } from './AuditAlerts'
3
+ export { AuditBadgeTooltip, type AuditBadgeMessage } from './AuditBadgeTooltip'
3
4
  export { AuditFindingsTable, type AuditFindingsTableProps, type ResourceGroup, type CheckMeta, type CheckReference } from './AuditFindingsTable'
@@ -184,11 +184,11 @@ export interface GitOpsDetailLayoutProps {
184
184
  isFleetContext?: boolean
185
185
  destinationCluster?: { id: string; name: string }
186
186
 
187
- // Tab-title side effect — sets document.title to "<name> Radar" while
187
+ // Tab-title side effect — sets document.title to "<name> · Radar" while
188
188
  // mounted, restores on unmount. Opt-in so hub-web fleet detail can pick
189
189
  // its own title format ("<name> — Fleet GitOps").
190
190
  manageDocumentTitle?: boolean
191
- documentTitleSuffix?: string // defaults to " Radar"
191
+ documentTitleSuffix?: string // defaults to " · Radar"
192
192
 
193
193
  // Children slot — for dialogs (SyncOptionsDialog, RollbackDialog, …)
194
194
  // that should portal to body. Caller owns dialog state. Children render
@@ -246,7 +246,7 @@ export function GitOpsDetailLayout(props: GitOpsDetailLayoutProps) {
246
246
  useEffect(() => {
247
247
  if (!manageDocumentTitle) return
248
248
  const previous = document.title
249
- const suffix = documentTitleSuffix ?? ' Radar'
249
+ const suffix = documentTitleSuffix ?? ' · Radar'
250
250
  document.title = `${identity.name}${suffix}`
251
251
  return () => { document.title = previous }
252
252
  }, [identity.name, manageDocumentTitle, documentTitleSuffix])
@@ -67,7 +67,10 @@ function getStatusIcon(status: GitOpsStatus) {
67
67
  }
68
68
 
69
69
  function getStatusColorClass(status: GitOpsStatus): string {
70
- if (status.suspended) return SEVERITY_BADGE_BORDERED.warning
70
+ // Suspended sync (manual-sync mode) is an intentional operating mode many teams
71
+ // run by default — sky (info), not amber. Real drift surfaces separately as
72
+ // OutOfSync (still amber); the Pause icon + "Suspended" label signal it's manual.
73
+ if (status.suspended) return SEVERITY_BADGE_BORDERED.info
71
74
  if (status.sync === 'Synced' && status.health === 'Healthy') return SEVERITY_BADGE_BORDERED.success
72
75
  if (status.health === 'Degraded') return SEVERITY_BADGE_BORDERED.error
73
76
  if (status.sync === 'OutOfSync') return SEVERITY_BADGE_BORDERED.warning
@@ -112,7 +115,9 @@ function getHealthInfo(health: GitOpsHealthStatus) {
112
115
  case 'Degraded':
113
116
  return { icon: XCircle, color: SEVERITY_BADGE.error, label: 'Degraded' }
114
117
  case 'Suspended':
115
- return { icon: Pause, color: SEVERITY_BADGE.warning, label: 'Suspended' }
118
+ // Intentional pause, not a degradation — sky (info), matching the resource
119
+ // table + app rollup. The Pause icon already signals it's deliberate.
120
+ return { icon: Pause, color: SEVERITY_BADGE.info, label: 'Suspended' }
116
121
  case 'Missing':
117
122
  return { icon: AlertCircle, color: SEVERITY_BADGE.warning, label: 'Missing' }
118
123
  default:
@@ -125,8 +130,9 @@ function getHealthInfo(health: GitOpsHealthStatus) {
125
130
  */
126
131
  export function SyncStatusBadge({ sync, suspended }: { sync: SyncStatus; suspended?: boolean }) {
127
132
  if (suspended) {
133
+ // Manual-sync mode is intentional — sky (info), not amber. See getStatusColorClass.
128
134
  return (
129
- <span className={clsx('badge', SEVERITY_BADGE_BORDERED.warning)}>
135
+ <span className={clsx('badge', SEVERITY_BADGE_BORDERED.info)}>
130
136
  <Pause className="w-3 h-3" />
131
137
  Suspended
132
138
  </span>
@@ -1704,7 +1704,9 @@ function compareRows(a: GitOpsRow, b: GitOpsRow, sortKey: SortKey) {
1704
1704
  // row sorting above a Synced-Progressing one. A sync-aware triage ordering is a
1705
1705
  // reasonable separate default, but not what "sort by Health" should mean.)
1706
1706
  const HEALTH_RANK: Record<string, number> = {
1707
- Degraded: 0, Missing: 0, Suspended: 1, Unknown: 2, Progressing: 3, Healthy: 4,
1707
+ // Suspended is intentional/benign (neutral), so it sorts at the healthy end
1708
+ // not near Degraded/Missing — matching its sky tone across the other surfaces.
1709
+ Degraded: 0, Missing: 0, Unknown: 2, Progressing: 3, Healthy: 4, Suspended: 4,
1708
1710
  }
1709
1711
  function healthRank(row: GitOpsRow): number {
1710
1712
  return HEALTH_RANK[row.health] ?? 2
@@ -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 } from './diagnostic';
5
6
  import {
6
7
  ISSUE_SEVERITY_BADGE_CLASS,
7
8
  ISSUE_SEVERITY_LABEL,
@@ -366,6 +367,14 @@ function DiagnosticContext({
366
367
  <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
368
  <div className="flex min-w-0 items-baseline gap-2">
368
369
  <span className="shrink-0 text-xs font-medium text-theme-text-secondary">{diagnosticFactLabel(fact.type)}</span>
370
+ {fact.confidence ? (
371
+ <span
372
+ className="shrink-0 badge-sm text-[10px] text-theme-text-tertiary"
373
+ title={confidenceTitle(fact.confidence)}
374
+ >
375
+ {fact.confidence} confidence
376
+ </span>
377
+ ) : null}
369
378
  {fact.message ? <span className="min-w-0 break-words text-xs leading-relaxed text-theme-text-tertiary">{fact.message}</span> : null}
370
379
  </div>
371
380
  {fact.related_issues?.length ? (
@@ -402,42 +411,6 @@ function DiagnosticContext({
402
411
  );
403
412
  }
404
413
 
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
414
  // Native-tooltip detail for the collapsed-row age chip: absolute first-seen + last-seen
442
415
  // freshness, the two facts the compact "2h" hides.
443
416
  function ageTitle(issue: Issue): string {
@@ -0,0 +1,142 @@
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
+ </>
117
+ )
118
+ return (
119
+ <li key={ri} className="text-theme-text-tertiary">
120
+ {onResourceClick ? (
121
+ <button
122
+ type="button"
123
+ onClick={() => onResourceClick(rel.ref)}
124
+ className="group inline-flex items-center gap-1 text-left hover:text-theme-text-secondary"
125
+ >
126
+ {label}
127
+ <ExternalLink className="h-3 w-3 opacity-0 group-hover:opacity-100" />
128
+ </button>
129
+ ) : (
130
+ label
131
+ )}
132
+ </li>
133
+ )
134
+ })}
135
+ </ul>
136
+ ) : null}
137
+ </li>
138
+ ))}
139
+ </ul>
140
+ </div>
141
+ )
142
+ }
@@ -0,0 +1,64 @@
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
+ default:
46
+ return type.replace(/_/g, ' ');
47
+ }
48
+ }
49
+
50
+ // Plain-language gloss for the confidence chip's tooltip — the operator should
51
+ // know a medium link is "these are co-located, the node may be the cause", not a
52
+ // proven fact.
53
+ export function confidenceTitle(confidence: string): string {
54
+ switch (confidence) {
55
+ case 'high':
56
+ return 'High confidence: a declared structural link (selector, owner, or claim reference).';
57
+ case 'medium':
58
+ return 'Medium confidence: these resources are related, but causation is inferred — verify before acting.';
59
+ case 'low':
60
+ return 'Low confidence: a heuristic match.';
61
+ default:
62
+ return '';
63
+ }
64
+ }
@@ -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, IssueChangeContext, IssueRecentChange, IssueRecentChangeField } from './types';
15
16
  export {
16
17
  ISSUE_SEVERITY_LABEL,
17
18
  ISSUE_SEVERITY_BADGE_CLASS,
@@ -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: '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: 'Liveness probe failing',
69
- readiness_failed: 'Readiness failing',
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: '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: 'HPA limited',
92
- rbac_forbidden: '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 violation',
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: '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 condition',
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',
@@ -73,9 +73,13 @@ export interface IssueDiagnosticIssueRef {
73
73
  severity?: IssueSeverity;
74
74
  }
75
75
 
76
+ export type IssueDiagnosticConfidence = 'high' | 'medium' | 'low';
77
+
76
78
  export interface IssueDiagnosticFact {
77
79
  type: string;
78
80
  message?: string;
81
+ /** How certain a cross-subject causal link is. Absent for non-causal facts. */
82
+ confidence?: IssueDiagnosticConfidence;
79
83
  refs?: IssueResourceRef[];
80
84
  related_issues?: IssueDiagnosticIssueRef[];
81
85
  }
@@ -99,6 +103,7 @@ export interface IssueRecentChangeField {
99
103
  }
100
104
 
101
105
  export interface IssueRecentChange {
106
+ source?: string;
102
107
  kind: string;
103
108
  namespace?: string;
104
109
  name: string;