@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.
- package/package.json +63 -0
- package/src/components/dock/BottomDock.tsx +204 -0
- package/src/components/dock/DockContext.tsx +212 -0
- package/src/components/dock/TerminalTab.tsx +333 -0
- package/src/components/dock/index.ts +3 -0
- package/src/components/gitops/GitOpsActions.tsx +227 -0
- package/src/components/gitops/GitOpsStatusBadge.tsx +221 -0
- package/src/components/gitops/ManagedResourcesList.tsx +210 -0
- package/src/components/gitops/SyncCountdown.tsx +146 -0
- package/src/components/gitops/index.ts +4 -0
- package/src/components/logs/JsonLogLine.tsx +90 -0
- package/src/components/logs/LogCore.tsx +553 -0
- package/src/components/logs/LogToolbarSelects.tsx +74 -0
- package/src/components/logs/LogsViewer.tsx +146 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +263 -0
- package/src/components/logs/index.ts +13 -0
- package/src/components/logs/useLogBuffer.ts +138 -0
- package/src/components/logs/useLogSearch.ts +151 -0
- package/src/components/logs/useLogStream.ts +61 -0
- package/src/components/resources/ResourcesView.tsx +5494 -0
- package/src/components/resources/index.ts +17 -0
- package/src/components/resources/renderers/AlertRenderer.tsx +166 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +233 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +89 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +194 -0
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +79 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +68 -0
- package/src/components/resources/renderers/CertificateRenderer.tsx +209 -0
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +123 -0
- package/src/components/resources/renderers/ChallengeRenderer.tsx +111 -0
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +164 -0
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +140 -0
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +153 -0
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +221 -0
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +124 -0
- package/src/components/resources/renderers/CronJobRenderer.tsx +86 -0
- package/src/components/resources/renderers/EventRenderer.tsx +175 -0
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +119 -0
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +172 -0
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +242 -0
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +263 -0
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +68 -0
- package/src/components/resources/renderers/GatewayRenderer.tsx +218 -0
- package/src/components/resources/renderers/GenericRenderer.tsx +476 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +154 -0
- package/src/components/resources/renderers/HPARenderer.tsx +164 -0
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +303 -0
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +167 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +49 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +104 -0
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +175 -0
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +176 -0
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +125 -0
- package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +84 -0
- package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +112 -0
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +288 -0
- package/src/components/resources/renderers/JobRenderer.tsx +103 -0
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +157 -0
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +188 -0
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +148 -0
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +90 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +136 -0
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +151 -0
- package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +70 -0
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +309 -0
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +142 -0
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +232 -0
- package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +97 -0
- package/src/components/resources/renderers/KnativeRouteRenderer.tsx +74 -0
- package/src/components/resources/renderers/KnativeServiceRenderer.tsx +138 -0
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +220 -0
- package/src/components/resources/renderers/KustomizationRenderer.tsx +151 -0
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +352 -0
- package/src/components/resources/renderers/LeaseRenderer.tsx +93 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +208 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +282 -0
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +153 -0
- package/src/components/resources/renderers/OrderRenderer.tsx +108 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +98 -0
- package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +138 -0
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +126 -0
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +63 -0
- package/src/components/resources/renderers/PodRenderer.tsx +677 -0
- package/src/components/resources/renderers/PriorityClassRenderer.tsx +50 -0
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +62 -0
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +71 -0
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +97 -0
- package/src/components/resources/renderers/RoleRenderer.tsx +192 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +227 -0
- package/src/components/resources/renderers/RuntimeClassRenderer.tsx +75 -0
- package/src/components/resources/renderers/SbomReportRenderer.tsx +98 -0
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +121 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +321 -0
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +111 -0
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +81 -0
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +65 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +98 -0
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +195 -0
- package/src/components/resources/renderers/StorageClassRenderer.tsx +82 -0
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +257 -0
- package/src/components/resources/renderers/VPARenderer.tsx +193 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +73 -0
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +193 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +161 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +134 -0
- package/src/components/resources/renderers/VeleroVSLRenderer.tsx +36 -0
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +347 -0
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +156 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +202 -0
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +91 -0
- package/src/components/resources/renderers/WorkloadRenderer.tsx +311 -0
- package/src/components/resources/renderers/argo-cells.tsx +118 -0
- package/src/components/resources/renderers/certmanager-cells.tsx +164 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +161 -0
- package/src/components/resources/renderers/eso-cells.tsx +115 -0
- package/src/components/resources/renderers/flux-cells.tsx +253 -0
- package/src/components/resources/renderers/index.ts +105 -0
- package/src/components/resources/renderers/istio-cells.tsx +210 -0
- package/src/components/resources/renderers/karpenter-cells.tsx +107 -0
- package/src/components/resources/renderers/keda-cells.tsx +94 -0
- package/src/components/resources/renderers/knative-cells.tsx +396 -0
- package/src/components/resources/renderers/kyverno-cells.tsx +85 -0
- package/src/components/resources/renderers/prometheus-cells.tsx +74 -0
- package/src/components/resources/renderers/traefik-cells.tsx +120 -0
- package/src/components/resources/renderers/trivy-cells.tsx +153 -0
- package/src/components/resources/renderers/trivy-shared.tsx +67 -0
- package/src/components/resources/renderers/velero-cells.tsx +164 -0
- package/src/components/resources/resource-utils-argo.ts +155 -0
- package/src/components/resources/resource-utils-certmanager.ts +198 -0
- package/src/components/resources/resource-utils-cnpg.ts +344 -0
- package/src/components/resources/resource-utils-eso.ts +385 -0
- package/src/components/resources/resource-utils-flux.ts +186 -0
- package/src/components/resources/resource-utils-istio.ts +372 -0
- package/src/components/resources/resource-utils-karpenter.ts +150 -0
- package/src/components/resources/resource-utils-keda.ts +203 -0
- package/src/components/resources/resource-utils-knative.ts +218 -0
- package/src/components/resources/resource-utils-kyverno.ts +224 -0
- package/src/components/resources/resource-utils-prometheus.ts +159 -0
- package/src/components/resources/resource-utils-traefik.ts +180 -0
- package/src/components/resources/resource-utils-trivy.ts +146 -0
- package/src/components/resources/resource-utils-velero.ts +293 -0
- package/src/components/resources/resource-utils.ts +1558 -0
- package/src/components/shared/EditableYamlView.tsx +302 -0
- package/src/components/shared/ResourceActionsBar.tsx +924 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +557 -0
- package/src/components/shared/index.ts +3 -0
- package/src/components/timeline/index.ts +1 -0
- package/src/components/timeline/shared.tsx +614 -0
- package/src/components/topology/GroupNode.tsx +198 -0
- package/src/components/topology/K8sResourceNode.tsx +440 -0
- package/src/components/topology/TopologyControls.tsx +62 -0
- package/src/components/topology/TopologyFilterSidebar.tsx +377 -0
- package/src/components/topology/TopologyGraph.tsx +748 -0
- package/src/components/topology/TopologySearch.tsx +304 -0
- package/src/components/topology/index.ts +7 -0
- package/src/components/topology/layout.ts +734 -0
- package/src/components/topology/layout.worker.ts +272 -0
- package/src/components/topology/topology.css +129 -0
- package/src/components/ui/CodeViewer.tsx +451 -0
- package/src/components/ui/ConfirmDialog.tsx +188 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +57 -0
- package/src/components/ui/HealthRing.tsx +72 -0
- package/src/components/ui/MetricsChart.tsx +259 -0
- package/src/components/ui/ResourceBar.tsx +68 -0
- package/src/components/ui/Toast.tsx +225 -0
- package/src/components/ui/Tooltip.tsx +165 -0
- package/src/components/ui/YamlEditor.tsx +280 -0
- package/src/components/ui/drawer-components.tsx +792 -0
- package/src/components/ui/index.ts +10 -0
- package/src/components/workload/ResourceDetailDrawer.tsx +158 -0
- package/src/components/workload/WorkloadView.tsx +1109 -0
- package/src/components/workload/index.ts +2 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/useAnimatedUnmount.ts +30 -0
- package/src/hooks/useKeyboardShortcuts.tsx +336 -0
- package/src/hooks/useRefreshAnimation.ts +58 -0
- package/src/index.ts +35 -0
- package/src/types/core.ts +894 -0
- package/src/types/gitops.ts +458 -0
- package/src/types/index.ts +2 -0
- package/src/utils/animation.ts +40 -0
- package/src/utils/api-resources.ts +185 -0
- package/src/utils/badge-colors.ts +399 -0
- package/src/utils/context-name.ts +72 -0
- package/src/utils/download.ts +19 -0
- package/src/utils/format.ts +233 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/log-format.ts +300 -0
- package/src/utils/navigation.ts +119 -0
- package/src/utils/resource-hierarchy.ts +630 -0
- package/src/utils/resource-icons.ts +206 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +24 -0
|
@@ -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'
|