@skyhook-io/k8s-ui 1.8.10 → 1.8.11

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 (116) hide show
  1. package/package.json +5 -4
  2. package/src/assets/gitops/argocd.png +0 -0
  3. package/src/assets/gitops/flux.svg +7 -0
  4. package/src/components/applications/AppChips.tsx +10 -0
  5. package/src/components/applications/ApplicationDetail.test.tsx +569 -0
  6. package/src/components/applications/ApplicationDetail.tsx +2517 -450
  7. package/src/components/applications/ApplicationsView.tsx +62 -10
  8. package/src/components/applications/applications-view.test.tsx +47 -0
  9. package/src/components/applications/index.ts +1 -1
  10. package/src/components/audit/AuditAlerts.tsx +14 -10
  11. package/src/components/checks/ChecksView.tsx +2 -3
  12. package/src/components/checks/severity.ts +14 -10
  13. package/src/components/cluster-switcher/ClusterSwitcher.tsx +2 -2
  14. package/src/components/compare/CompareResourcePicker.tsx +2 -2
  15. package/src/components/gitops/GitOpsGraphFilterRail.tsx +2 -1
  16. package/src/components/gitops/GitOpsTableView.tsx +3 -3
  17. package/src/components/gitops/ManagedResourcesList.tsx +8 -11
  18. package/src/components/gitops/SyncCountdown.tsx +26 -17
  19. package/src/components/gitops/SyncOptionsDialog.tsx +2 -2
  20. package/src/components/gitops/insights/GitOpsInsightViews.tsx +277 -104
  21. package/src/components/gitops/insights/insights-helpers.test.ts +86 -1
  22. package/src/components/gitops/insights/insights-helpers.ts +69 -1
  23. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +5 -2
  24. package/src/components/issues/IssuesView.tsx +51 -38
  25. package/src/components/issues/ResourceIssuesSection.test.tsx +6 -5
  26. package/src/components/issues/ResourceIssuesSection.tsx +28 -146
  27. package/src/components/issues/index.ts +3 -0
  28. package/src/components/issues/issue-timing.ts +53 -0
  29. package/src/components/issues/issues.test.ts +102 -1
  30. package/src/components/issues/severity.ts +2 -0
  31. package/src/components/logs/LogCore.tsx +14 -4
  32. package/src/components/logs/WorkloadLogsViewer.tsx +31 -6
  33. package/src/components/logs/useLogStream.ts +8 -1
  34. package/src/components/namespace-switcher/NamespacePicker.tsx +86 -107
  35. package/src/components/resources/ResourcesSidebar.test.tsx +71 -0
  36. package/src/components/resources/ResourcesSidebar.tsx +46 -46
  37. package/src/components/resources/ResourcesView.selected-kind.test.ts +143 -0
  38. package/src/components/resources/ResourcesView.tsx +233 -51
  39. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
  40. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +99 -0
  41. package/src/components/resources/renderers/JobRenderer.tsx +3 -1
  42. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +2 -2
  43. package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
  44. package/src/components/resources/renderers/SecretRenderer.tsx +2 -0
  45. package/src/components/resources/renderers/ServiceRenderer.tsx +58 -40
  46. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
  47. package/src/components/resources/renderers/WorkflowRenderer.test.tsx +43 -0
  48. package/src/components/resources/renderers/WorkflowRenderer.tsx +60 -78
  49. package/src/components/resources/renderers/index.ts +1 -0
  50. package/src/components/resources/resource-utils-capi.ts +1 -1
  51. package/src/components/resources/resource-utils.ts +31 -0
  52. package/src/components/shared/DetailShell.tsx +2 -2
  53. package/src/components/shared/ResourceActionsBar.tsx +140 -108
  54. package/src/components/shared/ResourceRendererDispatch.tsx +33 -4
  55. package/src/components/timeline/TimelineList.tsx +371 -354
  56. package/src/components/timeline/TimelineStrip.test.tsx +194 -0
  57. package/src/components/timeline/TimelineStrip.tsx +836 -0
  58. package/src/components/timeline/TimelineSwimlanes.test.tsx +1028 -0
  59. package/src/components/timeline/TimelineSwimlanes.tsx +2498 -593
  60. package/src/components/timeline/TimelineToolbar.test.tsx +320 -0
  61. package/src/components/timeline/TimelineToolbar.tsx +759 -0
  62. package/src/components/timeline/TimelineToolbarState.test.tsx +64 -0
  63. package/src/components/timeline/index.ts +23 -0
  64. package/src/components/timeline/scrubber-math.test.ts +235 -0
  65. package/src/components/timeline/scrubber-math.ts +249 -0
  66. package/src/components/timeline/shared.test.tsx +219 -0
  67. package/src/components/timeline/shared.tsx +232 -447
  68. package/src/components/timeline/timeline-filters.test.ts +196 -0
  69. package/src/components/timeline/timeline-filters.ts +180 -0
  70. package/src/components/timeline/timeline-lane-sort.test.ts +97 -0
  71. package/src/components/timeline/timeline-lane-sort.ts +77 -0
  72. package/src/components/timeline/timeline-live.test.ts +131 -0
  73. package/src/components/timeline/timeline-live.ts +77 -0
  74. package/src/components/topology/GroupNode.tsx +9 -2
  75. package/src/components/topology/K8sResourceNode.test.ts +11 -0
  76. package/src/components/topology/K8sResourceNode.tsx +387 -243
  77. package/src/components/topology/TopologyFilterSidebar.tsx +9 -0
  78. package/src/components/topology/TopologyGraph.tsx +89 -16
  79. package/src/components/topology/TopologySearch.tsx +2 -2
  80. package/src/components/topology/layout.ts +10 -2
  81. package/src/components/topology/topology.css +0 -92
  82. package/src/components/ui/Badge.tsx +5 -0
  83. package/src/components/ui/CodeViewer.tsx +2 -2
  84. package/src/components/ui/Collapse.tsx +59 -0
  85. package/src/components/ui/ConfirmDialog.tsx +7 -3
  86. package/src/components/ui/DialogPortal.tsx +31 -9
  87. package/src/components/ui/Input.tsx +24 -0
  88. package/src/components/ui/MultiSelectPicker.test.tsx +101 -0
  89. package/src/components/ui/MultiSelectPicker.tsx +167 -0
  90. package/src/components/ui/PaneLoader.tsx +10 -2
  91. package/src/components/ui/ResourceBar.tsx +68 -25
  92. package/src/components/ui/SearchBox.tsx +18 -4
  93. package/src/components/ui/SearchPillInput.tsx +2 -2
  94. package/src/components/ui/drawer-components.tsx +39 -26
  95. package/src/components/ui/index.ts +4 -0
  96. package/src/components/ui/severity-tone.ts +11 -4
  97. package/src/components/workload/ResourceDetailDrawer.tsx +6 -2
  98. package/src/components/workload/WorkloadView.tsx +2682 -472
  99. package/src/components/workload/index.ts +1 -1
  100. package/src/theme/variables.css +1 -1
  101. package/src/types/core.ts +57 -4
  102. package/src/utils/application-topology.test.ts +198 -0
  103. package/src/utils/application-topology.ts +261 -0
  104. package/src/utils/applications.test.ts +167 -1
  105. package/src/utils/applications.ts +1157 -393
  106. package/src/utils/index.ts +2 -0
  107. package/src/utils/navigation.test.ts +32 -1
  108. package/src/utils/navigation.ts +70 -0
  109. package/src/utils/resource-hierarchy.test.ts +990 -1
  110. package/src/utils/resource-hierarchy.ts +905 -267
  111. package/src/utils/resource-icons.ts +2 -0
  112. package/src/utils/topology-neighborhood.test.ts +202 -1
  113. package/src/utils/topology-neighborhood.ts +194 -13
  114. package/src/utils/workflow-execution.test.ts +556 -0
  115. package/src/utils/workflow-execution.ts +814 -0
  116. package/src/utils/workload-colors.ts +2 -3
