@skyhook-io/k8s-ui 1.7.11 → 1.7.13
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/AppChips.tsx +109 -0
- package/src/components/applications/AppTooltips.tsx +199 -0
- package/src/components/applications/ApplicationDetail.tsx +671 -0
- package/src/components/applications/ApplicationsList.tsx +569 -0
- package/src/components/applications/ReadyBar.tsx +22 -0
- package/src/components/applications/index.ts +8 -0
- package/src/components/audit/AuditFindingsTable.tsx +3 -25
- package/src/components/dock/TerminalTab.tsx +4 -2
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +1 -0
- package/src/components/issues/IssuesView.tsx +64 -17
- package/src/components/issues/index.ts +1 -1
- package/src/components/issues/issues.test.ts +4 -4
- package/src/components/issues/severity.ts +5 -0
- package/src/components/issues/types.ts +43 -0
- package/src/components/logs/LogCore.tsx +13 -2
- package/src/components/logs/LogToolbarSelects.tsx +6 -2
- package/src/components/logs/LogsViewer.tsx +66 -10
- package/src/components/logs/WorkloadLogsViewer.tsx +68 -13
- package/src/components/logs/useLogStream.ts +41 -3
- package/src/components/resources/ResourcesView.tsx +550 -52
- package/src/components/resources/column-filter-serialization.test.ts +26 -0
- package/src/components/resources/get-default-container-name.test.ts +31 -0
- package/src/components/resources/renderers/DeviceClassRenderer.tsx +49 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +7 -0
- package/src/components/resources/renderers/NvidiaClusterPolicyRenderer.tsx +57 -0
- package/src/components/resources/renderers/NvidiaDriverRenderer.tsx +47 -0
- package/src/components/resources/renderers/PodRenderer.tsx +2 -2
- package/src/components/resources/renderers/ResourceClaimRenderer.tsx +118 -0
- package/src/components/resources/renderers/ResourceClaimTemplateRenderer.tsx +39 -0
- package/src/components/resources/renderers/ResourceSliceRenderer.tsx +72 -0
- package/src/components/resources/renderers/WorkloadRenderer.tsx +5 -4
- package/src/components/resources/renderers/dra-cells.tsx +80 -0
- package/src/components/resources/renderers/index.ts +8 -0
- package/src/components/resources/renderers/nvidia-cells.tsx +43 -0
- package/src/components/resources/resource-utils-dra.ts +90 -0
- package/src/components/resources/resource-utils-nvidia.ts +63 -0
- package/src/components/resources/resource-utils.ts +62 -4
- package/src/components/shared/DetailShell.tsx +14 -7
- package/src/components/shared/EditableYamlView.tsx +37 -17
- package/src/components/shared/ResourceActionsBar.tsx +5 -4
- package/src/components/shared/ResourceRendererDispatch.test.tsx +103 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +27 -2
- package/src/components/timeline/TimelineList.tsx +3 -32
- package/src/components/timeline/TimelineSwimlanes.tsx +3 -31
- package/src/components/topology/K8sResourceNode.tsx +26 -5
- package/src/components/topology/TopologyGraph.tsx +102 -3
- package/src/components/topology/layout.ts +36 -11
- package/src/components/ui/CenteredEmpty.tsx +27 -0
- package/src/components/ui/ConfirmDialog.tsx +1 -1
- package/src/components/ui/SearchBox.tsx +85 -0
- package/src/components/ui/drawer-components.tsx +23 -1
- package/src/components/ui/index.ts +1 -0
- package/src/components/workload/WorkloadView.tsx +167 -33
- package/src/components/workload/index.ts +1 -1
- package/src/hooks/useKeyboardShortcuts.tsx +3 -1
- package/src/index.ts +4 -0
- package/src/types/core.ts +1 -0
- package/src/utils/api-resources.ts +21 -0
- package/src/utils/applications.test.ts +207 -0
- package/src/utils/applications.ts +674 -0
- package/src/utils/custom-columns.test.ts +111 -0
- package/src/utils/custom-columns.ts +49 -0
- package/src/utils/extended-resources.test.ts +152 -0
- package/src/utils/extended-resources.ts +121 -0
- package/src/utils/format.ts +11 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/topology-neighborhood.test.ts +185 -0
- package/src/utils/topology-neighborhood.ts +262 -0
- package/src/utils/workload-colors.ts +36 -0
package/package.json
CHANGED
|
@@ -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
|
+
}
|