@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.
Files changed (68) hide show
  1. package/package.json +3 -3
  2. package/src/components/applications/ApplicationsList.tsx +5 -2
  3. package/src/components/applications/ApplicationsView.tsx +6 -1
  4. package/src/components/audit/AuditAlerts.tsx +4 -0
  5. package/src/components/audit/AuditBadgeTooltip.test.tsx +30 -0
  6. package/src/components/audit/AuditBadgeTooltip.tsx +47 -0
  7. package/src/components/audit/AuditFindingsTable.tsx +4 -0
  8. package/src/components/audit/index.ts +1 -0
  9. package/src/components/cluster-switcher/ClusterSwitcher.tsx +27 -9
  10. package/src/components/gitops/GitOpsDetailLayout.tsx +3 -3
  11. package/src/components/gitops/GitOpsStatusBadge.tsx +9 -3
  12. package/src/components/gitops/GitOpsTableView.tsx +49 -46
  13. package/src/components/gitops/insights/GitOpsInsightViews.tsx +12 -5
  14. package/src/components/issues/IssuesView.tsx +49 -40
  15. package/src/components/issues/ResourceIssuesSection.tsx +145 -0
  16. package/src/components/issues/diagnostic.ts +86 -0
  17. package/src/components/issues/index.ts +2 -1
  18. package/src/components/issues/issues.test.ts +21 -0
  19. package/src/components/issues/severity.ts +10 -9
  20. package/src/components/issues/types.ts +23 -0
  21. package/src/components/namespace-switcher/NamespacePicker.tsx +381 -0
  22. package/src/components/namespace-switcher/index.ts +6 -0
  23. package/src/components/resources/ResourcesView.tsx +58 -83
  24. package/src/components/resources/cron-to-human.test.ts +41 -0
  25. package/src/components/resources/get-pod-problems.test.ts +18 -0
  26. package/src/components/resources/health-golden.test.ts +66 -0
  27. package/src/components/resources/renderers/JobRenderer.tsx +6 -2
  28. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -2
  29. package/src/components/resources/renderers/NodeRenderer.tsx +17 -8
  30. package/src/components/resources/renderers/PVCRenderer.tsx +7 -7
  31. package/src/components/resources/renderers/PodRenderer.tsx +28 -9
  32. package/src/components/resources/renderers/ServiceRenderer.tsx +23 -9
  33. package/src/components/resources/renderers/WorkloadRenderer.tsx +6 -3
  34. package/src/components/resources/resource-utils-argo.test.ts +23 -0
  35. package/src/components/resources/resource-utils-argo.ts +5 -1
  36. package/src/components/resources/resource-utils-keda.ts +12 -8
  37. package/src/components/resources/resource-utils.ts +34 -14
  38. package/src/components/scope-pill/ScopePill.tsx +35 -0
  39. package/src/components/scope-pill/index.ts +2 -0
  40. package/src/components/timeline/TimelineList.tsx +27 -1
  41. package/src/components/timeline/TimelineSwimlanes.tsx +1 -0
  42. package/src/components/timeline/shared.tsx +15 -4
  43. package/src/components/topology/K8sResourceNode.tsx +28 -1
  44. package/src/components/topology/TopologyControls.tsx +90 -14
  45. package/src/components/topology/layout.ts +11 -5
  46. package/src/components/ui/FreshnessControl.tsx +153 -0
  47. package/src/components/ui/PaneLoader.tsx +24 -6
  48. package/src/components/ui/SortableTh.tsx +16 -10
  49. package/src/components/ui/Toast.tsx +1 -1
  50. package/src/components/ui/drawer-components.test.tsx +35 -0
  51. package/src/components/ui/drawer-components.tsx +13 -1
  52. package/src/components/ui/index.ts +2 -0
  53. package/src/components/workload/ResourceDetailDrawer.tsx +215 -32
  54. package/src/components/workload/WorkloadView.tsx +61 -13
  55. package/src/hooks/index.ts +1 -0
  56. package/src/hooks/useKeyboardShortcuts.tsx +23 -2
  57. package/src/hooks/useRefreshAnimation.ts +15 -2
  58. package/src/index.ts +8 -0
  59. package/src/types/core.ts +142 -3
  60. package/src/types/gitops-insights.ts +4 -0
  61. package/src/utils/animation.ts +10 -0
  62. package/src/utils/applications.test.ts +55 -1
  63. package/src/utils/applications.ts +28 -7
  64. package/src/utils/badge-colors.ts +7 -0
  65. package/src/utils/format-freshness.test.ts +34 -0
  66. package/src/utils/format.ts +32 -0
  67. package/src/utils/resource-hierarchy.test.ts +51 -0
  68. 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 rawMessage = issue.cause ? issue.message ?? '' : [headline, detail].filter(Boolean).join(' ');
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 shown below the parsed cause so
312
- the precise error (URLs, resource names) is available without leading. */}
313
- {issue.cause && rawMessage ? (
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: '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',
@@ -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.