@skyhook-io/k8s-ui 1.7.12 → 1.7.14

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 (41) hide show
  1. package/package.json +1 -1
  2. package/src/components/applications/AppChips.tsx +109 -0
  3. package/src/components/applications/AppTooltips.tsx +199 -0
  4. package/src/components/applications/ApplicationDetail.tsx +671 -0
  5. package/src/components/applications/ApplicationsList.tsx +569 -0
  6. package/src/components/applications/ReadyBar.tsx +22 -0
  7. package/src/components/applications/index.ts +8 -0
  8. package/src/components/audit/AuditFindingsTable.tsx +3 -25
  9. package/src/components/audit/index.ts +1 -1
  10. package/src/components/checks/ChecksView.tsx +269 -142
  11. package/src/components/checks/index.ts +14 -1
  12. package/src/components/issues/IssuesView.tsx +56 -17
  13. package/src/components/issues/index.ts +2 -2
  14. package/src/components/issues/issues.test.ts +7 -0
  15. package/src/components/issues/types.ts +15 -5
  16. package/src/components/logs/WorkloadLogsViewer.tsx +8 -5
  17. package/src/components/resources/renderers/WorkloadRenderer.tsx +5 -4
  18. package/src/components/shared/DetailShell.tsx +14 -7
  19. package/src/components/shared/EditableYamlView.tsx +37 -17
  20. package/src/components/shared/ResourceActionsBar.tsx +6 -6
  21. package/src/components/timeline/TimelineList.tsx +3 -32
  22. package/src/components/timeline/TimelineSwimlanes.tsx +3 -31
  23. package/src/components/topology/K8sResourceNode.tsx +26 -5
  24. package/src/components/topology/TopologyGraph.tsx +102 -3
  25. package/src/components/topology/layout.ts +36 -11
  26. package/src/components/ui/CenteredEmpty.tsx +27 -0
  27. package/src/components/ui/ForceDeleteConfirmDialog.tsx +1 -2
  28. package/src/components/ui/SearchBox.tsx +85 -0
  29. package/src/components/ui/drawer-components.tsx +8 -0
  30. package/src/components/ui/index.ts +1 -0
  31. package/src/components/workload/WorkloadView.tsx +170 -36
  32. package/src/components/workload/index.ts +1 -1
  33. package/src/hooks/useKeyboardShortcuts.tsx +3 -1
  34. package/src/index.ts +4 -0
  35. package/src/utils/applications.test.ts +207 -0
  36. package/src/utils/applications.ts +674 -0
  37. package/src/utils/format.ts +11 -0
  38. package/src/utils/index.ts +2 -0
  39. package/src/utils/topology-neighborhood.test.ts +185 -0
  40. package/src/utils/topology-neighborhood.ts +262 -0
  41. package/src/utils/workload-colors.ts +36 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyhook-io/k8s-ui",
