@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.
- package/package.json +5 -4
- package/src/assets/gitops/argocd.png +0 -0
- package/src/assets/gitops/flux.svg +7 -0
- package/src/components/applications/AppChips.tsx +10 -0
- package/src/components/applications/ApplicationDetail.test.tsx +569 -0
- package/src/components/applications/ApplicationDetail.tsx +2517 -450
- package/src/components/applications/ApplicationsView.tsx +62 -10
- package/src/components/applications/applications-view.test.tsx +47 -0
- package/src/components/applications/index.ts +1 -1
- package/src/components/audit/AuditAlerts.tsx +14 -10
- package/src/components/checks/ChecksView.tsx +2 -3
- package/src/components/checks/severity.ts +14 -10
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +2 -2
- package/src/components/compare/CompareResourcePicker.tsx +2 -2
- package/src/components/gitops/GitOpsGraphFilterRail.tsx +2 -1
- package/src/components/gitops/GitOpsTableView.tsx +3 -3
- package/src/components/gitops/ManagedResourcesList.tsx +8 -11
- package/src/components/gitops/SyncCountdown.tsx +26 -17
- package/src/components/gitops/SyncOptionsDialog.tsx +2 -2
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +277 -104
- package/src/components/gitops/insights/insights-helpers.test.ts +86 -1
- package/src/components/gitops/insights/insights-helpers.ts +69 -1
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +5 -2
- package/src/components/issues/IssuesView.tsx +51 -38
- package/src/components/issues/ResourceIssuesSection.test.tsx +6 -5
- package/src/components/issues/ResourceIssuesSection.tsx +28 -146
- package/src/components/issues/index.ts +3 -0
- package/src/components/issues/issue-timing.ts +53 -0
- package/src/components/issues/issues.test.ts +102 -1
- package/src/components/issues/severity.ts +2 -0
- package/src/components/logs/LogCore.tsx +14 -4
- package/src/components/logs/WorkloadLogsViewer.tsx +31 -6
- package/src/components/logs/useLogStream.ts +8 -1
- package/src/components/namespace-switcher/NamespacePicker.tsx +86 -107
- package/src/components/resources/ResourcesSidebar.test.tsx +71 -0
- package/src/components/resources/ResourcesSidebar.tsx +46 -46
- package/src/components/resources/ResourcesView.selected-kind.test.ts +143 -0
- package/src/components/resources/ResourcesView.tsx +233 -51
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +99 -0
- package/src/components/resources/renderers/JobRenderer.tsx +3 -1
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +2 -2
- package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/SecretRenderer.tsx +2 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +58 -40
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/WorkflowRenderer.test.tsx +43 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +60 -78
- package/src/components/resources/renderers/index.ts +1 -0
- package/src/components/resources/resource-utils-capi.ts +1 -1
- package/src/components/resources/resource-utils.ts +31 -0
- package/src/components/shared/DetailShell.tsx +2 -2
- package/src/components/shared/ResourceActionsBar.tsx +140 -108
- package/src/components/shared/ResourceRendererDispatch.tsx +33 -4
- package/src/components/timeline/TimelineList.tsx +371 -354
- package/src/components/timeline/TimelineStrip.test.tsx +194 -0
- package/src/components/timeline/TimelineStrip.tsx +836 -0
- package/src/components/timeline/TimelineSwimlanes.test.tsx +1028 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +2498 -593
- package/src/components/timeline/TimelineToolbar.test.tsx +320 -0
- package/src/components/timeline/TimelineToolbar.tsx +759 -0
- package/src/components/timeline/TimelineToolbarState.test.tsx +64 -0
- package/src/components/timeline/index.ts +23 -0
- package/src/components/timeline/scrubber-math.test.ts +235 -0
- package/src/components/timeline/scrubber-math.ts +249 -0
- package/src/components/timeline/shared.test.tsx +219 -0
- package/src/components/timeline/shared.tsx +232 -447
- package/src/components/timeline/timeline-filters.test.ts +196 -0
- package/src/components/timeline/timeline-filters.ts +180 -0
- package/src/components/timeline/timeline-lane-sort.test.ts +97 -0
- package/src/components/timeline/timeline-lane-sort.ts +77 -0
- package/src/components/timeline/timeline-live.test.ts +131 -0
- package/src/components/timeline/timeline-live.ts +77 -0
- package/src/components/topology/GroupNode.tsx +9 -2
- package/src/components/topology/K8sResourceNode.test.ts +11 -0
- package/src/components/topology/K8sResourceNode.tsx +387 -243
- package/src/components/topology/TopologyFilterSidebar.tsx +9 -0
- package/src/components/topology/TopologyGraph.tsx +89 -16
- package/src/components/topology/TopologySearch.tsx +2 -2
- package/src/components/topology/layout.ts +10 -2
- package/src/components/topology/topology.css +0 -92
- package/src/components/ui/Badge.tsx +5 -0
- package/src/components/ui/CodeViewer.tsx +2 -2
- package/src/components/ui/Collapse.tsx +59 -0
- package/src/components/ui/ConfirmDialog.tsx +7 -3
- package/src/components/ui/DialogPortal.tsx +31 -9
- package/src/components/ui/Input.tsx +24 -0
- package/src/components/ui/MultiSelectPicker.test.tsx +101 -0
- package/src/components/ui/MultiSelectPicker.tsx +167 -0
- package/src/components/ui/PaneLoader.tsx +10 -2
- package/src/components/ui/ResourceBar.tsx +68 -25
- package/src/components/ui/SearchBox.tsx +18 -4
- package/src/components/ui/SearchPillInput.tsx +2 -2
- package/src/components/ui/drawer-components.tsx +39 -26
- package/src/components/ui/index.ts +4 -0
- package/src/components/ui/severity-tone.ts +11 -4
- package/src/components/workload/ResourceDetailDrawer.tsx +6 -2
- package/src/components/workload/WorkloadView.tsx +2682 -472
- package/src/components/workload/index.ts +1 -1
- package/src/theme/variables.css +1 -1
- package/src/types/core.ts +57 -4
- package/src/utils/application-topology.test.ts +198 -0
- package/src/utils/application-topology.ts +261 -0
- package/src/utils/applications.test.ts +167 -1
- package/src/utils/applications.ts +1157 -393
- package/src/utils/index.ts +2 -0
- package/src/utils/navigation.test.ts +32 -1
- package/src/utils/navigation.ts +70 -0
- package/src/utils/resource-hierarchy.test.ts +990 -1
- package/src/utils/resource-hierarchy.ts +905 -267
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/topology-neighborhood.test.ts +202 -1
- package/src/utils/topology-neighborhood.ts +194 -13
- package/src/utils/workflow-execution.test.ts +556 -0
- package/src/utils/workflow-execution.ts +814 -0
- 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
|
|
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
|
|
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] =
|
|
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:
|
|
330
|
-
//
|
|
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
|
|
335
|
-
|
|
336
|
-
|
|
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
|
|
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
|
-
<
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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
|
+
})
|