@skyhook-io/radar-app 1.14.1 → 1.14.3

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 (74) hide show
  1. package/package.json +2 -2
  2. package/src/api/client.ts +30 -0
  3. package/src/components/CloudConnectFlow.tsx +21 -7
  4. package/src/components/CloudFunnelButton.tsx +85 -8
  5. package/src/components/cloudConnectHandoff.test.ts +10 -1
  6. package/src/components/cloudConnectHandoff.ts +15 -1
  7. package/src/components/diagnose/AIMarkdown.tsx +87 -0
  8. package/src/components/diagnose/ActivityTurn.tsx +1244 -0
  9. package/src/components/diagnose/AgentControls.tsx +603 -0
  10. package/src/components/diagnose/AnalysisStory.test.tsx +31 -25
  11. package/src/components/diagnose/AnalysisStory.tsx +13 -18
  12. package/src/components/diagnose/ApplyDialog.tsx +289 -0
  13. package/src/components/diagnose/AssessmentCard.tsx +1560 -0
  14. package/src/components/diagnose/DiagnoseSurface.test.tsx +9 -9
  15. package/src/components/diagnose/DiagnoseSurface.tsx +5 -1
  16. package/src/components/diagnose/EvidenceCard.tsx +611 -0
  17. package/src/components/diagnose/Home.test.tsx +1 -3
  18. package/src/components/diagnose/Home.tsx +2 -1
  19. package/src/components/diagnose/InvestigationEvidencePane.story.test.tsx +6 -8
  20. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +5 -3
  21. package/src/components/diagnose/InvestigationEvidencePane.tsx +31 -3445
  22. package/src/components/diagnose/InvestigationEvidenceSections.tsx +523 -0
  23. package/src/components/diagnose/InvestigationView.tsx +11 -28
  24. package/src/components/diagnose/StoryExcerpt.tsx +118 -0
  25. package/src/components/diagnose/assessmentCopy.ts +169 -0
  26. package/src/components/diagnose/investigationCase.test.tsx +631 -2
  27. package/src/components/diagnose/investigationCase.ts +132 -24
  28. package/src/components/diagnose/investigationEvidence/adapters/coverage.test.tsx +660 -0
  29. package/src/components/diagnose/investigationEvidence/adapters/{helm.test.ts → helm.test.tsx} +63 -0
  30. package/src/components/diagnose/investigationEvidence/adapters/helm.ts +70 -1
  31. package/src/components/diagnose/investigationEvidence/adapters/listings.ts +134 -0
  32. package/src/components/diagnose/investigationEvidence/adapters/{permissions.test.ts → permissions.test.tsx} +50 -0
  33. package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +18 -0
  34. package/src/components/diagnose/investigationEvidence/adapters/posture.ts +272 -0
  35. package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +49 -0
  36. package/src/components/diagnose/investigationEvidence/adapters/rankings.ts +147 -0
  37. package/src/components/diagnose/investigationEvidence/adapters/resource.ts +20 -10
  38. package/src/components/diagnose/investigationEvidence/bodies/index.tsx +127 -0
  39. package/src/components/diagnose/investigationEvidence/bodies/inventory.tsx +158 -0
  40. package/src/components/diagnose/investigationEvidence/bodies/metrics.tsx +373 -0
  41. package/src/components/diagnose/investigationEvidence/bodies/network.tsx +274 -0
  42. package/src/components/diagnose/investigationEvidence/bodies/platform.tsx +475 -0
  43. package/src/components/diagnose/investigationEvidence/bodies/posture.tsx +60 -0
  44. package/src/components/diagnose/investigationEvidence/bodies/ranking.tsx +76 -0
  45. package/src/components/diagnose/investigationEvidence/bodies/streams.tsx +212 -0
  46. package/src/components/diagnose/investigationEvidence/bodies/workload.tsx +335 -0
  47. package/src/components/diagnose/investigationEvidence/builder.ts +15 -0
  48. package/src/components/diagnose/investigationEvidence/cardParts.tsx +312 -0
  49. package/src/components/diagnose/investigationEvidence/excerpt.test.ts +105 -0
  50. package/src/components/diagnose/investigationEvidence/excerpt.ts +92 -0
  51. package/src/components/diagnose/investigationEvidence/identity.test.ts +5 -7
  52. package/src/components/diagnose/investigationEvidence/index.ts +1 -1
  53. package/src/components/diagnose/investigationEvidence/navigation.tsx +26 -0
  54. package/src/components/diagnose/investigationEvidence/observations.ts +11 -0
  55. package/src/components/diagnose/investigationEvidence/projection.test.ts +1 -1
  56. package/src/components/diagnose/investigationEvidence/types.ts +97 -2
  57. package/src/components/diagnose/investigationEvidenceKinds.ts +22 -0
  58. package/src/components/diagnose/investigationEvidencePartition.ts +296 -0
  59. package/src/components/diagnose/parts.test.tsx +3 -1
  60. package/src/components/diagnose/parts.tsx +26 -3904
  61. package/src/components/diagnose/target.ts +9 -3
  62. package/src/components/diagnose/toolCallLabel.test.ts +47 -1
  63. package/src/components/diagnose/toolCallLabel.ts +41 -2
  64. package/src/components/helm/HelmReleaseDrawer.tsx +1 -1
  65. package/src/components/home/MCPSetupDialog.tsx +1 -1
  66. package/src/components/resource/PrometheusChartsGrid.tsx +1 -1
  67. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +1 -1
  68. package/src/components/resource/WorkloadMetricsSection.tsx +1 -1
  69. package/src/components/resources/PodFilePreview.tsx +1 -1
  70. package/src/components/ui/CommandPalette.tsx +12 -8
  71. package/src/components/ui/DiagnosticsOverlay.tsx +7 -5
  72. package/src/components/ui/Omnibar.tsx +33 -10
  73. package/src/components/ui/ShortcutHelpOverlay.tsx +8 -5
  74. package/src/components/ui/Disclosure.tsx +0 -47
