@skyhook-io/k8s-ui 1.4.1 → 1.4.3
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 +1 -1
- package/src/components/resources/ResourcesSidebar.tsx +152 -57
- package/src/components/resources/ResourcesView.tsx +296 -14
- package/src/components/resources/renderers/AWSMachineRenderer.tsx +96 -0
- package/src/components/resources/renderers/AWSMachineTemplateRenderer.tsx +39 -0
- package/src/components/resources/renderers/AWSManagedClusterRenderer.tsx +49 -0
- package/src/components/resources/renderers/AWSManagedControlPlaneRenderer.tsx +174 -0
- package/src/components/resources/renderers/AWSManagedMachinePoolRenderer.tsx +89 -0
- package/src/components/resources/renderers/AzureMachineRenderer.tsx +41 -0
- package/src/components/resources/renderers/AzureManagedControlPlaneRenderer.tsx +71 -0
- package/src/components/resources/renderers/AzureManagedMachinePoolRenderer.tsx +106 -0
- package/src/components/resources/renderers/CAPIClusterClassRenderer.tsx +156 -0
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +240 -0
- package/src/components/resources/renderers/CAPIKubeadmConfigRenderer.tsx +97 -0
- package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +124 -0
- package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +128 -0
- package/src/components/resources/renderers/CAPIMachineDrainRuleRenderer.tsx +38 -0
- package/src/components/resources/renderers/CAPIMachineHealthCheckRenderer.tsx +146 -0
- package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +92 -0
- package/src/components/resources/renderers/CAPIMachineRenderer.tsx +170 -0
- package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +101 -0
- package/src/components/resources/renderers/GCPMachineRenderer.tsx +53 -0
- package/src/components/resources/renderers/GCPManagedControlPlaneRenderer.tsx +75 -0
- package/src/components/resources/renderers/GCPManagedMachinePoolRenderer.tsx +102 -0
- package/src/components/resources/renderers/aws-capi-cells.tsx +117 -0
- package/src/components/resources/renderers/azure-capi-cells.tsx +83 -0
- package/src/components/resources/renderers/capi-cells.tsx +158 -0
- package/src/components/resources/renderers/gcp-capi-cells.tsx +68 -0
- package/src/components/resources/renderers/index.ts +25 -0
- package/src/components/resources/resource-utils-aws-capi.ts +211 -0
- package/src/components/resources/resource-utils-azure-capi.ts +123 -0
- package/src/components/resources/resource-utils-capi.ts +305 -0
- package/src/components/resources/resource-utils-gcp-capi.ts +130 -0
- package/src/components/shared/EditableYamlView.tsx +38 -3
- package/src/components/shared/ResourceRendererDispatch.tsx +97 -2
- package/src/components/topology/K8sResourceNode.tsx +8 -0
- package/src/components/topology/TopologyControls.tsx +16 -0
- package/src/components/topology/TopologyFilterSidebar.tsx +15 -0
- package/src/components/topology/TopologyGraph.tsx +2 -1
- package/src/components/topology/layout.ts +11 -0
- package/src/components/topology/topology.css +29 -0
- package/src/components/ui/Badge.tsx +10 -0
- package/src/components/ui/Tooltip.tsx +16 -0
- package/src/components/ui/drawer-components.tsx +54 -17
- package/src/types/core.ts +18 -2
- package/src/utils/api-resources.ts +4 -0
- package/src/utils/resource-icons.ts +35 -0
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useMemo, useEffect, useRef, forwardRef } from 'react'
|
|
1
|
+
import { useState, useMemo, useEffect, useRef, useCallback, forwardRef } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
Search,
|
|
4
4
|
ChevronDown,
|
|
@@ -44,6 +44,9 @@ export interface ResourcesSidebarProps {
|
|
|
44
44
|
onNavigate?: (path: string) => void
|
|
45
45
|
/** Base path for generating navigation URLs (e.g., '/org/clusters/id/k8s-resources') */
|
|
46
46
|
basePath?: string
|
|
47
|
+
/** Called when a kind is selected via keyboard (Enter in the filter). Parent uses this
|
|
48
|
+
* to move focus to the next UI level (e.g., the table search input). */
|
|
49
|
+
onKindNavigated?: () => void
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
// Persisted across remounts so collapsed categories survive tab switches
|
|
@@ -93,6 +96,8 @@ interface ResourceTypeButtonProps {
|
|
|
93
96
|
resource: APIResource
|
|
94
97
|
count: number
|
|
95
98
|
isSelected: boolean
|
|
99
|
+
/** Keyboard-highlight state (arrow nav in the filter input). */
|
|
100
|
+
isHighlighted?: boolean
|
|
96
101
|
isForbidden?: boolean
|
|
97
102
|
isPinned?: boolean
|
|
98
103
|
onTogglePin?: () => void
|
|
@@ -100,7 +105,7 @@ interface ResourceTypeButtonProps {
|
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
const ResourceTypeButton = forwardRef<HTMLButtonElement, ResourceTypeButtonProps>(
|
|
103
|
-
function ResourceTypeButton({ resource, count, isSelected, isForbidden: forbidden, isPinned, onTogglePin, onClick }, ref) {
|
|
108
|
+
function ResourceTypeButton({ resource, count, isSelected, isHighlighted, isForbidden: forbidden, isPinned, onTogglePin, onClick }, ref) {
|
|
104
109
|
const Icon = getResourceIcon(resource.kind)
|
|
105
110
|
return (
|
|
106
111
|
<button
|
|
@@ -110,9 +115,11 @@ const ResourceTypeButton = forwardRef<HTMLButtonElement, ResourceTypeButtonProps
|
|
|
110
115
|
'w-full flex items-center gap-2 px-2 xl:px-3 py-1.5 rounded-lg text-sm transition-colors group/kind min-w-0',
|
|
111
116
|
isSelected
|
|
112
117
|
? 'selection-strong selection-text'
|
|
113
|
-
:
|
|
114
|
-
? '
|
|
115
|
-
:
|
|
118
|
+
: isHighlighted
|
|
119
|
+
? 'bg-theme-hover text-theme-text-primary'
|
|
120
|
+
: forbidden
|
|
121
|
+
? 'text-theme-text-disabled hover:bg-theme-elevated hover:text-theme-text-secondary'
|
|
122
|
+
: 'text-theme-text-secondary hover:bg-theme-elevated hover:text-theme-text-primary'
|
|
116
123
|
)}
|
|
117
124
|
>
|
|
118
125
|
<Icon className="w-4 h-4 shrink-0" />
|
|
@@ -172,6 +179,7 @@ export function ResourcesSidebar({
|
|
|
172
179
|
className,
|
|
173
180
|
onNavigate,
|
|
174
181
|
basePath,
|
|
182
|
+
onKindNavigated,
|
|
175
183
|
}: ResourcesSidebarProps) {
|
|
176
184
|
// Wraps kind selection to also navigate when basePath/onNavigate are provided
|
|
177
185
|
const selectKind = (kind: SelectedKindInfo) => {
|
|
@@ -195,6 +203,15 @@ export function ResourcesSidebar({
|
|
|
195
203
|
// Ref to selected sidebar item for scrolling into view on deeplink
|
|
196
204
|
const selectedSidebarRef = useRef<HTMLButtonElement>(null)
|
|
197
205
|
|
|
206
|
+
// Ref to kind search input — auto-focused on mount so users can type a kind name immediately
|
|
207
|
+
const kindSearchRef = useRef<HTMLInputElement>(null)
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
// Only focus on fine-pointer devices to avoid popping up the virtual keyboard on touch
|
|
210
|
+
if (typeof window !== 'undefined' && window.matchMedia('(pointer: fine)').matches) {
|
|
211
|
+
kindSearchRef.current?.focus()
|
|
212
|
+
}
|
|
213
|
+
}, [])
|
|
214
|
+
|
|
198
215
|
// Effective selected kind — fall back to a safe default
|
|
199
216
|
const effectiveSelectedKind = selectedKind ?? { name: 'pods', kind: 'Pod', group: '' }
|
|
200
217
|
|
|
@@ -335,6 +352,69 @@ export function ResourcesSidebar({
|
|
|
335
352
|
})
|
|
336
353
|
}
|
|
337
354
|
|
|
355
|
+
// --- Keyboard navigation ---
|
|
356
|
+
// Flat list of all navigable kinds in the order they appear in the sidebar.
|
|
357
|
+
const flatVisibleKinds = useMemo<SelectedKindInfo[]>(() => {
|
|
358
|
+
const kinds: SelectedKindInfo[] = []
|
|
359
|
+
if (favoritesExpanded) {
|
|
360
|
+
for (const p of pinned) {
|
|
361
|
+
kinds.push({ name: p.name, kind: p.kind, group: p.group })
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (filteredCategories) {
|
|
365
|
+
for (const cat of filteredCategories) {
|
|
366
|
+
if (effectiveExpandedCategories.has(cat.name)) {
|
|
367
|
+
for (const r of cat.visibleResources) {
|
|
368
|
+
kinds.push({ name: r.name, kind: r.kind, group: r.group })
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return kinds
|
|
374
|
+
}, [favoritesExpanded, pinned, filteredCategories, effectiveExpandedCategories])
|
|
375
|
+
|
|
376
|
+
const [highlightedIndex, setHighlightedIndex] = useState(-1)
|
|
377
|
+
// Reset highlight when the filter or kind list changes
|
|
378
|
+
useEffect(() => {
|
|
379
|
+
setHighlightedIndex(kindFilter ? 0 : -1)
|
|
380
|
+
}, [kindFilter]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
381
|
+
|
|
382
|
+
const highlightedKind = highlightedIndex >= 0 && highlightedIndex < flatVisibleKinds.length
|
|
383
|
+
? flatVisibleKinds[highlightedIndex]
|
|
384
|
+
: null
|
|
385
|
+
|
|
386
|
+
// Scroll the highlighted kind button into view
|
|
387
|
+
const highlightedRef = useRef<HTMLButtonElement>(null)
|
|
388
|
+
useEffect(() => {
|
|
389
|
+
if (highlightedRef.current) {
|
|
390
|
+
highlightedRef.current.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
|
|
391
|
+
}
|
|
392
|
+
}, [highlightedIndex])
|
|
393
|
+
|
|
394
|
+
const handleSearchKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
395
|
+
if (e.key === 'Escape') {
|
|
396
|
+
setKindFilter('')
|
|
397
|
+
setHighlightedIndex(-1)
|
|
398
|
+
;(e.target as HTMLInputElement).blur()
|
|
399
|
+
} else if (e.key === 'ArrowDown') {
|
|
400
|
+
e.preventDefault()
|
|
401
|
+
setHighlightedIndex(prev => Math.min(prev + 1, flatVisibleKinds.length - 1))
|
|
402
|
+
} else if (e.key === 'ArrowUp') {
|
|
403
|
+
e.preventDefault()
|
|
404
|
+
setHighlightedIndex(prev => Math.max(prev - 1, 0))
|
|
405
|
+
} else if (e.key === 'Enter' && highlightedKind) {
|
|
406
|
+
e.preventDefault()
|
|
407
|
+
selectKind(highlightedKind)
|
|
408
|
+
setHighlightedIndex(-1)
|
|
409
|
+
setKindFilter('')
|
|
410
|
+
onKindNavigated?.()
|
|
411
|
+
}
|
|
412
|
+
}, [flatVisibleKinds.length, highlightedKind, selectKind, onKindNavigated]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
413
|
+
|
|
414
|
+
const isKindHighlighted = useCallback((name: string, group: string) => {
|
|
415
|
+
return highlightedKind?.name === name && highlightedKind?.group === group
|
|
416
|
+
}, [highlightedKind])
|
|
417
|
+
|
|
338
418
|
// Scroll sidebar to show selected kind on mount (deep linking) and on kind changes (keyboard nav)
|
|
339
419
|
const lastScrolledKind = useRef<string | null>(null)
|
|
340
420
|
const isInitialScroll = useRef(true)
|
|
@@ -362,11 +442,12 @@ export function ResourcesSidebar({
|
|
|
362
442
|
<div className="relative">
|
|
363
443
|
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-theme-text-tertiary" />
|
|
364
444
|
<input
|
|
445
|
+
ref={kindSearchRef}
|
|
365
446
|
type="text"
|
|
366
447
|
placeholder="Filter resources..."
|
|
367
448
|
value={kindFilter}
|
|
368
449
|
onChange={(e) => setKindFilter(e.target.value)}
|
|
369
|
-
onKeyDown={
|
|
450
|
+
onKeyDown={handleSearchKeyDown}
|
|
370
451
|
className="w-full pl-7 pr-7 py-2 bg-theme-elevated border border-theme-border-light rounded-lg text-sm text-theme-text-primary placeholder-theme-text-disabled focus:outline-none focus:ring-2 focus:ring-skyhook-500"
|
|
371
452
|
/>
|
|
372
453
|
{kindFilter && (
|
|
@@ -398,34 +479,41 @@ export function ResourcesSidebar({
|
|
|
398
479
|
</span>
|
|
399
480
|
)}
|
|
400
481
|
</button>
|
|
401
|
-
{
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
482
|
+
<div className={clsx(
|
|
483
|
+
'grid transition-[grid-template-rows] duration-200',
|
|
484
|
+
favoritesExpanded ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'
|
|
485
|
+
)} style={{ transitionTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)' }}>
|
|
486
|
+
<div className="overflow-hidden">
|
|
487
|
+
<div className="space-y-0.5">
|
|
488
|
+
{pinned.length === 0 ? (
|
|
489
|
+
<div className="px-3 py-2 text-xs text-theme-text-disabled">
|
|
490
|
+
No pinned resources. Click <Pin className="w-3 h-3 inline" /> on any resource type to pin it here.
|
|
491
|
+
</div>
|
|
492
|
+
) : (
|
|
493
|
+
pinned.map((p) => {
|
|
494
|
+
const isResourceSelected =
|
|
495
|
+
(effectiveSelectedKind.name === p.name && effectiveSelectedKind.group === p.group) ||
|
|
496
|
+
(effectiveSelectedKind.kind.toLowerCase() === p.kind.toLowerCase() && effectiveSelectedKind.group === p.group)
|
|
497
|
+
const highlighted = isKindHighlighted(p.name, p.group)
|
|
498
|
+
return (
|
|
499
|
+
<ResourceTypeButton
|
|
500
|
+
key={`${p.name}-${p.group}`}
|
|
501
|
+
ref={highlighted ? highlightedRef : (isResourceSelected ? selectedSidebarRef : null)}
|
|
502
|
+
resource={{ name: p.name, kind: p.kind, group: p.group, version: '', namespaced: true, isCrd: false, verbs: [] }}
|
|
503
|
+
count={counts?.[(p.group ? `${p.group}/${p.kind}` : p.kind)] ?? 0}
|
|
504
|
+
isSelected={isResourceSelected}
|
|
505
|
+
isHighlighted={highlighted}
|
|
506
|
+
isForbidden={forbiddenKinds.has(p.group ? `${p.group}/${p.kind}` : p.kind)}
|
|
507
|
+
isPinned={true}
|
|
508
|
+
onTogglePin={() => togglePin(p)}
|
|
509
|
+
onClick={() => selectKind({ name: p.name, kind: p.kind, group: p.group })}
|
|
510
|
+
/>
|
|
511
|
+
)
|
|
512
|
+
})
|
|
513
|
+
)}
|
|
514
|
+
</div>
|
|
427
515
|
</div>
|
|
428
|
-
|
|
516
|
+
</div>
|
|
429
517
|
</div>
|
|
430
518
|
{filteredCategories ? (
|
|
431
519
|
// Dynamic categories from API
|
|
@@ -449,31 +537,38 @@ export function ResourcesSidebar({
|
|
|
449
537
|
</span>
|
|
450
538
|
)}
|
|
451
539
|
</button>
|
|
452
|
-
{
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
540
|
+
<div className={clsx(
|
|
541
|
+
'grid transition-[grid-template-rows] duration-200',
|
|
542
|
+
isExpanded ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'
|
|
543
|
+
)} style={{ transitionTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)' }}>
|
|
544
|
+
<div className="overflow-hidden">
|
|
545
|
+
<div className="space-y-0.5">
|
|
546
|
+
{category.visibleResources.map((resource) => {
|
|
547
|
+
const resourceIsPinned = isPinned(resource.name, resource.group)
|
|
548
|
+
const isResourceSelected =
|
|
549
|
+
(effectiveSelectedKind.name === resource.name && effectiveSelectedKind.group === resource.group) ||
|
|
550
|
+
(effectiveSelectedKind.kind.toLowerCase() === resource.kind.toLowerCase() && effectiveSelectedKind.group === resource.group)
|
|
551
|
+
// If the resource is pinned, let the Favorites section own the highlight
|
|
552
|
+
const showSelected = isResourceSelected && !resourceIsPinned
|
|
553
|
+
const highlighted = isKindHighlighted(resource.name, resource.group)
|
|
554
|
+
return (
|
|
555
|
+
<ResourceTypeButton
|
|
556
|
+
key={resource.name}
|
|
557
|
+
ref={highlighted ? highlightedRef : (isResourceSelected ? selectedSidebarRef : null)}
|
|
558
|
+
resource={resource}
|
|
559
|
+
count={counts?.[(resource.group ? `${resource.group}/${resource.kind}` : resource.kind)] ?? 0}
|
|
560
|
+
isSelected={showSelected}
|
|
561
|
+
isHighlighted={highlighted}
|
|
562
|
+
isForbidden={forbiddenKinds.has(resource.group ? `${resource.group}/${resource.kind}` : resource.kind)}
|
|
563
|
+
isPinned={resourceIsPinned}
|
|
564
|
+
onTogglePin={() => togglePin({ name: resource.name, kind: resource.kind, group: resource.group })}
|
|
565
|
+
onClick={() => selectKind({ name: resource.name, kind: resource.kind, group: resource.group })}
|
|
566
|
+
/>
|
|
567
|
+
)
|
|
568
|
+
})}
|
|
569
|
+
</div>
|
|
475
570
|
</div>
|
|
476
|
-
|
|
571
|
+
</div>
|
|
477
572
|
</div>
|
|
478
573
|
)
|
|
479
574
|
})
|