3
- "version": "1.7.12",
3
+ "version": "1.7.14",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/skyhook-io/radar",
@@ -0,0 +1,109 @@
1
+ import { Tooltip } from '../ui/Tooltip'
2
+ import {
3
+ type AppRow,
4
+ type AppWorkloadClass,
5
+ CHIP,
6
+ CHIP_TONE,
7
+ CLASS_META,
8
+ overlayProvenance,
9
+ } from '../../utils/applications'
10
+ import { midTruncate } from '../../utils/format'
11
+ import { ProvenanceTooltip, CategoryTooltip, VersionTooltip } from './AppTooltips'
12
+
13
+ // The composed badges shared by ApplicationsList and ApplicationDetail. Both
14
+ // surfaces must render the same chip with the same tooltip for the same fact —
15
+ // defining them once is what keeps that true.
16
+
17
+ export function ProvenanceBadge({ tier, appKey, confidence }: { tier?: number; appKey: string; confidence?: string }) {
18
+ if (!tier) {
19
+ return (
20
+ <Tooltip content="No GitOps, Helm, or app-label grouping signal — shown as the raw workload." delay={150}>
21
+ <span className={`${CHIP} ${CHIP_TONE.muted}`}>ungrouped</span>
22
+ </Tooltip>
23
+ )
24
+ }
25
+ const conf = confidence ?? 'low'
26
+ const tone = conf === 'high' ? CHIP_TONE.emerald : conf === 'medium' ? CHIP_TONE.neutral : CHIP_TONE.amber
27
+ return (
28
+ <Tooltip content={<ProvenanceTooltip tier={tier} appKey={appKey} confidence={conf} />} delay={150}>
29
+ <span className={`${CHIP} ${tone}`}>{overlayProvenance(tier)}</span>
30
+ </Tooltip>
31
+ )
32
+ }
33
+
34
+ export function ClassBadge({
35
+ workloadClass,
36
+ composition,
37
+ }: {
38
+ workloadClass: AppWorkloadClass
39
+ /** Per-class counts (classCompositionOf) — lets a Mixed badge's tooltip say
40
+ * what the mix actually contains. */
41
+ composition?: { cls: AppWorkloadClass; count: number }[]
42
+ }) {
43
+ const meta = CLASS_META[workloadClass]
44
+ const tooltip =
45
+ workloadClass === 'mixed' && composition && composition.length > 0 ? (
46
+ <div className="space-y-1">
47
+ <div className="text-xs text-theme-text-primary">
48
+ Contains {composition.map((c) => `${c.count} ${CLASS_META[c.cls].label}${c.count > 1 ? 's' : ''}`).join(' · ')}
49
+ </div>
50
+ <div className="text-[11px] leading-snug text-theme-text-secondary">Class filters match any of the contained classes.</div>
51
+ </div>
52
+ ) : (
53
+ meta.tooltip
54
+ )
55
+ return (
56
+ <Tooltip content={tooltip} delay={150}>
57
+ <span className={`${CHIP} ${meta.pill}`}>{meta.label}</span>
58
+ </Tooltip>
59
+ )
60
+ }
61
+
62
+ /** The add-on / mixed classification chip; renders nothing for plain apps. */
63
+ export function CategoryChip({ category, addonReason }: { category?: string; addonReason?: string }) {
64
+ if (category !== 'addon' && category !== 'mixed') return null
65
+ return (
66
+ <Tooltip content={<CategoryTooltip category={category} addonReason={addonReason} />} delay={150}>
67
+ <span className={`${CHIP} ${category === 'mixed' ? CHIP_TONE.amber : CHIP_TONE.muted}`}>{category === 'addon' ? 'add-on' : 'mixed'}</span>
68
+ </Tooltip>
69
+ )
70
+ }
71
+
72
+ /** The version display: appVersion when the workloads agree on one upstream
73
+ * version, the single image tag, or "N versions" (amber only on real skew).
74
+ * `cell` = table chip styling, `fact` = the detail context strip. */
75
+ export function VersionInfo({ app, variant }: { app: AppRow; variant: 'cell' | 'fact' }) {
76
+ const versions = Array.from(new Set((app.versions || []).filter(Boolean)))
77
+ const workloads = app.workloads ?? []
78
+ const max = variant === 'fact' ? 32 : 24
79
+ const mono = variant === 'fact' ? 'font-mono' : 'font-mono text-xs text-theme-text-secondary'
80
+ if (app.appVersion) {
81
+ return (
82
+ <Tooltip content={<VersionTooltip workloads={workloads} />} delay={150}>
83
+ <span className={mono}>{midTruncate(app.appVersion, max)}</span>
84
+ </Tooltip>
85
+ )
86
+ }
87
+ if (versions.length === 0) {
88
+ return variant === 'cell' ? <span className="text-theme-text-tertiary">—</span> : null
89
+ }
90
+ if (versions.length === 1) {
91
+ const v = versions[0]
92
+ return v.length > max ? (
93
+ <Tooltip content={v} delay={150}>
94
+ <span className={mono}>{midTruncate(v, max)}</span>
95
+ </Tooltip>
96
+ ) : (
97
+ <span className={mono}>{v}</span>
98
+ )
99
+ }
100
+ return (
101
+ <Tooltip content={<VersionTooltip workloads={workloads} />} delay={150}>
102
+ {variant === 'cell' ? (
103
+ <span className={`${CHIP} ${app.versionSkew ? CHIP_TONE.amber : CHIP_TONE.neutral}`}>{versions.length} versions</span>
104
+ ) : (
105
+ <span className={mono}>{versions.length} versions</span>
106
+ )}
107
+ </Tooltip>
108
+ )
109
+ }
@@ -0,0 +1,199 @@
1
+ import { provenanceSource, HEALTH_META, type AppWorkload } from '../../utils/applications'
2
+ import { midTruncate } from '../../utils/format'
3
+
4
+ // Structured tooltip content for the application chips. Both render a short
5
+ // title/phrase plus the technical signal (label key, resource name, selector)
6
+ // in the shared inline-code chip — readable typography instead of a wall of text.
7
+
8
+ export function ProvenanceTooltip({
9
+ tier,
10
+ appKey,
11
+ confidence,
12
+ }: {
13
+ tier: number | undefined
14
+ appKey: string
15
+ confidence: string | undefined
16
+ }) {
17
+ const src = provenanceSource(tier, appKey)
18
+ const conf = confidence ?? 'low'
19
+ return (
20
+ <div className="space-y-1">
21
+ <div className="text-xs text-theme-text-primary">
22
+ Grouped by {src.lead}
23
+ {src.code && (
24
+ <>
25
+ {' '}
26
+ <code className="inline-code">{src.code}</code>
27
+ </>
28
+ )}
29
+ {src.trail ? ` ${src.trail}` : ''}
30
+ </div>
31
+ {/* Only flag the case the user should act on — weak evidence. Medium/high
32
+ confidence is resolver detail, not actionable. */}
33
+ {conf === 'low' && (
34
+ <div className={`text-[10px] uppercase tracking-wide ${HEALTH_META.degraded.text}`}>low confidence</div>
35
+ )}
36
+ </div>
37
+ )
38
+ }
39
+
40
+ // "3 versions" on its own says little; the breakdown says which component runs
41
+ // what — the right shape for umbrella apps that genuinely have no single version.
42
+ export function VersionTooltip({ workloads }: { workloads: Pick<AppWorkload, 'name' | 'version'>[] }) {
43
+ const rows = workloads.filter((w) => w.version)
44
+ if (rows.length === 0) return null
45
+ return (
46
+ <div className="max-w-xs space-y-1">
47
+ {/* These are the running image tags — they can legitimately differ from
48
+ the headline appVersion (a label), so the title says what it shows. */}
49
+ <div className="text-[10px] font-medium uppercase tracking-wide text-theme-text-tertiary">Image tag by workload</div>
50
+ <ul className="space-y-0.5">
51
+ {rows.map((w, i) => (
52
+ <li key={i} className="flex items-baseline justify-between gap-3">
53
+ <span className="truncate text-[11px] text-theme-text-secondary">{w.name}</span>
54
+ <code className="inline-code text-[10px]">{w.version}</code>
55
+ </li>
56
+ ))}
57
+ </ul>
58
+ </div>
59
+ )
60
+ }
61
+
62
+ export function CategoryTooltip({
63
+ category,
64
+ addonReason,
65
+ }: {
66
+ category: string
67
+ addonReason?: string
68
+ }) {
69
+ const mixed = category === 'mixed'
70
+ const title = mixed ? 'Mixed app / add-on' : 'Platform add-on'
71
+ const lead = mixed
72
+ ? 'Has both application and add-on evidence. Kept visible — classification is informational, not identity.'
73
+ : 'Classified as a platform add-on, shown here for completeness.'
74
+ // addonReason is a "; "-separated list of signals, e.g.
75
+ // "app.kubernetes.io/name=argocd-redis (argocd); chart=argo-cd".
76
+ // Render each as its own line; the selector goes in code, the trailing
77
+ // "(namespace)" hint is split out as muted text.
78
+ const items = (addonReason ?? '')
79
+ .split(/;\s*/)
80
+ .map((s) => s.trim())
81
+ .filter(Boolean)
82
+ return (
83
+ <div className="max-w-xs space-y-1.5">
84
+ <div className="text-xs font-semibold text-theme-text-primary">{title}</div>
85
+ <div className="text-[11px] leading-snug text-theme-text-secondary">{lead}</div>
86
+ {items.length > 0 && (
87
+ <div className="space-y-1 border-t border-theme-border pt-1.5">
88
+ <div className="text-[10px] font-medium uppercase tracking-wide text-theme-text-tertiary">Evidence</div>
89
+ <ul className="max-h-52 space-y-1 overflow-y-auto">
90
+ {items.map((it, i) => {
91
+ const parsed = splitTrailingParen(it)
92
+ const selector = parsed?.lead ?? it
93
+ const where = parsed?.paren
94
+ return (
95
+ <li key={i} className="flex items-baseline gap-1.5">
96
+ <code className="inline-code text-[10px]">{selector}</code>
97
+ {where && <span className="text-[10px] text-theme-text-tertiary">{where}</span>}
98
+ </li>
99
+ )
100
+ })}
101
+ </ul>
102
+ </div>
103
+ )}
104
+ </div>
105
+ )
106
+ }
107
+
108
+ // AppIdentityTooltip — why these instances are grouped. One line per distinct
109
+ // evidence in the ProvenanceTooltip idiom (signals as inline-code, long repo
110
+ // refs mid-truncated), plus a small flag when the grouping is heuristic-only.
111
+ // No member list — the env pills next to this chip already show the instances.
112
+ export function AppIdentityTooltip({
113
+ members,
114
+ }: {
115
+ identityKey?: string
116
+ members: { name: string; env: string; confidence: string; evidence: string }[]
117
+ }) {
118
+ const evidences = Array.from(new Set(members.map((m) => m.evidence).filter(Boolean)))
119
+ const heuristicOnly = members.every((m) => m.confidence !== 'high')
120
+ return (
121
+ <div className="max-w-xs space-y-1">
122
+ {evidences.map((e, i) => (
123
+ <div key={i} className="text-xs text-theme-text-primary">
124
+ <EvidenceLine evidence={e} />
125
+ </div>
126
+ ))}
127
+ {heuristicOnly && (
128
+ <div className={`text-[10px] uppercase tracking-wide ${HEALTH_META.degraded.text}`}>heuristic · medium confidence</div>
129
+ )}
130
+ </div>
131
+ )
132
+ }
133
+
134
+ // Renders the resolver's evidence string with its signals as inline-code.
135
+ // Known shapes from applications_identity.go; anything else passes through raw.
136
+ function EvidenceLine({ evidence }: { evidence: string }) {
137
+ const nameStem = quotedEvidenceValue(evidence, 'name stem "', '" + shared image repo ')
138
+ if (nameStem) {
139
+ return (
140
+ <>
141
+ Same name <code className="inline-code">{nameStem.value}</code> + image <code className="inline-code">{midTruncate(nameStem.rest, 32)}</code>
142
+ </>
143
+ )
144
+ }
145
+ const envLabel = quotedEvidenceValue(evidence, 'environment label "', '" + name/repo evidence')
146
+ if (envLabel) {
147
+ return (
148
+ <>
149
+ Environment label <code className="inline-code">{envLabel.value}</code> + name/repo evidence
150
+ </>
151
+ )
152
+ }
153
+ const argo = splitTrailingParen(evidence)
154
+ if (argo && argo.lead.startsWith('Argo CD source path ') && argo.paren.startsWith('env overlay ')) {
155
+ return (
156
+ <>
157
+ Argo CD source path <code className="inline-code">{argo.lead.slice('Argo CD source path '.length)}</code>
158
+ </>
159
+ )
160
+ }
161
+ return <>{evidence}</>
162
+ }
163
+
164
+ function splitTrailingParen(value: string): { lead: string; paren: string } | null {
165
+ if (!value.endsWith(')')) return null
166
+ const open = value.lastIndexOf(' (')
167
+ if (open < 0) return null
168
+ return { lead: value.slice(0, open), paren: value.slice(open + 2, -1) }
169
+ }
170
+
171
+ function quotedEvidenceValue(value: string, prefix: string, separator: string): { value: string; rest: string } | null {
172
+ if (!value.startsWith(prefix)) return null
173
+ const start = prefix.length
174
+ const sep = value.indexOf(separator, start)
175
+ if (sep < 0) return null
176
+ return { value: value.slice(start, sep), rest: value.slice(sep + separator.length) }
177
+ }
178
+
179
+ // EnvHint — how environments are determined + how to set one explicitly.
180
+ // Rendered from the Environment facet's info icon and the unlabeled env
181
+ // cells, so the answer lives exactly where the question arises.
182
+ export function EnvHint({ unlabeled }: { unlabeled?: boolean }) {
183
+ return (
184
+ <div className="max-w-xs space-y-1">
185
+ {unlabeled ? (
186
+ <div className="text-xs text-theme-text-primary">No environment detected for this app.</div>
187
+ ) : (
188
+ <div className="text-xs text-theme-text-primary">Detected from labels, GitOps overlay paths, or name patterns (shown <span className="italic">~inferred</span>).</div>
189
+ )}
190
+ <div className="text-[11px] leading-snug text-theme-text-secondary">
191
+ Set it explicitly — label the namespace or workload:
192
+ </div>
193
+ <code className="inline-code">environment=staging</code>
194
+ <div className="text-[10px] text-theme-text-tertiary">
195
+ also recognized: <code className="inline-code">app.kubernetes.io/environment</code> · <code className="inline-code">env</code> · <code className="inline-code">tags.datadoghq.com/env</code>
196
+ </div>
197
+ </div>
198
+ )
199
+ }