@@ -14,6 +14,7 @@ import type { APIResource } from '../../types'
14
14
  import { categorizeResources, CORE_RESOURCES } from '../../utils/api-resources'
15
15
  import { getResourceIcon } from '../../utils/resource-icons'
16
16
  import { Tooltip } from '../ui/Tooltip'
17
+ import { Input } from '../ui/Input'
17
18
 
18
19
  // Selected resource type info (need both name for API and kind for display)
19
20
  export interface SelectedKindInfo {
@@ -52,29 +53,7 @@ export interface ResourcesSidebarProps {
52
53
 
53
54
  // Persisted across remounts so collapsed categories survive tab switches
54
55
  let persistedExpandedCategories: Set<string> | null = null
55
-
56
- // Core kinds that are always shown even with 0 instances
57
- // These are the most commonly used Kubernetes resources (using Kind names, not plural names)
58
- const ALWAYS_SHOWN_KINDS = new Set([
59
- 'Pod',
60
- 'Deployment',
61
- 'DaemonSet',
62
- 'StatefulSet',
63
- 'ReplicaSet',
64
- 'Service',
65
- 'Ingress',
66
- 'ConfigMap',
67
- 'Secret',
68
- 'Job',
69
- 'CronJob',
70
- 'HorizontalPodAutoscaler',
71
- 'PersistentVolumeClaim',
72
- 'Node',
73
- 'Namespace',
74
- 'ServiceAccount',
75
- 'NetworkPolicy',
76
- 'Event',
77
- ])
56
+ const COUNT_UNAVAILABLE_MESSAGE = 'Count unavailable. Open to view resources.'
78
57
 
79
58
  // Fallback resource types when API resources aren't loaded yet
80
59
  const CORE_RESOURCE_TYPES = [
@@ -173,15 +152,25 @@ const ResourceTypeButton = forwardRef<HTMLButtonElement, ResourceTypeButtonProps
173
152
  <Tooltip content="Insufficient permissions" position="left">
174
153
  <Shield className="w-3.5 h-3.5 text-amber-400/60" />
175
154
  </Tooltip>
155
+ ) : count === null ? (
156
+ <Tooltip content={COUNT_UNAVAILABLE_MESSAGE} position="left">
157
+ <span
158
+ className={clsx(
159
+ 'text-xs py-0.5 rounded text-center font-mono w-8 text-theme-text-disabled',
160
+ isSelected ? 'bg-skyhook-500/30 selection-text' : 'bg-theme-elevated',
161
+ )}
162
+ aria-label={COUNT_UNAVAILABLE_MESSAGE}
163
+ >
164
+ –
165
+ </span>
166
+ </Tooltip>
176
167
  ) : (
177
168
  <span className={clsx(
178
169
  'text-xs py-0.5 rounded text-center font-mono',
179
170
  isSelected ? 'bg-skyhook-500/30 selection-text' : 'bg-theme-elevated',
180
- count === null
181
- ? 'w-8 text-theme-text-disabled'
182
- : count < 1000 ? 'w-8' : 'w-9',
171
+ count < 1000 ? 'w-8' : 'w-9',
183
172
  )}>
184
- {count === null ? '–' : count}
173
+ {count}
185
174
  </span>
186
175
  )}
187
176
  </div>
@@ -299,7 +288,7 @@ export function ResourcesSidebar({
299
288
  } else if (key in resourceCounts) {
300
289
  results[key] = resourceCounts[key] ?? null
301
290
  } else {
302
- results[key] = 0
291
+ results[key] = null
303
292
  }
304
293
  }
305
294
  return results
@@ -326,14 +315,14 @@ export function ResourcesSidebar({
326
315
  0
327
316
  )
328
317
 
329
- // Filter resources: show if has instances, is core kind, has an
330
- // unknown count (loading — don't pre-emptively hide), or
331
- // showEmptyKinds is true.
318
+ // Filter resources: hide only confirmed-empty kinds. Unknown counts stay
319
+ // visible as a dash so count coverage gaps do not masquerade as emptiness.
332
320
  const visibleResources = category.resources.filter(resource => {
333
321
  const count = counts[resource.group ? `${resource.group}/${resource.kind}` : resource.kind]
334
- const isCore = ALWAYS_SHOWN_KINDS.has(resource.kind)
335
- const isLoading = count === null
336
- const shouldShow = (count ?? 0) > 0 || isCore || isLoading || showEmptyKinds
322
+ const isSelectedResource =
323
+ (effectiveSelectedKind.name === resource.name && effectiveSelectedKind.group === resource.group) ||
324
+ (effectiveSelectedKind.kind.toLowerCase() === resource.kind.toLowerCase() && effectiveSelectedKind.group === resource.group)
325
+ const shouldShow = isSelectedResource || count === null || (count ?? 0) > 0 || showEmptyKinds
337
326
  if (!shouldShow) totalHiddenKinds++
338
327
  return shouldShow
339
328
  })
@@ -348,16 +337,15 @@ export function ResourcesSidebar({
348
337
  return 0
349
338
  })
350
339
 
351
- // Filter out empty groups unless they have visible resources (core kinds) or showEmptyKinds is true
340
+ // Filter out empty groups unless they have visible resources or showEmptyKinds is true.
352
341
  const visibleCategories = sorted.filter(category => {
353
- // Show if: has resources with instances, OR has visible resources (core kinds), OR showEmptyKinds
354
342
  const shouldShow = category.total > 0 || category.visibleResources.length > 0 || showEmptyKinds
355
343
  if (!shouldShow) totalHiddenGroups++
356
344
  return shouldShow
357
345
  })
358
346
 
359
347
  return { sortedCategories: visibleCategories, hiddenKindsCount: totalHiddenKinds, hiddenGroupsCount: totalHiddenGroups }
360
- }, [categories, counts, showEmptyKinds])
348
+ }, [categories, counts, showEmptyKinds, effectiveSelectedKind.name, effectiveSelectedKind.kind, effectiveSelectedKind.group])
361
349
 
