@skyhook-io/k8s-ui 1.8.5 → 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.
- package/package.json +1 -1
- package/src/components/applications/ApplicationsView.tsx +2 -0
- 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/gitops/GitOpsDetailLayout.tsx +3 -3
- package/src/components/gitops/GitOpsStatusBadge.tsx +9 -3
- package/src/components/gitops/GitOpsTableView.tsx +3 -1
- package/src/components/issues/IssuesView.tsx +9 -36
- package/src/components/issues/ResourceIssuesSection.tsx +142 -0
- package/src/components/issues/diagnostic.ts +64 -0
- package/src/components/issues/index.ts +2 -1
- package/src/components/issues/severity.ts +10 -9
- package/src/components/issues/types.ts +5 -0
- package/src/components/resources/ResourcesView.tsx +38 -2
- package/src/components/resources/cron-to-human.test.ts +41 -0
- package/src/components/resources/get-pod-phase-display.test.ts +115 -3
- 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/CronJobRenderer.tsx +14 -7
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +4 -3
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +9 -15
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +3 -11
- package/src/components/resources/renderers/GatewayRenderer.tsx +11 -35
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +10 -16
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -6
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +24 -28
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +19 -26
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +20 -19
- package/src/components/resources/renderers/JobRenderer.tsx +12 -7
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -2
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +9 -11
- 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/SimpleRouteRenderer.tsx +6 -12
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +11 -10
- package/src/components/resources/renderers/TraefikMiddlewareRenderer.tsx +185 -0
- package/src/components/resources/renderers/TraefikServersTransportRenderer.tsx +84 -0
- package/src/components/resources/renderers/TraefikServiceRenderer.tsx +118 -0
- package/src/components/resources/renderers/TraefikTLSOptionRenderer.tsx +62 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +1 -1
- package/src/components/resources/renderers/WorkloadRenderer.tsx +6 -3
- package/src/components/resources/renderers/badge-no-handrolled.test.tsx +66 -0
- package/src/components/resources/renderers/contour-cells.tsx +6 -5
- package/src/components/resources/renderers/index.ts +4 -0
- package/src/components/resources/renderers/istio-cells.tsx +16 -25
- 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-cnpg.ts +1 -1
- package/src/components/resources/resource-utils-keda.ts +17 -7
- package/src/components/resources/resource-utils.ts +206 -71
- package/src/components/shared/ResourceRendererDispatch.tsx +10 -0
- 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 +41 -9
- package/src/components/topology/layout.ts +11 -5
- package/src/components/ui/Badge.tsx +66 -3
- package/src/components/ui/ClusterName.tsx +12 -4
- package/src/components/ui/PaneLoader.tsx +30 -7
- package/src/components/ui/drawer-components.test.tsx +35 -0
- package/src/components/ui/drawer-components.tsx +13 -1
- package/src/components/workload/WorkloadView.tsx +35 -5
- package/src/types/core.ts +136 -2
- package/src/utils/applications.test.ts +55 -1
- package/src/utils/applications.ts +28 -7
- package/src/utils/asset-url.ts +13 -0
- package/src/utils/badge-colors.ts +7 -0
package/package.json
CHANGED
|
@@ -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>
|
|
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 "
|
|
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 ?? '
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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: '
|
|
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',
|
|
@@ -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;
|