@skyhook-io/k8s-ui 1.0.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 (193) hide show
  1. package/package.json +63 -0
  2. package/src/components/dock/BottomDock.tsx +204 -0
  3. package/src/components/dock/DockContext.tsx +212 -0
  4. package/src/components/dock/TerminalTab.tsx +333 -0
  5. package/src/components/dock/index.ts +3 -0
  6. package/src/components/gitops/GitOpsActions.tsx +227 -0
  7. package/src/components/gitops/GitOpsStatusBadge.tsx +221 -0
  8. package/src/components/gitops/ManagedResourcesList.tsx +210 -0
  9. package/src/components/gitops/SyncCountdown.tsx +146 -0
  10. package/src/components/gitops/index.ts +4 -0
  11. package/src/components/logs/JsonLogLine.tsx +90 -0
  12. package/src/components/logs/LogCore.tsx +553 -0
  13. package/src/components/logs/LogToolbarSelects.tsx +74 -0
  14. package/src/components/logs/LogsViewer.tsx +146 -0
  15. package/src/components/logs/WorkloadLogsViewer.tsx +263 -0
  16. package/src/components/logs/index.ts +13 -0
  17. package/src/components/logs/useLogBuffer.ts +138 -0
  18. package/src/components/logs/useLogSearch.ts +151 -0
  19. package/src/components/logs/useLogStream.ts +61 -0
  20. package/src/components/resources/ResourcesView.tsx +5494 -0
  21. package/src/components/resources/index.ts +17 -0
  22. package/src/components/resources/renderers/AlertRenderer.tsx +166 -0
  23. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +233 -0
  24. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +89 -0
  25. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +194 -0
  26. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +79 -0
  27. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +68 -0
  28. package/src/components/resources/renderers/CertificateRenderer.tsx +209 -0
  29. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +123 -0
  30. package/src/components/resources/renderers/ChallengeRenderer.tsx +111 -0
  31. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +164 -0
  32. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +140 -0
  33. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +153 -0
  34. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +221 -0
  35. package/src/components/resources/renderers/ConfigMapRenderer.tsx +124 -0
  36. package/src/components/resources/renderers/CronJobRenderer.tsx +86 -0
  37. package/src/components/resources/renderers/EventRenderer.tsx +175 -0
  38. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +119 -0
  39. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +172 -0
  40. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +242 -0
  41. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +263 -0
  42. package/src/components/resources/renderers/GatewayClassRenderer.tsx +68 -0
  43. package/src/components/resources/renderers/GatewayRenderer.tsx +218 -0
  44. package/src/components/resources/renderers/GenericRenderer.tsx +476 -0
  45. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +154 -0
  46. package/src/components/resources/renderers/HPARenderer.tsx +164 -0
  47. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +303 -0
  48. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +167 -0
  49. package/src/components/resources/renderers/IngressClassRenderer.tsx +49 -0
  50. package/src/components/resources/renderers/IngressRenderer.tsx +104 -0
  51. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +175 -0
  52. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +176 -0
  53. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +125 -0
  54. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +84 -0
  55. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +112 -0
  56. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +288 -0
  57. package/src/components/resources/renderers/JobRenderer.tsx +103 -0
  58. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +157 -0
  59. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +188 -0
  60. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +148 -0
  61. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +90 -0
  62. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +136 -0
  63. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +151 -0
  64. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +70 -0
  65. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +309 -0
  66. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +142 -0
  67. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +232 -0
  68. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +97 -0
  69. package/src/components/resources/renderers/KnativeRouteRenderer.tsx +74 -0
  70. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +138 -0
  71. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +220 -0
  72. package/src/components/resources/renderers/KustomizationRenderer.tsx +151 -0
  73. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +352 -0
  74. package/src/components/resources/renderers/LeaseRenderer.tsx +93 -0
  75. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +208 -0
  76. package/src/components/resources/renderers/NodeRenderer.tsx +282 -0
  77. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +153 -0
  78. package/src/components/resources/renderers/OrderRenderer.tsx +108 -0
  79. package/src/components/resources/renderers/PVCRenderer.tsx +98 -0
  80. package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +138 -0
  81. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +126 -0
  82. package/src/components/resources/renderers/PodMonitorRenderer.tsx +63 -0
  83. package/src/components/resources/renderers/PodRenderer.tsx +677 -0
  84. package/src/components/resources/renderers/PriorityClassRenderer.tsx +50 -0
  85. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +62 -0
  86. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +71 -0
  87. package/src/components/resources/renderers/RoleBindingRenderer.tsx +97 -0
  88. package/src/components/resources/renderers/RoleRenderer.tsx +192 -0
  89. package/src/components/resources/renderers/RolloutRenderer.tsx +227 -0
  90. package/src/components/resources/renderers/RuntimeClassRenderer.tsx +75 -0
  91. package/src/components/resources/renderers/SbomReportRenderer.tsx +98 -0
  92. package/src/components/resources/renderers/SealedSecretRenderer.tsx +121 -0
  93. package/src/components/resources/renderers/SecretRenderer.tsx +321 -0
  94. package/src/components/resources/renderers/SecretStoreRenderer.tsx +111 -0
  95. package/src/components/resources/renderers/ServiceAccountRenderer.tsx +81 -0
  96. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +65 -0
  97. package/src/components/resources/renderers/ServiceRenderer.tsx +98 -0
  98. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +195 -0
  99. package/src/components/resources/renderers/StorageClassRenderer.tsx +82 -0
  100. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +257 -0
  101. package/src/components/resources/renderers/VPARenderer.tsx +193 -0
  102. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +73 -0
  103. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +193 -0
  104. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +161 -0
  105. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +134 -0
  106. package/src/components/resources/renderers/VeleroVSLRenderer.tsx +36 -0
  107. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +347 -0
  108. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +156 -0
  109. package/src/components/resources/renderers/WorkflowRenderer.tsx +202 -0
  110. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +91 -0
  111. package/src/components/resources/renderers/WorkloadRenderer.tsx +311 -0
  112. package/src/components/resources/renderers/argo-cells.tsx +118 -0
  113. package/src/components/resources/renderers/certmanager-cells.tsx +164 -0
  114. package/src/components/resources/renderers/cnpg-cells.tsx +161 -0
  115. package/src/components/resources/renderers/eso-cells.tsx +115 -0
  116. package/src/components/resources/renderers/flux-cells.tsx +253 -0
  117. package/src/components/resources/renderers/index.ts +105 -0
  118. package/src/components/resources/renderers/istio-cells.tsx +210 -0
  119. package/src/components/resources/renderers/karpenter-cells.tsx +107 -0
  120. package/src/components/resources/renderers/keda-cells.tsx +94 -0
  121. package/src/components/resources/renderers/knative-cells.tsx +396 -0
  122. package/src/components/resources/renderers/kyverno-cells.tsx +85 -0
  123. package/src/components/resources/renderers/prometheus-cells.tsx +74 -0
  124. package/src/components/resources/renderers/traefik-cells.tsx +120 -0
  125. package/src/components/resources/renderers/trivy-cells.tsx +153 -0
  126. package/src/components/resources/renderers/trivy-shared.tsx +67 -0
  127. package/src/components/resources/renderers/velero-cells.tsx +164 -0
  128. package/src/components/resources/resource-utils-argo.ts +155 -0
  129. package/src/components/resources/resource-utils-certmanager.ts +198 -0
  130. package/src/components/resources/resource-utils-cnpg.ts +344 -0
  131. package/src/components/resources/resource-utils-eso.ts +385 -0
  132. package/src/components/resources/resource-utils-flux.ts +186 -0
  133. package/src/components/resources/resource-utils-istio.ts +372 -0
  134. package/src/components/resources/resource-utils-karpenter.ts +150 -0
  135. package/src/components/resources/resource-utils-keda.ts +203 -0
  136. package/src/components/resources/resource-utils-knative.ts +218 -0
  137. package/src/components/resources/resource-utils-kyverno.ts +224 -0
  138. package/src/components/resources/resource-utils-prometheus.ts +159 -0
  139. package/src/components/resources/resource-utils-traefik.ts +180 -0
  140. package/src/components/resources/resource-utils-trivy.ts +146 -0
  141. package/src/components/resources/resource-utils-velero.ts +293 -0
  142. package/src/components/resources/resource-utils.ts +1558 -0
  143. package/src/components/shared/EditableYamlView.tsx +302 -0
  144. package/src/components/shared/ResourceActionsBar.tsx +924 -0
  145. package/src/components/shared/ResourceRendererDispatch.tsx +557 -0
  146. package/src/components/shared/index.ts +3 -0
  147. package/src/components/timeline/index.ts +1 -0
  148. package/src/components/timeline/shared.tsx +614 -0
  149. package/src/components/topology/GroupNode.tsx +198 -0
  150. package/src/components/topology/K8sResourceNode.tsx +440 -0
  151. package/src/components/topology/TopologyControls.tsx +62 -0
  152. package/src/components/topology/TopologyFilterSidebar.tsx +377 -0
  153. package/src/components/topology/TopologyGraph.tsx +748 -0
  154. package/src/components/topology/TopologySearch.tsx +304 -0
  155. package/src/components/topology/index.ts +7 -0
  156. package/src/components/topology/layout.ts +734 -0
  157. package/src/components/topology/layout.worker.ts +272 -0
  158. package/src/components/topology/topology.css +129 -0
  159. package/src/components/ui/CodeViewer.tsx +451 -0
  160. package/src/components/ui/ConfirmDialog.tsx +188 -0
  161. package/src/components/ui/ForceDeleteConfirmDialog.tsx +57 -0
  162. package/src/components/ui/HealthRing.tsx +72 -0
  163. package/src/components/ui/MetricsChart.tsx +259 -0
  164. package/src/components/ui/ResourceBar.tsx +68 -0
  165. package/src/components/ui/Toast.tsx +225 -0
  166. package/src/components/ui/Tooltip.tsx +165 -0
  167. package/src/components/ui/YamlEditor.tsx +280 -0
  168. package/src/components/ui/drawer-components.tsx +792 -0
  169. package/src/components/ui/index.ts +10 -0
  170. package/src/components/workload/ResourceDetailDrawer.tsx +158 -0
  171. package/src/components/workload/WorkloadView.tsx +1109 -0
  172. package/src/components/workload/index.ts +2 -0
  173. package/src/hooks/index.ts +9 -0
  174. package/src/hooks/useAnimatedUnmount.ts +30 -0
  175. package/src/hooks/useKeyboardShortcuts.tsx +336 -0
  176. package/src/hooks/useRefreshAnimation.ts +58 -0
  177. package/src/index.ts +35 -0
  178. package/src/types/core.ts +894 -0
  179. package/src/types/gitops.ts +458 -0
  180. package/src/types/index.ts +2 -0
  181. package/src/utils/animation.ts +40 -0
  182. package/src/utils/api-resources.ts +185 -0
  183. package/src/utils/badge-colors.ts +399 -0
  184. package/src/utils/context-name.ts +72 -0
  185. package/src/utils/download.ts +19 -0
  186. package/src/utils/format.ts +233 -0
  187. package/src/utils/index.ts +10 -0
  188. package/src/utils/log-format.ts +300 -0
  189. package/src/utils/navigation.ts +119 -0
  190. package/src/utils/resource-hierarchy.ts +630 -0
  191. package/src/utils/resource-icons.ts +206 -0
  192. package/src/vite-env.d.ts +1 -0
  193. package/tsconfig.json +24 -0
