@skyhook-io/k8s-ui 1.1.0 → 1.2.0
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 +8 -2
- package/src/components/dock/BottomDock.tsx +14 -15
- package/src/components/dock/DockContext.tsx +40 -1
- package/src/components/dock/LocalTerminalTab.tsx +241 -0
- package/src/components/dock/NodeTerminalTab.tsx +129 -0
- package/src/components/dock/TerminalTab.tsx +1 -1
- package/src/components/dock/index.ts +2 -0
- package/src/components/gitops/GitOpsStatusBadge.tsx +18 -67
- package/src/components/logs/LogCore.tsx +30 -4
- package/src/components/logs/StructuredLogLine.tsx +168 -0
- package/src/components/logs/index.ts +1 -1
- package/src/components/logs/useLogBuffer.ts +16 -10
- package/src/components/resources/ResourcesView.tsx +186 -67
- package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +207 -0
- package/src/components/resources/renderers/HPARenderer.tsx +2 -1
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -1
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +2 -1
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -1
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +7 -6
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +2 -1
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +3 -2
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +3 -2
- package/src/components/resources/renderers/PodRenderer.tsx +164 -8
- package/src/components/resources/renderers/VPARenderer.tsx +2 -1
- package/src/components/resources/renderers/WorkloadRenderer.tsx +53 -55
- package/src/components/resources/renderers/contour-cells.tsx +48 -0
- package/src/components/resources/renderers/index.ts +2 -0
- package/src/components/resources/renderers/trivy-shared.tsx +9 -25
- package/src/components/resources/resource-utils-contour.ts +34 -0
- package/src/components/resources/resource-utils.ts +91 -0
- package/src/components/shared/ResourceActionsBar.tsx +276 -144
- package/src/components/shared/ResourceRendererDispatch.tsx +59 -5
- package/src/components/shared/index.ts +1 -1
- package/src/components/timeline/shared.tsx +8 -10
- package/src/components/topology/K8sResourceNode.tsx +1 -0
- package/src/components/topology/TopologyFilterSidebar.tsx +3 -0
- package/src/components/topology/TopologyGraph.tsx +2 -17
- package/src/components/topology/layout.ts +1 -0
- package/src/components/topology/topology.css +1 -0
- package/src/components/ui/CodeViewer.tsx +1 -2
- package/src/components/ui/ConfirmDialog.tsx +85 -130
- package/src/components/ui/DialogPortal.tsx +82 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +84 -10
- package/src/components/ui/Toast.tsx +5 -5
- package/src/components/ui/drawer-components.tsx +28 -22
- package/src/components/ui/index.ts +1 -0
- package/src/components/workload/WorkloadView.tsx +30 -2
- package/src/theme/index.ts +3 -0
- package/src/theme/tailwind-theme.css +61 -0
- package/src/theme/variables.css +121 -0
- package/src/types/core.ts +13 -2
- package/src/utils/animation.ts +1 -1
- package/src/utils/badge-colors.ts +158 -123
- package/src/utils/log-format.ts +127 -35
- package/src/utils/navigation.test.ts +142 -0
- package/src/utils/navigation.ts +52 -47
- package/src/utils/resource-hierarchy.ts +5 -0
- package/src/utils/resource-icons.ts +3 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { describe, test, expect } from 'vitest'
|
|
2
|
+
import { kindToPlural, pluralToKind, refToSelectedResource } from './navigation'
|
|
3
|
+
|
|
4
|
+
describe('kindToPlural', () => {
|
|
5
|
+
test('singular PascalCase to plural lowercase', () => {
|
|
6
|
+
expect(kindToPlural('Secret')).toBe('secrets')
|
|
7
|
+
expect(kindToPlural('Deployment')).toBe('deployments')
|
|
8
|
+
expect(kindToPlural('Pod')).toBe('pods')
|
|
9
|
+
expect(kindToPlural('Service')).toBe('services')
|
|
10
|
+
expect(kindToPlural('ConfigMap')).toBe('configmaps')
|
|
11
|
+
expect(kindToPlural('Node')).toBe('nodes')
|
|
12
|
+
expect(kindToPlural('Job')).toBe('jobs')
|
|
13
|
+
expect(kindToPlural('CronJob')).toBe('cronjobs')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('handles kinds ending in s/x/ch/sh (adds -es)', () => {
|
|
17
|
+
expect(kindToPlural('Ingress')).toBe('ingresses')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('handles kinds ending in consonant+y (changes to -ies)', () => {
|
|
21
|
+
expect(kindToPlural('NetworkPolicy')).toBe('networkpolicies')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('handles kinds ending in ss (Class-suffix)', () => {
|
|
25
|
+
expect(kindToPlural('StorageClass')).toBe('storageclasses')
|
|
26
|
+
expect(kindToPlural('IngressClass')).toBe('ingressclasses')
|
|
27
|
+
expect(kindToPlural('PriorityClass')).toBe('priorityclasses')
|
|
28
|
+
expect(kindToPlural('RuntimeClass')).toBe('runtimeclasses')
|
|
29
|
+
expect(kindToPlural('GatewayClass')).toBe('gatewayclasses')
|
|
30
|
+
expect(kindToPlural('EC2NodeClass')).toBe('ec2nodeclasses')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('idempotent on known plurals (prevents double-pluralization)', () => {
|
|
34
|
+
// This was the original bug: "secrets" → "secretses"
|
|
35
|
+
expect(kindToPlural('secrets')).toBe('secrets')
|
|
36
|
+
expect(kindToPlural('services')).toBe('services')
|
|
37
|
+
expect(kindToPlural('ingresses')).toBe('ingresses')
|
|
38
|
+
expect(kindToPlural('deployments')).toBe('deployments')
|
|
39
|
+
expect(kindToPlural('pods')).toBe('pods')
|
|
40
|
+
expect(kindToPlural('configmaps')).toBe('configmaps')
|
|
41
|
+
expect(kindToPlural('nodes')).toBe('nodes')
|
|
42
|
+
expect(kindToPlural('storageclasses')).toBe('storageclasses')
|
|
43
|
+
expect(kindToPlural('networkpolicies')).toBe('networkpolicies')
|
|
44
|
+
expect(kindToPlural('horizontalpodautoscalers')).toBe('horizontalpodautoscalers')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('handles aliases', () => {
|
|
48
|
+
expect(kindToPlural('HorizontalPodAutoscaler')).toBe('horizontalpodautoscalers')
|
|
49
|
+
expect(kindToPlural('pvc')).toBe('persistentvolumeclaims')
|
|
50
|
+
expect(kindToPlural('PodGroup')).toBe('pods')
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// Demonstrate that the naive .toLowerCase() + 's' pattern used by renderers is broken.
|
|
55
|
+
// These tests prove WHY renderers must use kindToPlural() instead of ad-hoc pluralization.
|
|
56
|
+
describe('naive pluralization (renderer bug demonstration)', () => {
|
|
57
|
+
const naivePlural = (kind: string) => kind.toLowerCase() + 's'
|
|
58
|
+
|
|
59
|
+
test('breaks for Class-suffix kinds (triple-s)', () => {
|
|
60
|
+
// What HPARenderer, KarpenterNodePoolRenderer, etc. actually produce
|
|
61
|
+
expect(naivePlural('EC2NodeClass')).toBe('ec2nodeclasss') // WRONG
|
|
62
|
+
expect(kindToPlural('EC2NodeClass')).toBe('ec2nodeclasses') // CORRECT
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('breaks for Policy-suffix kinds', () => {
|
|
66
|
+
expect(naivePlural('NetworkPolicy')).toBe('networkpolicys') // WRONG
|
|
67
|
+
expect(kindToPlural('NetworkPolicy')).toBe('networkpolicies') // CORRECT
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('breaks for Ingress-like kinds (ending in s)', () => {
|
|
71
|
+
expect(naivePlural('Ingress')).toBe('ingresss') // WRONG
|
|
72
|
+
expect(kindToPlural('Ingress')).toBe('ingresses') // CORRECT
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('breaks for Repository-suffix kinds', () => {
|
|
76
|
+
expect(naivePlural('GitRepository')).toBe('gitrepositorys') // WRONG
|
|
77
|
+
expect(kindToPlural('GitRepository')).toBe('gitrepositories') // CORRECT
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('pluralToKind', () => {
|
|
82
|
+
test('reverse mapping for known plurals', () => {
|
|
83
|
+
expect(pluralToKind('secrets')).toBe('Secret')
|
|
84
|
+
expect(pluralToKind('deployments')).toBe('Deployment')
|
|
85
|
+
expect(pluralToKind('horizontalpodautoscalers')).toBe('HorizontalPodAutoscaler')
|
|
86
|
+
expect(pluralToKind('ingresses')).toBe('Ingress')
|
|
87
|
+
expect(pluralToKind('configmaps')).toBe('ConfigMap')
|
|
88
|
+
expect(pluralToKind('networkpolicies')).toBe('NetworkPolicy')
|
|
89
|
+
expect(pluralToKind('storageclasses')).toBe('StorageClass')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('PascalCase input returned as-is', () => {
|
|
93
|
+
expect(pluralToKind('Deployment')).toBe('Deployment')
|
|
94
|
+
expect(pluralToKind('Secret')).toBe('Secret')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('fallback de-pluralization for unknown kinds', () => {
|
|
98
|
+
expect(pluralToKind('widgets')).toBe('Widget')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('fallback handles -ies suffix', () => {
|
|
102
|
+
// Unknown kind not in the map
|
|
103
|
+
expect(pluralToKind('batteries')).toBe('Battery')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('fallback handles -ses suffix', () => {
|
|
107
|
+
// "databases" triggers the -ses rule (strips 2 chars) — a known limitation
|
|
108
|
+
// of the heuristic fallback. Known kinds use the PLURAL_TO_KIND map instead.
|
|
109
|
+
expect(pluralToKind('databases')).toBe('Databas')
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('refToSelectedResource', () => {
|
|
114
|
+
test('converts singular kind to plural for navigation', () => {
|
|
115
|
+
const result = refToSelectedResource({
|
|
116
|
+
kind: 'Secret',
|
|
117
|
+
name: 'test-tls',
|
|
118
|
+
namespace: 'platform',
|
|
119
|
+
})
|
|
120
|
+
expect(result).toEqual({
|
|
121
|
+
kind: 'secrets',
|
|
122
|
+
name: 'test-tls',
|
|
123
|
+
namespace: 'platform',
|
|
124
|
+
group: undefined,
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('preserves group field', () => {
|
|
129
|
+
const result = refToSelectedResource({
|
|
130
|
+
kind: 'Certificate',
|
|
131
|
+
name: 'my-cert',
|
|
132
|
+
namespace: 'default',
|
|
133
|
+
group: 'cert-manager.io',
|
|
134
|
+
})
|
|
135
|
+
expect(result).toEqual({
|
|
136
|
+
kind: 'certificates',
|
|
137
|
+
name: 'my-cert',
|
|
138
|
+
namespace: 'default',
|
|
139
|
+
group: 'cert-manager.io',
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
})
|
package/src/utils/navigation.ts
CHANGED
|
@@ -6,14 +6,65 @@ import type { SelectedResource, ResourceRef } from '../types/core'
|
|
|
6
6
|
*/
|
|
7
7
|
export type NavigateToResource = (resource: SelectedResource) => void
|
|
8
8
|
|
|
9
|
+
// Known plural API resource names → singular PascalCase kind.
|
|
10
|
+
// Shared between kindToPlural (idempotency guard) and pluralToKind (reverse lookup).
|
|
11
|
+
const PLURAL_TO_KIND: Record<string, string> = {
|
|
12
|
+
pods: 'Pod',
|
|
13
|
+
services: 'Service',
|
|
14
|
+
deployments: 'Deployment',
|
|
15
|
+
daemonsets: 'DaemonSet',
|
|
16
|
+
statefulsets: 'StatefulSet',
|
|
17
|
+
replicasets: 'ReplicaSet',
|
|
18
|
+
ingresses: 'Ingress',
|
|
19
|
+
gateways: 'Gateway',
|
|
20
|
+
httproutes: 'HTTPRoute',
|
|
21
|
+
grpcroutes: 'GRPCRoute',
|
|
22
|
+
tcproutes: 'TCPRoute',
|
|
23
|
+
tlsroutes: 'TLSRoute',
|
|
24
|
+
configmaps: 'ConfigMap',
|
|
25
|
+
secrets: 'Secret',
|
|
26
|
+
namespaces: 'Namespace',
|
|
27
|
+
events: 'Event',
|
|
28
|
+
nodes: 'Node',
|
|
29
|
+
jobs: 'Job',
|
|
30
|
+
cronjobs: 'CronJob',
|
|
31
|
+
horizontalpodautoscalers: 'HorizontalPodAutoscaler',
|
|
32
|
+
persistentvolumeclaims: 'PersistentVolumeClaim',
|
|
33
|
+
persistentvolumes: 'PersistentVolume',
|
|
34
|
+
storageclasses: 'StorageClass',
|
|
35
|
+
poddisruptionbudgets: 'PodDisruptionBudget',
|
|
36
|
+
rollouts: 'Rollout',
|
|
37
|
+
applications: 'Application',
|
|
38
|
+
kustomizations: 'Kustomization',
|
|
39
|
+
helmreleases: 'HelmRelease',
|
|
40
|
+
gitrepositories: 'GitRepository',
|
|
41
|
+
certificates: 'Certificate',
|
|
42
|
+
roles: 'Role',
|
|
43
|
+
clusterroles: 'ClusterRole',
|
|
44
|
+
rolebindings: 'RoleBinding',
|
|
45
|
+
clusterrolebindings: 'ClusterRoleBinding',
|
|
46
|
+
serviceaccounts: 'ServiceAccount',
|
|
47
|
+
networkpolicies: 'NetworkPolicy',
|
|
48
|
+
verticalpodautoscalers: 'VerticalPodAutoscaler',
|
|
49
|
+
virtualservices: 'VirtualService',
|
|
50
|
+
destinationrules: 'DestinationRule',
|
|
51
|
+
serviceentries: 'ServiceEntry',
|
|
52
|
+
peerauthentications: 'PeerAuthentication',
|
|
53
|
+
authorizationpolicies: 'AuthorizationPolicy',
|
|
54
|
+
}
|
|
55
|
+
|
|
9
56
|
/**
|
|
10
57
|
* Convert a singular kind (e.g., "Deployment") to plural API resource name (e.g., "deployments").
|
|
11
58
|
* Single source of truth — uses English pluralization rules with a small alias map for
|
|
12
59
|
* abbreviations and special mappings that aren't simple plurals.
|
|
60
|
+
* Idempotent: already-plural inputs (e.g., "secrets") are returned as-is.
|
|
13
61
|
*/
|
|
14
62
|
export function kindToPlural(kind: string): string {
|
|
15
63
|
const kindLower = kind.toLowerCase()
|
|
16
64
|
|
|
65
|
+
// Already a known plural — return as-is to prevent double-pluralization
|
|
66
|
+
if (kindLower in PLURAL_TO_KIND) return kindLower
|
|
67
|
+
|
|
17
68
|
// Aliases: abbreviations or mappings to a different resource name
|
|
18
69
|
const aliases: Record<string, string> = {
|
|
19
70
|
horizontalpodautoscaler: 'horizontalpodautoscalers',
|
|
@@ -40,53 +91,7 @@ export function kindToPlural(kind: string): string {
|
|
|
40
91
|
export function pluralToKind(plural: string): string {
|
|
41
92
|
const lower = plural.toLowerCase()
|
|
42
93
|
|
|
43
|
-
|
|
44
|
-
const reverseMap: Record<string, string> = {
|
|
45
|
-
pods: 'Pod',
|
|
46
|
-
services: 'Service',
|
|
47
|
-
deployments: 'Deployment',
|
|
48
|
-
daemonsets: 'DaemonSet',
|
|
49
|
-
statefulsets: 'StatefulSet',
|
|
50
|
-
replicasets: 'ReplicaSet',
|
|
51
|
-
ingresses: 'Ingress',
|
|
52
|
-
gateways: 'Gateway',
|
|
53
|
-
httproutes: 'HTTPRoute',
|
|
54
|
-
grpcroutes: 'GRPCRoute',
|
|
55
|
-
tcproutes: 'TCPRoute',
|
|
56
|
-
tlsroutes: 'TLSRoute',
|
|
57
|
-
configmaps: 'ConfigMap',
|
|
58
|
-
secrets: 'Secret',
|
|
59
|
-
namespaces: 'Namespace',
|
|
60
|
-
events: 'Event',
|
|
61
|
-
nodes: 'Node',
|
|
62
|
-
jobs: 'Job',
|
|
63
|
-
cronjobs: 'CronJob',
|
|
64
|
-
horizontalpodautoscalers: 'HorizontalPodAutoscaler',
|
|
65
|
-
persistentvolumeclaims: 'PersistentVolumeClaim',
|
|
66
|
-
persistentvolumes: 'PersistentVolume',
|
|
67
|
-
storageclasses: 'StorageClass',
|
|
68
|
-
poddisruptionbudgets: 'PodDisruptionBudget',
|
|
69
|
-
rollouts: 'Rollout',
|
|
70
|
-
applications: 'Application',
|
|
71
|
-
kustomizations: 'Kustomization',
|
|
72
|
-
helmreleases: 'HelmRelease',
|
|
73
|
-
gitrepositories: 'GitRepository',
|
|
74
|
-
certificates: 'Certificate',
|
|
75
|
-
roles: 'Role',
|
|
76
|
-
clusterroles: 'ClusterRole',
|
|
77
|
-
rolebindings: 'RoleBinding',
|
|
78
|
-
clusterrolebindings: 'ClusterRoleBinding',
|
|
79
|
-
serviceaccounts: 'ServiceAccount',
|
|
80
|
-
networkpolicies: 'NetworkPolicy',
|
|
81
|
-
verticalpodautoscalers: 'VerticalPodAutoscaler',
|
|
82
|
-
virtualservices: 'VirtualService',
|
|
83
|
-
destinationrules: 'DestinationRule',
|
|
84
|
-
serviceentries: 'ServiceEntry',
|
|
85
|
-
peerauthentications: 'PeerAuthentication',
|
|
86
|
-
authorizationpolicies: 'AuthorizationPolicy',
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (reverseMap[lower]) return reverseMap[lower]
|
|
94
|
+
if (PLURAL_TO_KIND[lower]) return PLURAL_TO_KIND[lower]
|
|
90
95
|
|
|
91
96
|
// If it already looks like a singular PascalCase kind (starts with uppercase), return as-is
|
|
92
97
|
if (plural[0] === plural[0].toUpperCase() && plural[0] !== plural[0].toLowerCase()) {
|
|
@@ -143,6 +143,8 @@ function nodeIdToLaneId(nodeId: string): string | null {
|
|
|
143
143
|
middlewaretcp: 'MiddlewareTCP', traefikservice: 'TraefikService',
|
|
144
144
|
serverstransport: 'ServersTransport', serverstransporttcp: 'ServersTransportTCP',
|
|
145
145
|
tlsoption: 'TLSOption', tlsstore: 'TLSStore',
|
|
146
|
+
// Contour
|
|
147
|
+
httpproxy: 'HTTPProxy',
|
|
146
148
|
}
|
|
147
149
|
return `${kindMap[kind] || kind}/${namespace}/${name}`
|
|
148
150
|
}
|
|
@@ -417,6 +419,7 @@ export function buildResourceHierarchy(options: HierarchyOptions): ResourceLane[
|
|
|
417
419
|
'KnativeService', 'KnativeConfiguration', 'KnativeRevision', 'KnativeRoute',
|
|
418
420
|
'Broker', 'Trigger',
|
|
419
421
|
'IngressRoute', 'IngressRouteTCP', 'IngressRouteUDP',
|
|
422
|
+
'HTTPProxy', // Contour
|
|
420
423
|
])
|
|
421
424
|
|
|
422
425
|
// Group lanes by app label
|
|
@@ -448,6 +451,7 @@ export function buildResourceHierarchy(options: HierarchyOptions): ResourceLane[
|
|
|
448
451
|
PingSource: 3, ApiServerSource: 3, ContainerSource: 3, SinkBinding: 3,
|
|
449
452
|
IngressRoute: 2, IngressRouteTCP: 2, IngressRouteUDP: 2,
|
|
450
453
|
TraefikService: 3, Middleware: 4, MiddlewareTCP: 4,
|
|
454
|
+
HTTPProxy: 2, // Contour
|
|
451
455
|
}
|
|
452
456
|
|
|
453
457
|
const sorted = [...laneIds].sort((a, b) => {
|
|
@@ -510,6 +514,7 @@ export function buildResourceHierarchy(options: HierarchyOptions): ResourceLane[
|
|
|
510
514
|
PingSource: 3, ApiServerSource: 3, ContainerSource: 3, SinkBinding: 3,
|
|
511
515
|
IngressRoute: 1, IngressRouteTCP: 1, IngressRouteUDP: 1,
|
|
512
516
|
TraefikService: 2, Middleware: 3, MiddlewareTCP: 3,
|
|
517
|
+
HTTPProxy: 1, // Contour
|
|
513
518
|
}
|
|
514
519
|
lane.children.sort((a, b) => {
|
|
515
520
|
const aPriority = kindPriority[a.kind] || 10
|