@@ -0,0 +1,475 @@
1
+ import { useContext } from "react";
2
+ import { clsx } from "clsx";
3
+ import {
4
+ Badge,
5
+ StatusDot,
6
+ formatRelativeAgeTime,
7
+ mapHealthToTone,
8
+ ResourceLink,
9
+ } from "@skyhook-io/k8s-ui";
10
+ import { apiVersionToGroup } from "../../../../utils/navigation";
11
+ import { Tooltip } from "../../../ui/Tooltip";
12
+ import { EvidenceNavigationContext } from "../navigation";
13
+ import { type EvidenceDataOf, ResourceFact, severityBadge } from "../cardParts";
14
+
15
+ function helmStatusSeverity(status: string) {
16
+ const normalized = status.toLowerCase();
17
+ if (normalized === "deployed") return "success" as const;
18
+ if (normalized.includes("failed")) return "error" as const;
19
+ if (normalized.startsWith("pending") || normalized === "uninstalling")
20
+ return "info" as const;
21
+ return "warning" as const;
22
+ }
23
+
24
+ // Scalars read as they are; a nested object shows the keys it holds, not its
25
+ // contents, which is enough to see what was overridden.
26
+ function helmValueText(value: unknown): string {
27
+ if (value === null) return "null";
28
+ if (typeof value !== "object") return String(value);
29
+ if (Array.isArray(value)) return `[${value.length} items]`;
30
+ const keys = Object.keys(value as Record<string, unknown>);
31
+ return `{ ${keys.slice(0, 6).join(", ")}${keys.length > 6 ? ", …" : ""} }`;
32
+ }
33
+
34
+ export function HelmBody({ data }: { data: EvidenceDataOf<"helm"> }) {
35
+ const { onOpenResource } = useContext(EvidenceNavigationContext);
36
+ const { release } = data;
37
+ const operation = release.lastOperation;
38
+ return (
39
+ <div className="space-y-3">
40
+ <div className="flex flex-wrap items-center gap-1.5">
41
+ <Badge severity={helmStatusSeverity(release.status)} size="sm">
42
+ {release.status}
43
+ </Badge>
44
+ <Badge tone="structural" size="sm">
45
+ {release.chart}
46
+ {release.chartVersion ? ` ${release.chartVersion}` : ""}
47
+ </Badge>
48
+ <Badge tone="structural" size="sm">
49
+ revision {release.revision}
50
+ </Badge>
51
+ {release.appVersion ? (
52
+ <Badge tone="note" size="sm">
53
+ app {release.appVersion}
54
+ </Badge>
55
+ ) : null}
56
+ {release.resourceHealth ? (
57
+ <Badge
58
+ severity={
59
+ mapHealthToTone(release.resourceHealth) === "healthy"
60
+ ? "success"
61
+ : mapHealthToTone(release.resourceHealth) === "unhealthy"
62
+ ? "error"
63
+ : "warning"
64
+ }
65
+ size="sm"
66
+ >
67
+ resources {release.resourceHealth}
68
+ </Badge>
69
+ ) : null}
70
+ <Tooltip
71
+ content={new Date(release.updated).toLocaleString()}
72
+ delay={150}
73
+ position="left"
74
+ wrapperClassName="ml-auto"
75
+ >
76
+ <time
77
+ dateTime={release.updated}
78
+ className="text-xs text-theme-text-tertiary"
79
+ >
80
+ updated {formatRelativeAgeTime(release.updated)}
81
+ </time>
82
+ </Tooltip>
83
+ </div>
84
+ {release.healthIssue ? (
85
+ <p className="text-xs leading-relaxed text-warning-text">
86
+ {release.healthIssue}
87
+ </p>
88
+ ) : null}
89
+ {release.healthSummary &&
90
+ release.healthSummary !== release.healthIssue ? (
91
+ <p className="text-xs leading-relaxed text-theme-text-secondary">
92
+ {release.healthSummary}
93
+ </p>
94
+ ) : null}
95
+ {operation ? (
96
+ <div className="rounded-md border border-theme-border bg-theme-base/40 px-2.5 py-2">
97
+ <div className="flex flex-wrap items-center gap-1.5">
98
+ <span className="text-xs font-semibold uppercase tracking-wide text-theme-text-tertiary">
99
+ Last operation
100
+ </span>
101
+ <Badge tone="note" size="sm">
102
+ {operation.kind.replaceAll("_", " ")}
103
+ </Badge>
104
+ <Badge
105
+ severity={
106
+ operation.status === "completed"
107
+ ? "success"
108
+ : operation.status === "failed"
109
+ ? "error"
110
+ : "warning"
111
+ }
112
+ size="sm"
113
+ >
114
+ {operation.status.replaceAll("_", " ")}
115
+ </Badge>
116
+ </div>
117
+ {operation.message ? (
118
+ <p className="mt-1 text-xs leading-relaxed text-theme-text-secondary [overflow-wrap:anywhere]">
119
+ {operation.message}
120
+ </p>
121
+ ) : null}
122
+ </div>
123
+ ) : null}
124
+ {release.description ? (
125
+ <p className="text-xs text-theme-text-tertiary [overflow-wrap:anywhere]">
126
+ {release.description}
127
+ </p>
128
+ ) : null}
129
+ {release.storageNamespace &&
130
+ release.storageNamespace !== release.namespace ? (
131
+ <p className="text-xs text-theme-text-tertiary">
132
+ Release metadata stored in namespace {release.storageNamespace}
133
+ </p>
134
+ ) : null}
135
+ {release.managedByFluxHelmRelease ? (
136
+ <p className="text-xs text-theme-text-tertiary">
137
+ Managed by Flux HelmRelease {release.managedByFluxHelmRelease}
138
+ </p>
139
+ ) : null}
140
+ {release.hooks && release.hooks.length > 0 ? (
141
+ <div>
142
+ <div className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-theme-text-tertiary">
143
+ Hooks
144
+ </div>
145
+ <div className="rounded-md border border-theme-border">
146
+ {release.hooks.map((hook, index) => (
147
+ <div
148
+ key={`${hook.kind}-${hook.namespace ?? ""}-${hook.name}-${index}`}
149
+ data-helm-hook
150
+ className={clsx(
151
+ "flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 px-2.5 py-1.5 text-xs",
152
+ index > 0 && "border-t border-theme-border/60",
153
+ )}
154
+ >
155
+ <Badge tone="structural" size="sm">
156
+ {hook.kind}
157
+ </Badge>
158
+ <span className="min-w-0 truncate font-mono text-theme-text-secondary">
159
+ {hook.name}
160
+ </span>
161
+ <span className="text-theme-text-tertiary">
162
+ {hook.events.join(", ")}
163
+ </span>
164
+ {hook.status ? (
165
+ <Badge severity={severityBadge(hook.status)} size="sm">
166
+ {hook.status}
167
+ </Badge>
168
+ ) : null}
169
+ {hook.completedAt ? (
170
+ <span className="ml-auto shrink-0 text-theme-text-tertiary">
171
+ {formatRelativeAgeTime(hook.completedAt)}
172
+ </span>
173
+ ) : null}
174
+ </div>
175
+ ))}
176
+ </div>
177
+ </div>
178
+ ) : null}
179
+ {release.history && release.history.length > 0 ? (
180
+ <div>
181
+ <div className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-theme-text-tertiary">
182
+ History
183
+ </div>
184
+ <div className="rounded-md border border-theme-border">
185
+ {release.history.map((rev, index) => (
186
+ <div
187
+ key={rev.revision}
188
+ data-helm-revision={rev.revision}
189
+ className={clsx(
190
+ "flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 px-2.5 py-1.5 text-xs",
191
+ index > 0 && "border-t border-theme-border/60",
192
+ rev.revision === release.revision && "bg-theme-hover/40",
193
+ )}
194
+ >
195
+ <span className="font-mono text-theme-text-primary">
196
+ rev {rev.revision}
197
+ </span>
198
+ <Badge severity={helmStatusSeverity(rev.status)} size="sm">
199
+ {rev.status}
200
+ </Badge>
201
+ <span className="font-mono text-theme-text-secondary">
202
+ {rev.chart}
203
+ </span>
204
+ {rev.description ? (
205
+ <span className="min-w-0 truncate text-theme-text-tertiary">
206
+ {rev.description}
207
+ </span>
208
+ ) : null}
209
+ <span className="ml-auto shrink-0 text-theme-text-tertiary">
210
+ {formatRelativeAgeTime(rev.updated)}
211
+ </span>
212
+ </div>
213
+ ))}
214
+ </div>
215
+ </div>
216
+ ) : null}
217
+ {release.values && Object.keys(release.values).length > 0 ? (
218
+ <div>
219
+ <div className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-theme-text-tertiary">
220
+ Values set by the user
221
+ </div>
222
+ <div className="rounded-md border border-theme-border font-mono text-xs">
223
+ {Object.entries(release.values)
224
+ .slice(0, 40)
225
+ .map(([key, value], index) => (
226
+ <div
227
+ key={key}
228
+ data-helm-value={key}
229
+ className={clsx(
230
+ "flex min-w-0 gap-2 px-2.5 py-1",
231
+ index > 0 && "border-t border-theme-border/60",
232
+ )}
233
+ >
234
+ <span className="shrink-0 text-theme-text-secondary">
235
+ {key}
236
+ </span>
237
+ <span className="min-w-0 truncate text-theme-text-primary">
238
+ {helmValueText(value)}
239
+ </span>
240
+ </div>
241
+ ))}
242
+ </div>
243
+ </div>
244
+ ) : null}
245
+ {release.resources.length > 0 ? (
246
+ <div className="max-h-52 overflow-y-auto rounded-md border border-theme-border">
247
+ {release.resources.map((owned, index) => (
248
+ <div
249
+ key={`${owned.kind}-${owned.namespace}-${owned.name}`}
250
+ className={clsx(
251
+ "flex min-w-0 items-center gap-2 px-2.5 py-1.5",
252
+ index > 0 && "border-t border-theme-border/60",
253
+ )}
254
+ >
255
+ <StatusDot
256
+ tone={mapHealthToTone(
257
+ owned.issue ? "unhealthy" : (owned.status ?? ""),
258
+ )}
259
+ className="shrink-0"
260
+ />
261
+ <Badge tone="structural" size="sm">
262
+ {owned.kind}
263
+ </Badge>
264
+ <span className="min-w-0 flex-1">
265
+ <span className="block truncate font-mono text-xs">
266
+ <ResourceLink
267
+ name={owned.name}
268
+ kind={owned.kind}
269
+ namespace={owned.namespace}
270
+ group={
271
+ owned.apiVersion
272
+ ? apiVersionToGroup(owned.apiVersion)
273
+ : undefined
274
+ }
275
+ label={`${owned.namespace ? `${owned.namespace}/` : ""}${owned.name}`}
276
+ onNavigate={
277
+ owned.apiVersion && onOpenResource
278
+ ? (ref) => onOpenResource(ref)
279
+ : undefined
280
+ }
281
+ />
282
+ </span>
283
+ {owned.issue ? (
284
+ <span className="block truncate text-xs text-warning-text">
285
+ {owned.issue}
286
+ </span>
287
+ ) : owned.summary || owned.message ? (
288
+ <span className="block truncate text-xs text-theme-text-tertiary">
289
+ {owned.summary || owned.message}
290
+ </span>
291
+ ) : null}
292
+ </span>
293
+ {owned.ready || owned.status ? (
294
+ <span className="ml-auto shrink-0 font-mono text-xs text-theme-text-tertiary">
295
+ {owned.ready || owned.status}
296
+ </span>
297
+ ) : null}
298
+ </div>
299
+ ))}
300
+ </div>
301
+ ) : null}
302
+ </div>
303
+ );
304
+ }
305
+
306
+ export function PermissionsBody({
307
+ data,
308
+ }: {
309
+ data: EvidenceDataOf<"permissions">;
310
+ }) {
311
+ const { subject, accessCheck } = data;
312
+ const subjectBadges = (
313
+ <>
314
+ <Badge tone="structural" size="sm">
315
+ {subject.kind}
316
+ </Badge>
317
+ <span className="font-mono text-xs text-theme-text-secondary">
318
+ {subject.namespace ? `${subject.namespace}/` : ""}
319
+ {subject.name}
320
+ </span>
321
+ </>
322
+ );
323
+ if (accessCheck) {
324
+ const facts = [
325
+ ["Verb", accessCheck.verb],
326
+ ["Resource", accessCheck.resource],
327
+ ["Subresource", accessCheck.subresource],
328
+ ["API group", accessCheck.group || "core"],
329
+ ["Namespace", accessCheck.namespace || "cluster-wide"],
330
+ ["Name", accessCheck.resourceName],
331
+ ] as const;
332
+ return (
333
+ <div className="space-y-2.5">
334
+ <div className="flex flex-wrap items-center gap-1.5">
335
+ {subjectBadges}
336
+ <Badge
337
+ severity={accessCheck.allowed ? "success" : "warning"}
338
+ size="sm"
339
+ >
340
+ {accessCheck.allowed
341
+ ? "allowed"
342
+ : accessCheck.denied
343
+ ? "denied"
344
+ : "not allowed"}
345
+ </Badge>
346
+ </div>
347
+ <dl className="flex flex-wrap gap-x-6 gap-y-2 text-xs">
348
+ {facts.map(([label, value]) => (
349
+ <ResourceFact key={label} label={label} value={value} />
350
+ ))}
351
+ </dl>
352
+ {accessCheck.reason ? (
353
+ <p className="text-xs leading-relaxed text-theme-text-secondary [overflow-wrap:anywhere]">
354
+ {accessCheck.reason}
355
+ </p>
356
+ ) : null}
357
+ {accessCheck.evaluationError ? (
358
+ <p className="text-xs leading-relaxed text-semantic-error">
359
+ {accessCheck.evaluationError}
360
+ </p>
361
+ ) : null}
362
+ </div>
363
+ );
364
+ }
365
+ const bindings = data.bindings ?? [];
366
+ const usedByPods = data.usedByPods ?? [];
367
+ return (
368
+ <div className="space-y-2.5">
369
+ <div className="flex flex-wrap items-center gap-1.5">
370
+ {subjectBadges}
371
+ <Badge tone="note" size="sm">
372
+ {data.flatRulesCount ?? 0}
373
+ {data.truncated ? "+" : ""} effective rules
374
+ </Badge>
375
+ {data.truncated ? (
376
+ <Badge severity="warning" size="sm">
377
+ rule list truncated
378
+ </Badge>
379
+ ) : null}
380
+ </div>
381
+ {bindings.length > 0 ? (
382
+ <div className="max-h-52 overflow-y-auto rounded-md border border-theme-border">
383
+ {bindings.map((binding, index) => (
384
+ <div
385
+ key={`${binding.bindingKind}-${binding.bindingNamespace ?? ""}-${binding.bindingName}`}
386
+ className={clsx(
387
+ "flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 px-2.5 py-1.5 text-xs",
388
+ index > 0 && "border-t border-theme-border/60",
389
+ )}
390
+ >
391
+ <Badge tone="structural" size="sm">
392
+ {binding.bindingKind}
393
+ </Badge>
394
+ <span className="min-w-0 truncate font-mono text-theme-text-secondary">
395
+ {binding.bindingNamespace ? `${binding.bindingNamespace}/` : ""}
396
+ {binding.bindingName}
397
+ </span>
398
+ <span className="text-theme-text-tertiary">→</span>
399
+ <Badge tone="structural" size="sm">
400
+ {binding.roleKind}
401
+ </Badge>
402
+ <span className="min-w-0 truncate font-mono text-theme-text-primary">
403
+ {binding.roleNamespace ? `${binding.roleNamespace}/` : ""}
404
+ {binding.roleName}
405
+ </span>
406
+ <span className="ml-auto shrink-0 font-mono text-theme-text-tertiary">
407
+ {binding.rulesCount} rule{binding.rulesCount === 1 ? "" : "s"}
408
+ </span>
409
+ {binding.inheritedFromGroup ? (
410
+ <Badge tone="note" size="sm">
411
+ via {binding.inheritedFromGroup}
412
+ </Badge>
413
+ ) : null}
414
+ </div>
415
+ ))}
416
+ </div>
417
+ ) : (
418
+ <p className="text-xs italic text-theme-text-tertiary">
419
+ No RoleBinding or ClusterRoleBinding grants this subject anything.
420
+ </p>
421
+ )}
422
+ {data.rules && data.rules.length > 0 ? (
423
+ <div>
424
+ <div className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-theme-text-tertiary">
425
+ Effective rules
426
+ </div>
427
+ <div className="max-h-60 overflow-y-auto rounded-md border border-theme-border">
428
+ {data.rules.slice(0, 60).map((rule, index) => (
429
+ <div
430
+ key={index}
431
+ data-permission-rule
432
+ className={clsx(
433
+ "flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 px-2.5 py-1.5 font-mono text-xs",
434
+ index > 0 && "border-t border-theme-border/60",
435
+ )}
436
+ >
437
+ <span className="text-theme-text-primary">
438
+ {rule.verbs.join(", ")}
439
+ </span>
440
+ <span className="text-theme-text-tertiary">on</span>
441
+ <span className="min-w-0 truncate text-theme-text-secondary">
442
+ {rule.nonResourceURLs?.length
443
+ ? rule.nonResourceURLs.join(", ")
444
+ : rule.resources.join(", ")}
445
+ {rule.resourceNames?.length
446
+ ? ` [${rule.resourceNames.join(", ")}]`
447
+ : ""}
448
+ </span>
449
+ {rule.apiGroups.length > 0 ? (
450
+ <span className="ml-auto shrink-0 text-theme-text-tertiary">
451
+ {rule.apiGroups.map((g) => g || "core").join(", ")}
452
+ </span>
453
+ ) : null}
454
+ </div>
455
+ ))}
456
+ {data.rules.length > 60 ? (
457
+ <div className="border-t border-theme-border/60 px-2.5 py-1.5 text-xs text-theme-text-tertiary">
458
+ {data.rules.length - 60} more rules in the result
459
+ </div>
460
+ ) : null}
461
+ </div>
462
+ </div>
463
+ ) : null}
464
+ {usedByPods.length > 0 ? (
465
+ <p className="text-xs leading-relaxed text-theme-text-secondary [overflow-wrap:anywhere]">
466
+ <span className="text-theme-text-tertiary">Used by pods: </span>
467
+ <span className="font-mono">{usedByPods.join(", ")}</span>
468
+ {data.podsTotal && data.podsTotal > usedByPods.length
469
+ ? ` and ${data.podsTotal - usedByPods.length} more`
470
+ : ""}
471
+ </p>
472
+ ) : null}
473
+ </div>
474
+ );
475
+ }
@@ -0,0 +1,60 @@
1
+ import { clsx } from "clsx";
2
+ import { Badge } from "@skyhook-io/k8s-ui";
3
+ import { severityBadge, type EvidenceDataOf } from "../cardParts";
4
+ import type { InvestigationPostureFinding } from "../types";
5
+
6
+ export function PostureFindingRow({
7
+ finding,
8
+ divider = false,
9
+ }: {
10
+ finding: InvestigationPostureFinding;
11
+ divider?: boolean;
12
+ }) {
13
+ return (
14
+ <div
15
+ data-posture-finding={finding.target ? "target" : undefined}
16
+ className={clsx(
17
+ "space-y-1 px-2.5 py-1.5 text-xs",
18
+ divider && "border-t border-theme-border/60",
19
+ )}
20
+ >
21
+ <div className="flex min-w-0 flex-wrap items-center gap-1.5">
22
+ <Badge severity={severityBadge(finding.severity)} size="sm">
23
+ {finding.severity}
24
+ </Badge>
25
+ <span className="font-medium text-theme-text-primary">
26
+ {finding.check}
27
+ </span>
28
+ {finding.category ? (
29
+ <span className="text-theme-text-tertiary">{finding.category}</span>
30
+ ) : null}
31
+ <span className="ml-auto min-w-0 truncate font-mono text-theme-text-tertiary">
32
+ {finding.kind} {finding.namespace ? `${finding.namespace}/` : ""}
33
+ {finding.name}
34
+ </span>
35
+ </div>
36
+ <p className="text-theme-text-secondary [overflow-wrap:anywhere]">
37
+ {finding.message}
38
+ </p>
39
+ {finding.remediation ? (
40
+ <p className="text-theme-text-tertiary [overflow-wrap:anywhere]">
41
+ {finding.remediation}
42
+ </p>
43
+ ) : null}
44
+ </div>
45
+ );
46
+ }
47
+
48
+ export function PostureBody({ data }: { data: EvidenceDataOf<"posture"> }) {
49
+ return (
50
+ <div className="max-h-80 overflow-y-auto rounded-md border border-theme-border">
51
+ {data.findings.map((finding, index) => (
52
+ <PostureFindingRow
53
+ key={`${finding.kind}-${finding.namespace ?? ""}-${finding.name}-${finding.check}`}
54
+ finding={finding}
55
+ divider={index > 0}
56
+ />
57
+ ))}
58
+ </div>
59
+ );
60
+ }
@@ -0,0 +1,76 @@
1
+ import { clsx } from "clsx";
2
+ import { Badge } from "@skyhook-io/k8s-ui";
3
+ import type { EvidenceDataOf } from "../cardParts";
4
+ import type { InvestigationRankingRow } from "../types";
5
+
6
+ export function RankingRow({
7
+ row,
8
+ divider = false,
9
+ }: {
10
+ row: InvestigationRankingRow;
11
+ divider?: boolean;
12
+ }) {
13
+ return (
14
+ <div
15
+ data-ranking-row={row.target ? "target" : undefined}
16
+ className={clsx(
17
+ "flex min-w-0 items-center gap-2 px-2.5 py-1.5 font-mono text-xs",
18
+ divider && "border-t border-theme-border/60",
19
+ row.target && "bg-theme-hover/40",
20
+ )}
21
+ >
22
+ <Badge tone="structural" size="sm">
23
+ {row.kind}
24
+ </Badge>
25
+ <span className="min-w-0 flex-1 truncate text-theme-text-secondary">
26
+ {row.namespace ? `${row.namespace}/` : ""}
27
+ <span className={row.target ? "text-theme-text-primary" : undefined}>
28
+ {row.name}
29
+ </span>
30
+ </span>
31
+ <span className="shrink-0 tabular-nums text-theme-text-primary">
32
+ {row.cpu}
33
+ {row.cpuLimit ? (
34
+ <span className="text-theme-text-tertiary">/{row.cpuLimit}</span>
35
+ ) : null}
36
+ </span>
37
+ <span className="shrink-0 tabular-nums text-theme-text-primary">
38
+ {row.memory}
39
+ {row.memoryLimit ? (
40
+ <span className="text-theme-text-tertiary">/{row.memoryLimit}</span>
41
+ ) : null}
42
+ </span>
43
+ {row.restarts ? (
44
+ <Badge severity="warning" size="sm">
45
+ {row.restarts} restarts
46
+ </Badge>
47
+ ) : null}
48
+ {row.target ? (
49
+ <Badge tone="note" size="sm">
50
+ this workload
51
+ </Badge>
52
+ ) : null}
53
+ </div>
54
+ );
55
+ }
56
+
57
+ export function RankingBody({ data }: { data: EvidenceDataOf<"ranking"> }) {
58
+ return (
59
+ <div className="space-y-1.5">
60
+ <div className="flex gap-2 px-2.5 text-[11px] text-theme-text-tertiary">
61
+ <span className="min-w-0 flex-1">ranked by {data.sort}</span>
62
+ <span className="shrink-0">cpu</span>
63
+ <span className="shrink-0">memory</span>
64
+ </div>
65
+ <div className="max-h-72 overflow-y-auto rounded-md border border-theme-border">
66
+ {data.rows.map((row, index) => (
67
+ <RankingRow
68
+ key={`${row.kind}-${row.namespace ?? ""}-${row.name}`}
69
+ row={row}
70
+ divider={index > 0}
71
+ />
72
+ ))}
73
+ </div>
74
+ </div>
75
+ );
76
+ }