362
350
  // Filter sidebar categories/kinds by the kind search term
363
351
  const filteredCategories = useMemo(() => {
@@ -489,9 +477,8 @@ export function ResourcesSidebar({
489
477
  <div className="px-2 py-3 border-b border-theme-border">
490
478
  <div className="relative">
491
479
  <Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-theme-text-tertiary" />
492
- <input
480
+ <Input
493
481
  ref={kindSearchRef}
494
- type="text"
495
482
  placeholder="Filter resources..."
496
483
  value={kindFilter}
497
484
  onChange={(e) => setKindFilter(e.target.value)}
@@ -647,13 +634,26 @@ export function ResourcesSidebar({
647
634
  >
648
635
  <Icon className="w-4 h-4 shrink-0" />
649
636
  <span className="flex-1 text-left">{type.label}</span>
650
- <span className={clsx(
651
- 'badge font-mono',
652
- isSelected ? 'bg-skyhook-500/30 selection-text' : 'bg-theme-elevated',
653
- count === null && 'text-theme-text-disabled',
654
- )}>
655
- {count === null ? '–' : count}
656
- </span>
637
+ {count === null ? (
638
+ <Tooltip content={COUNT_UNAVAILABLE_MESSAGE} position="left">
639
+ <span
640
+ className={clsx(
641
+ 'badge font-mono text-theme-text-disabled',
642
+ isSelected ? 'bg-skyhook-500/30 selection-text' : 'bg-theme-elevated',
643
+ )}
644
+ aria-label={COUNT_UNAVAILABLE_MESSAGE}
645
+ >
646
+ –
647
+ </span>
648
+ </Tooltip>
649
+ ) : (
650
+ <span className={clsx(
651
+ 'badge font-mono',
652
+ isSelected ? 'bg-skyhook-500/30 selection-text' : 'bg-theme-elevated',
653
+ )}>
654
+ {count}
655
+ </span>
656
+ )}
657
657
  </button>
658
658
  )
659
659
  })
@@ -0,0 +1,143 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { APIResource } from '../../types'
3
+ import {
4
+ canonicalizeSelectedKind,
5
+ deriveSidebarResourceCounts,
6
+ LOADED_RESOURCE_COUNT_TTL_MS,
7
+ resourceQueryMatchesSelectedKind,
8
+ } from './ResourcesView'
9
+
10
+ const endpoints: APIResource = {
11
+ group: '',
12
+ version: 'v1',
13
+ kind: 'Endpoints',
14
+ name: 'endpoints',
15
+ namespaced: true,
16
+ isCrd: false,
17
+ verbs: ['list', 'get', 'watch'],
18
+ }
19
+
20
+ const httpRoute: APIResource = {
21
+ group: 'gateway.networking.k8s.io',
22
+ version: 'v1',
23
+ kind: 'HTTPRoute',
24
+ name: 'httproutes',
25
+ namespaced: true,
26
+ isCrd: true,
27
+ verbs: ['list', 'get', 'watch'],
28
+ }
29
+
30
+ describe('canonicalizeSelectedKind', () => {
31
+ it('restores the canonical Kind when URL hydration only has the plural resource name', () => {
32
+ expect(
33
+ canonicalizeSelectedKind(
34
+ { name: 'endpoints', kind: 'endpoints', group: '' },
35
+ [endpoints],
36
+ [endpoints]
37
+ )
38
+ ).toEqual({ name: 'endpoints', kind: 'Endpoints', group: '' })
39
+ })
40
+
41
+ it('leaves an already canonical selection alone', () => {
42
+ expect(
43
+ canonicalizeSelectedKind(
44
+ { name: 'endpoints', kind: 'Endpoints', group: '' },
45
+ [endpoints],
46
+ [endpoints]
47
+ )
48
+ ).toBeNull()
49
+ })
50
+
51
+ it('resolves CRD deep links from Kind to plural resource name', () => {
52
+ expect(
53
+ canonicalizeSelectedKind(
54
+ { name: 'HTTPRoute', kind: 'HTTPRoute', group: 'gateway.networking.k8s.io' },
55
+ [],
56
+ [httpRoute]
57
+ )
58
+ ).toEqual({ name: 'httproutes', kind: 'HTTPRoute', group: 'gateway.networking.k8s.io' })
59
+ })
60
+ })
61
+
62
+ describe('deriveSidebarResourceCounts', () => {
63
+ it('keeps recently loaded counts for resources omitted from resource-counts', () => {
64
+ const now = 10_000
65
+
66
+ expect(
67
+ deriveSidebarResourceCounts(
68
+ [endpoints],
69
+ {},
70
+ undefined,
71
+ { Endpoints: { count: 132, expiresAt: now + LOADED_RESOURCE_COUNT_TTL_MS } },
72
+ now
73
+ )
74
+ ).toEqual({ Endpoints: 132 })
75
+ })
76
+
77
+ it('expires loaded counts after the TTL', () => {
78
+ const now = 10_000
79
+
80
+ expect(
81
+ deriveSidebarResourceCounts(
82
+ [endpoints],
83
+ {},
84
+ undefined,
85
+ { Endpoints: { count: 132, expiresAt: now } },
86
+ now
87
+ )
88
+ ).toEqual({ Endpoints: null })
89
+ })
90
+
91
+ it('prefers authoritative resource-counts over cached loaded counts', () => {
92
+ const now = 10_000
93
+
94
+ expect(
95
+ deriveSidebarResourceCounts(
96
+ [endpoints],
97
+ { Endpoints: 7 },
98
+ undefined,
99
+ { Endpoints: { count: 132, expiresAt: now + LOADED_RESOURCE_COUNT_TTL_MS } },
100
+ now
101
+ )
102
+ ).toEqual({ Endpoints: 7 })
103
+ })
104
+
105
+ it('lets a fresh loaded count override an unavailable count marker', () => {
106
+ const now = 10_000
107
+
108
+ expect(
109
+ deriveSidebarResourceCounts(
110
+ [endpoints],
111
+ {},
112
+ ['Endpoints'],
113
+ { Endpoints: { count: 132, expiresAt: now + LOADED_RESOURCE_COUNT_TTL_MS } },
114
+ now
115
+ )
116
+ ).toEqual({ Endpoints: 132 })
117
+ })
118
+ })
119
+
120
+ describe('resourceQueryMatchesSelectedKind', () => {
121
+ it('accepts untagged legacy query results', () => {
122
+ expect(resourceQueryMatchesSelectedKind(undefined, { name: 'endpoints', kind: 'Endpoints', group: '' })).toBe(true)
123
+ expect(resourceQueryMatchesSelectedKind({}, { name: 'endpoints', kind: 'Endpoints', group: '' })).toBe(true)
124
+ })
125
+
126
+ it('rejects stale selected-kind query results from the previous kind', () => {
127
+ expect(
128
+ resourceQueryMatchesSelectedKind(
129
+ { resourceName: 'pods', group: '' },
130
+ { name: 'endpoints', kind: 'Endpoints', group: '' }
131
+ )
132
+ ).toBe(false)
133
+ })
134
+
135
+ it('matches grouped built-in resources by plural name and group', () => {
136
+ expect(
137
+ resourceQueryMatchesSelectedKind(
138
+ { resourceName: 'endpointslices', group: 'discovery.k8s.io' },
139
+ { name: 'endpointslices', kind: 'EndpointSlice', group: 'discovery.k8s.io' }
140
+ )
141
+ ).toBe(true)
142
+ })
143
+ })