@@ -0,0 +1,2 @@
1
+ export { WorkloadView } from './WorkloadView'
2
+ export { ResourceDetailDrawer } from './ResourceDetailDrawer'
@@ -0,0 +1,9 @@
1
+ export { useAnimatedUnmount } from './useAnimatedUnmount'
2
+ export { useRefreshAnimation } from './useRefreshAnimation'
3
+ export {
4
+ KeyboardShortcutProvider,
5
+ useRegisterShortcut,
6
+ useRegisterShortcuts,
7
+ useActiveShortcuts,
8
+ } from './useKeyboardShortcuts'
9
+ export type { KeyboardShortcut, ShortcutCategory } from './useKeyboardShortcuts'
@@ -0,0 +1,30 @@
1
+ import { useState, useEffect, useRef } from 'react'
2
+ import { DURATION_NORMAL } from '../utils/animation'
3
+
4
+ /**
5
+ * Delays React unmount until exit animation completes.
6
+ * - `shouldRender`: whether the component should be in the DOM
7
+ * - `isOpen`: drives CSS transition classes (false → closed state, true → open state)
8
+ */
9
+ export function useAnimatedUnmount(isVisible: boolean, duration = DURATION_NORMAL) {
10
+ const [shouldRender, setShouldRender] = useState(isVisible)
11
+ const [isOpen, setIsOpen] = useState(false)
12
+ const rafRef = useRef(0)
13
+
14
+ useEffect(() => {
15
+ if (isVisible) {
16
+ setShouldRender(true)
17
+ // Double rAF: DOM paints closed state, then we flip to open → triggers CSS transition
18
+ rafRef.current = requestAnimationFrame(() => {
19
+ rafRef.current = requestAnimationFrame(() => setIsOpen(true))
20
+ })
21
+ return () => cancelAnimationFrame(rafRef.current)
22
+ } else if (shouldRender) {
23
+ setIsOpen(false)
24
+ const t = setTimeout(() => setShouldRender(false), duration)
25
+ return () => clearTimeout(t)
26
+ }
27
+ }, [isVisible, duration]) // eslint-disable-line react-hooks/exhaustive-deps — shouldRender intentionally omitted to avoid re-triggering cleanup
28
+
29
+ return { shouldRender, isOpen }
30
+ }
@@ -0,0 +1,336 @@
1
+ import { createContext, useContext, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
2
+
3
+ export type ShortcutScope = 'global' | 'topology' | 'resources' | 'timeline' | 'helm' | 'traffic' | 'drawer'
4
+
5
+ // Scope priority: higher number = higher priority (wins when multiple scopes active)
6
+ const SCOPE_PRIORITY: Record<ShortcutScope, number> = {
7
+ global: 0,
8
+ topology: 1,
9
+ resources: 1,
10
+ timeline: 1,
11
+ helm: 1,
12
+ traffic: 1,
13
+ drawer: 2,
14
+ }
15
+
16
+ export type ShortcutCategory = 'Navigation' | 'Search' | 'Resource Actions' | 'Table' | 'General' | 'Topology' | 'Timeline' | 'Helm' | 'Drawer' | 'Dock'
17
+
18
+ export interface KeyboardShortcut {
19
+ /** Unique ID for this shortcut */
20
+ id: string
21
+ /** Display key(s) — e.g. "j", "g g", "Shift+N", "Cmd+K" */
22
+ keys: string
23
+ /** Human-readable description */
24
+ description: string
25
+ /** Category for help overlay grouping */
26
+ category: ShortcutCategory
27
+ /** Scope determines priority — higher scope wins */
28
+ scope: ShortcutScope
29
+ /** The handler function */
30
+ handler: (e: KeyboardEvent) => void
31
+ /** Whether this shortcut is currently active */
32
+ enabled?: boolean
33
+ }
34
+
35
+ // Internal registration with stable ID
36
+ interface RegisteredShortcut extends KeyboardShortcut {
37
+ _registrationId: number
38
+ }
39
+
40
+ interface KeyboardShortcutContextType {
41
+ registerShortcut: (shortcut: KeyboardShortcut) => () => void
42
+ activeShortcuts: KeyboardShortcut[]
43
+ }
44
+
45
+ const KeyboardShortcutContext = createContext<KeyboardShortcutContextType | null>(null)
46
+
47
+ let nextRegistrationId = 0
48
+
49
+ // Check if focus is in an element that should suppress shortcuts
50
+ function shouldSuppressShortcut(e: KeyboardEvent): boolean {
51
+ const target = e.target as HTMLElement
52
+ if (!target) return false
53
+
54
+ // Always allow Escape
55
+ if (e.key === 'Escape') return false
56
+
57
+ // Suppress in text inputs
58
+ const tagName = target.tagName
59
+ if (tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT') return true
60
+
61
+ // Suppress in contenteditable
62
+ if (target.isContentEditable) return true
63
+
64
+ // Suppress in Monaco editor
65
+ if (target.closest('.monaco-editor')) return true
66
+
67
+ // Suppress in xterm terminal
68
+ if (target.closest('.xterm')) return true
69
+
70
+ return false
71
+ }
72
+
73
+ // Parse a key string like "Shift+N", "Cmd+K", "g g" into match criteria
74
+ interface KeyMatcher {
75
+ sequence: string[] // For multi-key sequences like ["g", "g"]
76
+ ctrlKey?: boolean
77
+ metaKey?: boolean
78
+ shiftKey?: boolean
79
+ altKey?: boolean
80
+ }
81
+
82
+ const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform)
83
+
84
+ function parseKeys(keys: string): KeyMatcher {
85
+ // Multi-key sequence (space-separated, e.g. "g g")
86
+ if (keys.includes(' ') && !keys.includes('+')) {
87
+ return { sequence: keys.split(' ') }
88
+ }
89
+
90
+ const parts = keys.split('+')
91
+ const matcher: KeyMatcher = { sequence: [] }
92
+
93
+ for (const part of parts) {
94
+ const lower = part.toLowerCase().trim()
95
+ if (lower === 'cmd' || lower === 'meta') {
96
+ // Cmd maps to Meta on Mac, Ctrl on Windows/Linux
97
+ if (isMac) matcher.metaKey = true
98
+ else matcher.ctrlKey = true
99
+ }
100
+ else if (lower === 'ctrl') matcher.ctrlKey = true
101
+ else if (lower === 'shift') matcher.shiftKey = true
102
+ else if (lower === 'alt') matcher.altKey = true
103
+ else matcher.sequence = [part]
104
+ }
105
+
106
+ return matcher
107
+ }
108
+
109
+ function matchesKey(e: KeyboardEvent, matcher: KeyMatcher, currentKey: string): boolean {
110
+ // Single key (possibly with modifiers)
111
+ if (matcher.sequence.length === 1) {
112
+ const key = matcher.sequence[0]
113
+ // Match the key
114
+ if (e.key !== key && e.key.toLowerCase() !== key.toLowerCase()) return false
115
+ // For uppercase letters (Shift+X), check shift
116
+ if (key.length === 1 && key === key.toUpperCase() && key !== key.toLowerCase()) {
117
+ if (!e.shiftKey) return false
118
+ }
119
+ // Check required modifiers
120
+ if (matcher.metaKey && !e.metaKey) return false
121
+ if (matcher.ctrlKey && !e.ctrlKey) return false
122
+ if (matcher.shiftKey && !e.shiftKey) return false
123
+ if (matcher.altKey && !e.altKey) return false
124
+ // Check no extra modifiers (unless required) — but only for non-modifier keys
125
+ if (!matcher.metaKey && e.metaKey) return false
126
+ if (!matcher.ctrlKey && e.ctrlKey) return false
127
+ // Don't check shift for plain lowercase letters (prevents Shift+j triggering "j")
128
+ // but allow Shift-produced symbols like ? (Shift+/) and + (Shift+=)
129
+ if (matcher.shiftKey === undefined && e.shiftKey && key.length === 1 && key >= 'a' && key <= 'z') return false
130
+ if (!matcher.altKey && e.altKey) return false
131
+ return true
132
+ }
133
+
134
+ // Multi-key sequence — currentKey tracks the first key pressed
135
+ return currentKey === matcher.sequence[0] && e.key === matcher.sequence[1]
136
+ }
137
+
138
+ const SEQUENCE_TIMEOUT = 500
139
+
140
+ export function KeyboardShortcutProvider({ children }: { children: ReactNode }) {
141
+ const shortcutsRef = useRef<Map<number, RegisteredShortcut>>(new Map())
142
+ const [version, setVersion] = useState(0)
143
+ const sequenceKeyRef = useRef<string | null>(null)
144
+ const sequenceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
145
+
146
+ const registerShortcut = useCallback((shortcut: KeyboardShortcut) => {
147
+ const id = nextRegistrationId++
148
+ const registered: RegisteredShortcut = { ...shortcut, _registrationId: id }
149
+ shortcutsRef.current.set(id, registered)
150
+ setVersion(v => v + 1)
151
+
152
+ return () => {
153
+ shortcutsRef.current.delete(id)
154
+ setVersion(v => v + 1)
155
+ }
156
+ }, [])
157
+
158
+ // Global keydown listener
159
+ useEffect(() => {
160
+ const handleKeyDown = (e: KeyboardEvent) => {
161
+ const suppressed = shouldSuppressShortcut(e)
162
+
163
+ // Get all enabled shortcuts, sorted by scope priority (highest first)
164
+ const shortcuts = Array.from(shortcutsRef.current.values())
165
+ .filter(s => s.enabled !== false)
166
+ .sort((a, b) => SCOPE_PRIORITY[b.scope] - SCOPE_PRIORITY[a.scope])
167
+
168
+ // Check for multi-key sequence completion
169
+ if (sequenceKeyRef.current) {
170
+ const firstKey = sequenceKeyRef.current
171
+ sequenceKeyRef.current = null
172
+ if (sequenceTimerRef.current) {
173
+ clearTimeout(sequenceTimerRef.current)
174
+ sequenceTimerRef.current = null
175
+ }
176
+
177
+ if (!suppressed) {
178
+ for (const shortcut of shortcuts) {
179
+ const matcher = parseKeys(shortcut.keys)
180
+ if (matcher.sequence.length === 2 && matchesKey(e, matcher, firstKey)) {
181
+ e.preventDefault()
182
+ try { shortcut.handler(e) } catch (err) { console.error(`[KeyboardShortcuts] Handler "${shortcut.id}" threw:`, err) }
183
+ return
184
+ }
185
+ }
186
+ }
187
+ // Sequence didn't match — fall through to single-key handling
188
+ }
189
+
190
+ // Check single-key shortcuts
191
+ for (const shortcut of shortcuts) {
192
+ const matcher = parseKeys(shortcut.keys)
193
+
194
+ // Skip multi-key sequences in this pass (handled above)
195
+ if (matcher.sequence.length === 2) {
196
+ // But check if this could be the START of a sequence
197
+ if (!suppressed && e.key === matcher.sequence[0] && !e.metaKey && !e.ctrlKey && !e.altKey) {
198
+ // Start sequence tracking
199
+ sequenceKeyRef.current = e.key
200
+ if (sequenceTimerRef.current) clearTimeout(sequenceTimerRef.current)
201
+ sequenceTimerRef.current = setTimeout(() => {
202
+ sequenceKeyRef.current = null
203
+ sequenceTimerRef.current = null
204
+ }, SEQUENCE_TIMEOUT)
205
+ // Don't prevent default yet — the key might be standalone
206
+ return
207
+ }
208
+ continue
209
+ }
210
+
211
+ // Escape always fires (even when suppressed)
212
+ if (e.key === 'Escape' && shortcut.keys === 'Escape') {
213
+ e.preventDefault()
214
+ try { shortcut.handler(e) } catch (err) { console.error(`[KeyboardShortcuts] Handler "${shortcut.id}" threw:`, err) }
215
+ return
216
+ }
217
+
218
+ if (suppressed) continue
219
+
220
+ if (matchesKey(e, matcher, '')) {
221
+ e.preventDefault()
222
+ try { shortcut.handler(e) } catch (err) { console.error(`[KeyboardShortcuts] Handler "${shortcut.id}" threw:`, err) }
223
+ return
224
+ }
225
+ }
226
+ }
227
+
228
+ document.addEventListener('keydown', handleKeyDown)
229
+ return () => {
230
+ document.removeEventListener('keydown', handleKeyDown)
231
+ if (sequenceTimerRef.current) {
232
+ clearTimeout(sequenceTimerRef.current)
233
+ sequenceTimerRef.current = null
234
+ }
235
+ }
236
+ }, [])
237
+
238
+ // Compute active shortcuts for help overlay (memoized on version changes).
239
+ // Shows ALL registered shortcuts regardless of `enabled` state — the help overlay
240
+ // should present a stable view of what's available, not flicker based on transient state.
241
+ const activeShortcuts = useMemo(() =>
242
+ Array.from(shortcutsRef.current.values())
243
+ .map(({ _registrationId, ...s }) => s as KeyboardShortcut)
244
+ // Deduplicate by id (keep highest priority)
245
+ .reduce((acc, s) => {
246
+ const existing = acc.find(a => a.id === s.id)
247
+ if (!existing || SCOPE_PRIORITY[s.scope] > SCOPE_PRIORITY[existing.scope]) {
248
+ return [...acc.filter(a => a.id !== s.id), s]
249
+ }
250
+ return acc
251
+ }, [] as KeyboardShortcut[])
252
+ .sort((a, b) => {
253
+ // Sort by category, then by keys
254
+ if (a.category !== b.category) return a.category.localeCompare(b.category)
255
+ return a.keys.localeCompare(b.keys)
256
+ })
257
+ , [version])
258
+
259
+ return (
260
+ <KeyboardShortcutContext.Provider value={{ registerShortcut, activeShortcuts }}>
261
+ {children}
262
+ </KeyboardShortcutContext.Provider>
263
+ )
264
+ }
265
+
266
+ /** Register a keyboard shortcut. Automatically deregisters on unmount or when key config changes. */
267
+ export function useRegisterShortcut(shortcut: KeyboardShortcut) {
268
+ const ctx = useContext(KeyboardShortcutContext)
269
+ const register = ctx?.registerShortcut
270
+ const handlerRef = useRef(shortcut.handler)
271
+ handlerRef.current = shortcut.handler
272
+
273
+ const stableHandler = useCallback((e: KeyboardEvent) => {
274
+ handlerRef.current(e)
275
+ }, [])
276
+
277
+ useEffect(() => {
278
+ if (!register) {
279
+ if (import.meta.env.DEV) {
280
+ console.warn(`[useRegisterShortcut] Shortcut "${shortcut.id}" registered outside KeyboardShortcutProvider — it will not work.`)
281
+ }
282
+ return
283
+ }
284
+ return register({
285
+ ...shortcut,
286
+ handler: stableHandler,
287
+ })
288
+ // Re-register when key config changes (but NOT handler — that's stable via ref)
289
+ // eslint-disable-next-line react-hooks/exhaustive-deps
290
+ }, [register, shortcut.id, shortcut.keys, shortcut.scope, shortcut.enabled, stableHandler])
291
+ }
292
+
293
+ /** Register multiple shortcuts at once. */
294
+ export function useRegisterShortcuts(shortcuts: KeyboardShortcut[]) {
295
+ const ctx = useContext(KeyboardShortcutContext)
296
+ const handlersRef = useRef<Map<string, (e: KeyboardEvent) => void>>(new Map())
297
+
298
+ // Update handler refs
299
+ for (const s of shortcuts) {
300
+ handlersRef.current.set(s.id, s.handler)
301
+ }
302
+
303
+ const register = ctx?.registerShortcut
304
+
305
+ useEffect(() => {
306
+ if (!register) {
307
+ if (import.meta.env.DEV) {
308
+ console.warn(`[useRegisterShortcuts] ${shortcuts.length} shortcuts (${shortcuts.map(s => s.id).join(', ')}) registered outside KeyboardShortcutProvider — they will not work.`)
309
+ }
310
+ return
311
+ }
312
+
313
+ const cleanups: (() => void)[] = []
314
+ for (const shortcut of shortcuts) {
315
+ const handler = handlersRef.current.get(shortcut.id)
316
+ if (handler) {
317
+ cleanups.push(register({
318
+ ...shortcut,
319
+ handler: (e) => {
320
+ const current = handlersRef.current.get(shortcut.id)
321
+ current?.(e)
322
+ },
323
+ }))
324
+ }
325
+ }
326
+
327
+ return () => cleanups.forEach(fn => fn())
328
+ // eslint-disable-next-line react-hooks/exhaustive-deps
329
+ }, [register, shortcuts.map(s => `${s.id}:${s.keys}:${s.scope}:${s.enabled}`).join('|')])
330
+ }
331
+
332
+ /** Get all active shortcuts (for help overlay). */
333
+ export function useActiveShortcuts(): KeyboardShortcut[] {
334
+ const ctx = useContext(KeyboardShortcutContext)
335
+ return ctx?.activeShortcuts ?? []
336
+ }
@@ -0,0 +1,58 @@
1
+ import { useState, useCallback, useRef } from 'react'
2
+
3
+ const MIN_SPIN_DURATION = 400 // ms
4
+ const SUCCESS_DISPLAY_DURATION = 1200 // ms
5
+
6
+ type RefreshPhase = 'idle' | 'spinning' | 'success'
7
+
8
+ /**
9
+ * Hook that provides a three-phase refresh animation:
10
+ * idle → spinning → success (checkmark) → idle
11
+ *
12
+ * @param refetchFn - The actual refetch function to call
13
+ * @returns [wrappedRefetch, phase] - A wrapped function and current animation phase
14
+ */
15
+ export function useRefreshAnimation(refetchFn: () => void | Promise<unknown>): [() => void, boolean, RefreshPhase] {
16
+ const [phase, setPhase] = useState<RefreshPhase>('idle')
17
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
18
+
19
+ const wrappedRefetch = useCallback(() => {
20
+ if (timeoutRef.current) {
21
+ clearTimeout(timeoutRef.current)
22
+ }
23
+
24
+ setPhase('spinning')
25
+
26
+ const result = refetchFn()
27
+ const startTime = Date.now()
28
+
29
+ const showSuccess = () => {
30
+ const elapsed = Date.now() - startTime
31
+ const remaining = MIN_SPIN_DURATION - elapsed
32
+
33
+ const transitionToSuccess = () => {
34
+ setPhase('success')
35
+ timeoutRef.current = setTimeout(() => {
36
+ setPhase('idle')
37
+ }, SUCCESS_DISPLAY_DURATION)
38
+ }
39
+
40
+ if (remaining > 0) {
41
+ timeoutRef.current = setTimeout(transitionToSuccess, remaining)
42
+ } else {
43
+ transitionToSuccess()
44
+ }
45
+ }
46
+
47
+ if (result instanceof Promise) {
48
+ result.finally(showSuccess)
49
+ } else {
50
+ showSuccess()
51
+ }
52
+ }, [refetchFn])
53
+
54
+ // isAnimating = backward compat (true when not idle)
55
+ const isAnimating = phase !== 'idle'
56
+
57
+ return [wrappedRefetch, isAnimating, phase]
58
+ }
package/src/index.ts ADDED
@@ -0,0 +1,35 @@
1
+ // @skyhook/k8s-ui — Shared Kubernetes UI types, utilities, and components
2
+ // Used by both radar (OSS) and koala-frontend (Skyhook platform)
3
+
4
+ // Types
5
+ export * from './types'
6
+
7
+ // Utilities
8
+ export * from './utils'
9
+
10
+ // Resource utilities (status extractors, formatters)
11
+ export * from './components/resources'
12
+
13
+ // UI primitives
14
+ export * from './components/ui'
15
+
16
+ // Hooks
17
+ export * from './hooks'
18
+
19
+ // Logs
20
+ export * from './components/logs'
21
+
22
+ // Timeline
23
+ export * from './components/timeline'
24
+
25
+ // Shared components (ResourceRendererDispatch, EditableYamlView, ResourceActionsBar)
26
+ export * from './components/shared'
27
+
28
+ // Workload components (WorkloadView, ResourceDetailDrawer)
29
+ export * from './components/workload'
30
+
31
+ // Dock (DockProvider, BottomDock, useDock, useOpenLogs, useOpenWorkloadLogs, useOpenTerminal)
32
+ export * from './components/dock'
33
+
34
+ // Topology (TopologyGraph, TopologySearch, TopologyFilterSidebar, layout utilities)
35
+ export * from './components/topology'