@skyhook-io/k8s-ui 1.5.3 → 1.5.4

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 (44) hide show
  1. package/package.json +1 -1
  2. package/src/components/audit/AuditAlerts.tsx +6 -0
  3. package/src/components/audit/AuditFindingsTable.tsx +83 -33
  4. package/src/components/gitops/ManagedResourcesList.tsx +2 -1
  5. package/src/components/resources/ResourcesView.tsx +183 -52
  6. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +3 -2
  7. package/src/components/resources/renderers/CertificateRenderer.tsx +4 -3
  8. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -1
  9. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +4 -3
  10. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +2 -1
  11. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +2 -1
  12. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +4 -3
  13. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +3 -2
  14. package/src/components/resources/renderers/SecretRenderer.tsx +10 -4
  15. package/src/components/resources/renderers/trivy-shared.tsx +3 -2
  16. package/src/components/resources/resource-utils-istio.ts +4 -3
  17. package/src/components/resources/resource-utils.ts +10 -2
  18. package/src/components/timeline/TimelineList.tsx +2 -1
  19. package/src/components/topology/GroupNode.tsx +2 -1
  20. package/src/components/topology/TopologyGraph.tsx +2 -1
  21. package/src/components/ui/Badge.tsx +2 -1
  22. package/src/components/ui/ClusterName.tsx +95 -0
  23. package/src/components/ui/EmptyState.tsx +118 -0
  24. package/src/components/ui/FilterPill.tsx +98 -0
  25. package/src/components/ui/ForceDeleteConfirmDialog.tsx +2 -1
  26. package/src/components/ui/ResourceBar.tsx +1 -1
  27. package/src/components/ui/Tooltip.tsx +43 -0
  28. package/src/components/ui/drawer-components.tsx +11 -0
  29. package/src/components/ui/index.ts +7 -0
  30. package/src/components/ui/provider-logos/aws-dark.png +0 -0
  31. package/src/components/ui/provider-logos/aws.png +0 -0
  32. package/src/components/ui/provider-logos/azure.svg +23 -0
  33. package/src/components/ui/provider-logos/gcp.png +0 -0
  34. package/src/components/ui/status-tone.test.ts +53 -0
  35. package/src/components/ui/status-tone.tsx +104 -0
  36. package/src/components/workload/WorkloadView.tsx +3 -3
  37. package/src/theme/components.css +7 -0
  38. package/src/utils/badge-colors.ts +1 -0
  39. package/src/utils/context-name.test.ts +106 -0
  40. package/src/utils/context-name.ts +25 -6
  41. package/src/utils/index.ts +1 -0
  42. package/src/utils/navigation.ts +4 -8
  43. package/src/utils/pluralize.test.ts +98 -0
  44. package/src/utils/pluralize.ts +60 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyhook-io/k8s-ui",
3
- "version": "1.5.3",
3
+ "version": "1.5.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/skyhook-io/radar",
@@ -11,6 +11,12 @@ export interface AuditFinding {
11
11
  category: string
12
12
  severity: string
13
13
  message: string
14
+ /** Cluster context — set when findings come from multiple clusters
15
+ * (cross-cluster aggregation). Rendered as a Cluster column when the
16
+ * host passes `multiCluster` to AuditFindingsTable. Grouped as one
17
+ * optional object so id and name stay in lockstep — `name` is
18
+ * meaningless without `id`. */
19
+ cluster?: { id: string; name: string }
14
20
  }
15
21
 
