@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.
Files changed (58) hide show
  1. package/package.json +8 -2
  2. package/src/components/dock/BottomDock.tsx +14 -15
  3. package/src/components/dock/DockContext.tsx +40 -1
  4. package/src/components/dock/LocalTerminalTab.tsx +241 -0
  5. package/src/components/dock/NodeTerminalTab.tsx +129 -0
  6. package/src/components/dock/TerminalTab.tsx +1 -1
  7. package/src/components/dock/index.ts +2 -0
  8. package/src/components/gitops/GitOpsStatusBadge.tsx +18 -67
  9. package/src/components/logs/LogCore.tsx +30 -4
  10. package/src/components/logs/StructuredLogLine.tsx +168 -0
  11. package/src/components/logs/index.ts +1 -1
  12. package/src/components/logs/useLogBuffer.ts +16 -10
  13. package/src/components/resources/ResourcesView.tsx +186 -67
  14. package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +207 -0
  15. package/src/components/resources/renderers/HPARenderer.tsx +2 -1
  16. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -1
  17. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +2 -1
  18. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -1
  19. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +7 -6
  20. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +2 -1
  21. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +3 -2
  22. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +3 -2
  23. package/src/components/resources/renderers/PodRenderer.tsx +164 -8
  24. package/src/components/resources/renderers/VPARenderer.tsx +2 -1
  25. package/src/components/resources/renderers/WorkloadRenderer.tsx +53 -55
  26. package/src/components/resources/renderers/contour-cells.tsx +48 -0
  27. package/src/components/resources/renderers/index.ts +2 -0
  28. package/src/components/resources/renderers/trivy-shared.tsx +9 -25
  29. package/src/components/resources/resource-utils-contour.ts +34 -0
  30. package/src/components/resources/resource-utils.ts +91 -0
  31. package/src/components/shared/ResourceActionsBar.tsx +276 -144
  32. package/src/components/shared/ResourceRendererDispatch.tsx +59 -5
  33. package/src/components/shared/index.ts +1 -1
  34. package/src/components/timeline/shared.tsx +8 -10
  35. package/src/components/topology/K8sResourceNode.tsx +1 -0
  36. package/src/components/topology/TopologyFilterSidebar.tsx +3 -0
  37. package/src/components/topology/TopologyGraph.tsx +2 -17
  38. package/src/components/topology/layout.ts +1 -0
  39. package/src/components/topology/topology.css +1 -0
  40. package/src/components/ui/CodeViewer.tsx +1 -2
  41. package/src/components/ui/ConfirmDialog.tsx +85 -130
  42. package/src/components/ui/DialogPortal.tsx +82 -0
  43. package/src/components/ui/ForceDeleteConfirmDialog.tsx +84 -10
  44. package/src/components/ui/Toast.tsx +5 -5
  45. package/src/components/ui/drawer-components.tsx +28 -22
  46. package/src/components/ui/index.ts +1 -0
  47. package/src/components/workload/WorkloadView.tsx +30 -2
  48. package/src/theme/index.ts +3 -0
  49. package/src/theme/tailwind-theme.css +61 -0
  50. package/src/theme/variables.css +121 -0
  51. package/src/types/core.ts +13 -2
  52. package/src/utils/animation.ts +1 -1
  53. package/src/utils/badge-colors.ts +158 -123
  54. package/src/utils/log-format.ts +127 -35
  55. package/src/utils/navigation.test.ts +142 -0
  56. package/src/utils/navigation.ts +52 -47
  57. package/src/utils/resource-hierarchy.ts +5 -0
  58. 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
+ })
@@ -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
- // Explicit reverse mappings for irregular/aliased plurals
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
@@ -184,6 +184,9 @@ const KIND_ICON_MAP: Record<string, LucideIcon> = {
184
184
  tlsoption: Lock,
185
185
  tlsstore: Lock,
186
186
 
187
+ // Contour
188
+ httpproxy: Globe,
189
+
187
190
  // Trivy Operator
188
191
  vulnerabilityreport: Shield,
189
192
  configauditreport: ShieldCheck,