16
22
  interface AuditAlertsProps {
@@ -1,8 +1,11 @@
1
1
  import { useState, useMemo, useRef, useEffect } from 'react'
2
- import { ShieldAlert, AlertTriangle, ChevronRight, Search, ExternalLink, MoreHorizontal, EyeOff, Layers } from 'lucide-react'
2
+ import { ShieldAlert, AlertTriangle, ChevronRight, CheckCircle2, Search, ExternalLink, MoreHorizontal, EyeOff, Layers } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
4
  import type { AuditFinding } from './AuditAlerts'
5
5
  import { SEVERITY_TEXT, BP_CATEGORY_BADGE, DEFAULT_BADGE_COLOR } from '../../utils/badge-colors'
6
+ import { EmptyState } from '../ui/EmptyState'
7
+ import { FilterPill } from '../ui/FilterPill'
8
+ import { pluralize } from '../../utils/pluralize'
6
9
 
7
10
  const CATEGORIES = ['Security', 'Reliability', 'Efficiency'] as const
8
11
  const SEVERITIES = ['danger', 'warning'] as const
@@ -32,9 +35,19 @@ export interface AuditFindingsTableProps {
32
35
  onHideCheck?: (checkID: string, title: string) => void
33
36
  onHideCategory?: (category: string) => void
34
37
  onHideNamespace?: (namespace: string) => void
38
+ /** When true, render a Cluster column on flat findings rows. Set this
39
+ * when findings come from multiple clusters (cross-cluster aggregation).
40
+ * Each finding's clusterId/clusterName fields populate the column.
41
+ * (Currently flat-rows only; grouped views don't surface a Cluster
42
+ * column today.) */
43
+ multiCluster?: boolean
44
+ /** Click-through for cluster-name links in the multi-cluster view.
45
+ * Defaults to no-op; multi-cluster hosts pass a navigator that opens
46
+ * the cluster's per-cluster audit page. */
47
+ onClusterClick?: (clusterId: string) => void
35
48
  }
36
49
 
37
- export function AuditFindingsTable({ groups, findings, checks, onResourceClick, onHideCheck, onHideCategory, onHideNamespace }: AuditFindingsTableProps) {
50
+ export function AuditFindingsTable({ groups, findings, checks, onResourceClick, onHideCheck, onHideCategory, onHideNamespace, multiCluster, onClusterClick }: AuditFindingsTableProps) {
38
51
  const [categoryFilter, setCategoryFilter] = useState<string | null>(null)
39
52
  const [severityFilter, setSeverityFilter] = useState<string | null>(null)
40
53
  const [frameworkFilter, setFrameworkFilter] = useState<string | null>(null)
@@ -230,22 +243,28 @@ export function AuditFindingsTable({ groups, findings, checks, onResourceClick,
230
243
  )}
231
244
  </div>
232
245
 
233
- {/* Row 2: Filter chips */}
234
- <div className="flex flex-wrap items-center gap-1">
235
- <FilterChip label="All" active={!categoryFilter && !severityFilter && !frameworkFilter} onClick={() => { setCategoryFilter(null); setSeverityFilter(null); setFrameworkFilter(null) }} />
236
- <span className="w-px h-4 bg-theme-text-tertiary/30 mx-3" />
246
+ {/* Row 2: Filter chips — three groups separated by dividers (All | Categories | Severities | Frameworks). */}
247
+ <div className="flex flex-wrap items-center gap-1.5">
248
+ <FilterPill label="All" active={!categoryFilter && !severityFilter && !frameworkFilter} onClick={() => { setCategoryFilter(null); setSeverityFilter(null); setFrameworkFilter(null) }} />
249
+ <span className="w-px h-5 bg-theme-border mx-2" />
237
250
  {CATEGORIES.map(cat => (
238
- <FilterChip key={cat} label={cat} active={categoryFilter === cat} onClick={() => setCategoryFilter(categoryFilter === cat ? null : cat)} />
251
+ <FilterPill key={cat} label={cat} active={categoryFilter === cat} onClick={() => setCategoryFilter(categoryFilter === cat ? null : cat)} />
239
252
  ))}
240
- <span className="w-px h-4 bg-theme-text-tertiary/30 mx-3" />
253
+ <span className="w-px h-5 bg-theme-border mx-2" />
241
254
  {SEVERITIES.map(sev => (
242
- <FilterChip key={sev} label={sev === 'danger' ? 'Critical' : 'Warning'} active={severityFilter === sev} onClick={() => setSeverityFilter(severityFilter === sev ? null : sev)} />
255
+ <FilterPill
256
+ key={sev}
257
+ label={sev === 'danger' ? 'Critical' : 'Warning'}
258
+ active={severityFilter === sev}
259
+ tone={sev === 'danger' ? 'danger' : 'warn'}
260
+ onClick={() => setSeverityFilter(severityFilter === sev ? null : sev)}
261
+ />
243
262
  ))}
244
263
  {frameworks.length > 0 && (
245
264
  <>
246
- <span className="w-px h-4 bg-theme-text-tertiary/30 mx-3" />
265
+ <span className="w-px h-5 bg-theme-border mx-2" />
247
266
  {frameworks.map(fw => (
248
- <FilterChip key={fw} label={fw} active={frameworkFilter === fw} onClick={() => setFrameworkFilter(frameworkFilter === fw ? null : fw)} />
267
+ <FilterPill key={fw} label={fw} active={frameworkFilter === fw} onClick={() => setFrameworkFilter(frameworkFilter === fw ? null : fw)} />
249
268
  ))}
250
269
  </>
251
270
  )}
@@ -254,9 +273,33 @@ export function AuditFindingsTable({ groups, findings, checks, onResourceClick,
254
273
 
255
274
  {/* Content */}
256
275
  {isEmpty ? (
257
- <div className="flex items-center justify-center py-12 text-theme-text-tertiary text-sm">
258
- {totalEmpty ? 'All checks passing no issues found' : 'No findings match the current filters'}
259
- </div>
276
+ totalEmpty ? (
277
+ // Healthy statecommunicates "you're winning", not "no data".
278
+ // Replaces a grey one-liner that conflated both meanings.
279
+ <EmptyState
280
+ tone="healthy"
281
+ variant="card"
282
+ icon={CheckCircle2}
283
+ headline="All checks passing"
284
+ body="No issues found across these resources."
285
+ />
286
+ ) : (
287
+ <EmptyState
288
+ tone="filtered"
289
+ variant="card"
290
+ headline="No findings match the current filters"
291
+ body="Try clearing one or more filters to see more results."
292
+ action={
293
+ <button
294
+ type="button"
295
+ onClick={() => { setCategoryFilter(null); setSeverityFilter(null); setFrameworkFilter(null); setSearchTerm('') }}
296
+ className="badge badge-sm border border-theme-border bg-theme-elevated text-theme-text-primary hover:bg-theme-hover transition-colors"
297
+ >
298
+ Clear all filters
299
+ </button>
300
+ }
301
+ />
302
+ )
260
303
  ) : namespacedGroups ? (
261
304
  /* Namespace-grouped view */
262
305
  <div className="flex flex-col gap-1">
@@ -272,7 +315,7 @@ export function AuditFindingsTable({ groups, findings, checks, onResourceClick,
272
315
  >
273
316
  <ChevronRight className={clsx('w-4 h-4 text-theme-text-tertiary shrink-0 transition-transform duration-200', nsExpanded && 'rotate-90')} />
274
317
  <span className="text-sm font-semibold text-theme-text-primary">{ns}</span>
275
- <span className="text-xs text-theme-text-tertiary">{nsGroups.length} resource{nsGroups.length !== 1 ? 's' : ''}</span>
318
+ <span className="text-xs text-theme-text-tertiary">{pluralize(nsGroups.length, 'resource')}</span>
276
319
  <span className="flex-1" />
277
320
  <div className="flex items-center gap-3 shrink-0">
278
321
  {nsDanger > 0 && <span className={clsx('text-xs font-semibold tabular-nums', SEVERITY_TEXT.error)}>{nsDanger} critical</span>}
@@ -309,7 +352,13 @@ export function AuditFindingsTable({ groups, findings, checks, onResourceClick,
309
352
  /* Flat fallback (per-resource view) */
310
353
  <div className="flex flex-col gap-1">
311
354
  {filteredFindings?.map((f, i) => (
312
- <FlatFindingRow key={`${f.checkID}-${i}`} finding={f} onResourceClick={onResourceClick} />
355
+ <FlatFindingRow
356
+ key={`${f.cluster?.id ?? ''}-${f.checkID}-${i}`}
357
+ finding={f}
358
+ onResourceClick={onResourceClick}
359
+ showCluster={multiCluster}
360
+ onClusterClick={onClusterClick}
361
+ />
313
362
  ))}
314
363
  </div>
315
364
  )}
@@ -425,7 +474,7 @@ function ResourceGroupRow({ group: g, checks, expanded, onToggle, onResourceClic
425
474
  )
426
475
  }
427
476
 
428
- function FlatFindingRow({ finding, onResourceClick }: { finding: AuditFinding; onResourceClick?: (kind: string, namespace: string, name: string) => void }) {
477
+ function FlatFindingRow({ finding, onResourceClick, showCluster, onClusterClick }: { finding: AuditFinding; onResourceClick?: (kind: string, namespace: string, name: string) => void; showCluster?: boolean; onClusterClick?: (clusterId: string) => void }) {
429
478
  const isDanger = finding.severity === 'danger'
430
479
  const severityColor = isDanger ? SEVERITY_TEXT.error : SEVERITY_TEXT.warning
431
480
 
@@ -436,6 +485,23 @@ function FlatFindingRow({ finding, onResourceClick }: { finding: AuditFinding; o
436
485
  ) : (
437
486
  <AlertTriangle className={clsx('w-4 h-4 shrink-0', severityColor)} />
438
487
  )}
488
+ {showCluster && finding.cluster && (
489
+ // Cluster column for multi-cluster contexts. Renders before
490
+ // the kind/namespace/name path so cluster scope is established
491
+ // first in the read order.
492
+ onClusterClick ? (
493
+ <button
494
+ onClick={() => onClusterClick(finding.cluster!.id)}
495
+ className="text-xs text-[var(--color-radar-accent)] hover:underline shrink-0 max-w-[160px] truncate text-left"
496
+ >
497
+ {finding.cluster.name}
498
+ </button>
499
+ ) : (
500
+ <span className="text-xs text-theme-text-secondary shrink-0 max-w-[160px] truncate">
501
+ {finding.cluster.name}
502
+ </span>
503
+ )
504
+ )}
439
505
  {onResourceClick ? (
440
506
  <button
441
507
  onClick={() => onResourceClick(finding.kind, finding.namespace, finding.name)}
@@ -465,22 +531,6 @@ function SummaryBadge({ label, count, color }: { label: string; count: number; c
465
531
  )
466
532
  }
467
533
 
468
- function FilterChip({ label, active, onClick }: { label: string; active: boolean; onClick: () => void }) {
469
- return (
470
- <button
471
- onClick={onClick}
472
- className={clsx(
473
- 'px-2 py-0.5 text-xs rounded-md transition-colors focus-visible:ring-2 focus-visible:ring-theme-text-primary/20 focus-visible:outline-none',
474
- active
475
- ? 'bg-theme-text-primary/10 text-theme-text-primary font-medium'
476
- : 'text-theme-text-tertiary hover:text-theme-text-secondary hover:bg-theme-hover'
477
- )}
478
- >
479
- {label}
480
- </button>
481
- )
482
- }
483
-
484
534
  interface ContextMenuItem {
485
535
  label: string
486
536
  onClick: () => void
@@ -3,6 +3,7 @@ import { ChevronDown, ChevronRight, Package, ExternalLink } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
4
  import type { ManagedResource, GitOpsHealthStatus } from '../../types/gitops'
5
5
  import { groupManagedResourcesByKind } from '../../types/gitops'
6
+ import { pluralize } from '../../utils/pluralize'
6
7
 
7
8
  interface ManagedResourcesListProps {
8
9
  resources: ManagedResource[]
@@ -204,7 +205,7 @@ export function InventoryCount({ count, healthy, unhealthy }: { count: number; h
204
205
 
205
206
  return (
206
207
  <span className="text-xs text-theme-text-secondary">
207
- {count} {count === 1 ? 'resource' : 'resources'}
208
+ {pluralize(count, 'resource')}
208
209
  </span>
209
210
  )
210
211
  }