@skyhook-io/k8s-ui 1.13.0 → 1.13.4
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 +2 -2
- package/src/components/applications/AppTooltips.tsx +13 -1
- package/src/components/applications/ApplicationDetail.test.tsx +25 -0
- package/src/components/applications/ApplicationDetail.tsx +41 -9
- package/src/components/applications/ApplicationsView.tsx +18 -3
- package/src/components/applications/ReadyBar.tsx +3 -2
- package/src/components/applications/applications-view.test.tsx +91 -0
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +35 -7
- package/src/components/issues/IssuesView.tsx +40 -31
- package/src/components/issues/ResourceIssuesSection.tsx +3 -2
- package/src/components/issues/index.ts +3 -1
- package/src/components/issues/issue-timing.ts +123 -4
- package/src/components/issues/issues.test.ts +221 -10
- package/src/components/issues/types.ts +31 -5
- package/src/components/namespace-switcher/NamespacePicker.tsx +21 -13
- package/src/components/resources/ResourcesView.tsx +973 -100
- package/src/components/resources/column-settings-key.test.ts +98 -0
- package/src/components/resources/column-visibility-merge.test.ts +64 -0
- package/src/components/resources/curated-column-ownership.test.ts +236 -0
- package/src/components/resources/gateway-policy-status.test.ts +291 -0
- package/src/components/resources/gateway-policy-status.ts +237 -0
- package/src/components/resources/generic-status.test.ts +199 -0
- package/src/components/resources/generic-status.ts +171 -0
- package/src/components/resources/gpu-ecosystem-registry.test.ts +63 -0
- package/src/components/resources/index.ts +9 -1
- package/src/components/resources/istio-gateway-filter.test.ts +97 -0
- package/src/components/resources/printer-columns.test.ts +298 -0
- package/src/components/resources/printer-columns.ts +249 -0
- package/src/components/resources/renderers/EventRenderer.test.tsx +50 -1
- package/src/components/resources/renderers/EventRenderer.tsx +3 -5
- package/src/components/resources/renderers/GenericRenderer.tsx +18 -83
- package/src/components/resources/renderers/HPARenderer.test.tsx +28 -0
- package/src/components/resources/renderers/HPARenderer.tsx +6 -1
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +2 -1
- package/src/components/resources/renderers/NamespaceRenderer.test.ts +30 -0
- package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -3
- package/src/components/resources/renderers/PVCRenderer.test.tsx +44 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +19 -2
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +35 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +22 -3
- package/src/components/resources/renderers/WorkloadRenderer.test.tsx +83 -0
- package/src/components/resources/renderers/WorkloadRenderer.tsx +25 -65
- package/src/components/resources/renderers/amd-gpu-cells.tsx +36 -0
- package/src/components/resources/renderers/inference-gateway-cells.tsx +62 -0
- package/src/components/resources/renderers/jobset-lws-cells.tsx +82 -0
- package/src/components/resources/renderers/kai-cells.tsx +67 -0
- package/src/components/resources/renderers/kaito-cells.tsx +62 -0
- package/src/components/resources/renderers/kserve-cells.tsx +145 -0
- package/src/components/resources/renderers/kubeflow-training-cells.tsx +72 -0
- package/src/components/resources/renderers/kueue-cells.tsx +176 -0
- package/src/components/resources/renderers/nim-cells.tsx +81 -0
- package/src/components/resources/renderers/ray-cells.tsx +124 -0
- package/src/components/resources/renderers/volcano-cells.tsx +140 -0
- package/src/components/resources/resource-utils-amd-gpu.ts +61 -0
- package/src/components/resources/resource-utils-gpu-ecosystem.test.ts +222 -0
- package/src/components/resources/resource-utils-hpa.test.ts +4 -0
- package/src/components/resources/resource-utils-hpa.ts +6 -2
- package/src/components/resources/resource-utils-inference-gateway.ts +119 -0
- package/src/components/resources/resource-utils-jobset-lws.ts +117 -0
- package/src/components/resources/resource-utils-kai.ts +105 -0
- package/src/components/resources/resource-utils-kaito.ts +114 -0
- package/src/components/resources/resource-utils-kserve.ts +186 -0
- package/src/components/resources/resource-utils-kubeflow-training.ts +121 -0
- package/src/components/resources/resource-utils-kueue.ts +198 -0
- package/src/components/resources/resource-utils-nim.ts +109 -0
- package/src/components/resources/resource-utils-ray.ts +214 -0
- package/src/components/resources/resource-utils-rollout.test.ts +183 -0
- package/src/components/resources/resource-utils-velero.test.ts +30 -0
- package/src/components/resources/resource-utils-volcano.ts +176 -0
- package/src/components/resources/resource-utils.ts +156 -47
- package/src/components/resources/resources-column-filter.test.ts +27 -1
- package/src/components/resources/workload-rollout-golden.test.ts +55 -0
- package/src/components/shared/CreateResourceDialog.tsx +1 -0
- package/src/components/shared/EditableYamlView.tsx +4 -2
- package/src/components/shared/ResourceActionsBar.tsx +170 -25
- package/src/components/shared/ResourceRendererDispatch.test.tsx +206 -1
- package/src/components/shared/ResourceRendererDispatch.tsx +84 -40
- package/src/components/shared/SetImageDialog.test.ts +129 -0
- package/src/components/shared/SetImageDialog.tsx +577 -0
- package/src/components/shared/index.ts +1 -0
- package/src/components/shared/revision-role-badges.test.ts +56 -4
- package/src/components/timeline/TimelineList.tsx +19 -3
- package/src/components/timeline/TimelineSwimlanes.tsx +11 -3
- package/src/components/trace/ReachabilityView.tsx +1 -1
- package/src/components/trace/TracePanel.tsx +1 -0
- package/src/components/ui/InClusterConsentDialog.tsx +5 -4
- package/src/components/ui/SelectMenu.tsx +202 -28
- package/src/components/ui/drawer-components.tsx +12 -0
- package/src/components/workload/WorkloadRolloutNotice.tsx +60 -0
- package/src/components/workload/WorkloadView.tsx +146 -83
- package/src/hooks/useProgressiveRefresh.ts +37 -0
- package/src/source-hygiene.test.ts +28 -0
- package/src/theme/tailwind-theme.css +4 -0
- package/src/types/core.ts +62 -4
- package/src/utils/api-resources.test.ts +40 -1
- package/src/utils/api-resources.ts +11 -1
- package/src/utils/applications.ts +50 -0
- package/src/utils/bulk-workload-actions.test.ts +20 -0
- package/src/utils/bulk-workload-actions.ts +15 -0
- package/src/utils/clipboard.test.ts +86 -0
- package/src/utils/clipboard.ts +50 -0
- package/src/utils/extended-resources.ts +4 -7
- package/src/utils/format.test.ts +83 -1
- package/src/utils/format.ts +80 -1
- package/src/utils/inClusterConsent.test.ts +9 -0
- package/src/utils/inClusterConsent.ts +9 -9
- package/src/utils/index.ts +1 -0
- package/src/utils/navigation.test.ts +31 -1
- package/src/utils/navigation.ts +21 -2
- package/src/utils/workload-rollout.ts +292 -0
|
@@ -30,7 +30,7 @@ import { isChangeEvent, isHistoricalEvent } from '../../types'
|
|
|
30
30
|
import { getHealthBadgeColor, SEVERITY_BADGE } from '../../utils/badge-colors'
|
|
31
31
|
import { ResourceRefBadge } from '../ui/drawer-components'
|
|
32
32
|
import type { NavigateToResource } from '../../utils/navigation'
|
|
33
|
-
import {
|
|
33
|
+
import { kindToPluralWithGroup, refToSelectedResource, apiVersionToGroup } from '../../utils/navigation'
|
|
34
34
|
import { pluralize } from '../../utils/pluralize'
|
|
35
35
|
|
|
36
36
|
/** Format resource age (e.g., "3d", "5h", "10m") */
|
|
@@ -604,7 +604,15 @@ function ActivityCard({ item, expanded, onToggle, onResourceClick, compact, sele
|
|
|
604
604
|
<button
|
|
605
605
|
onClick={(e) => {
|
|
606
606
|
e.stopPropagation()
|
|
607
|
-
onResourceClick?.({
|
|
607
|
+
onResourceClick?.({
|
|
608
|
+
kind: kindToPluralWithGroup(
|
|
609
|
+
item.kind,
|
|
610
|
+
apiVersionToGroup(item.apiVersion),
|
|
611
|
+
),
|
|
612
|
+
namespace: item.namespace,
|
|
613
|
+
name: item.name,
|
|
614
|
+
group: apiVersionToGroup(item.apiVersion),
|
|
615
|
+
})
|
|
608
616
|
}}
|
|
609
617
|
className="flex items-center gap-2 hover:bg-theme-elevated/50 rounded px-1 -ml-1 transition-colors group"
|
|
610
618
|
>
|
|
@@ -777,7 +785,15 @@ function AggregatedActivityCard({ first, last, count, reason, expanded, onToggle
|
|
|
777
785
|
<button
|
|
778
786
|
onClick={(e) => {
|
|
779
787
|
e.stopPropagation()
|
|
780
|
-
onResourceClick?.({
|
|
788
|
+
onResourceClick?.({
|
|
789
|
+
kind: kindToPluralWithGroup(
|
|
790
|
+
first.kind,
|
|
791
|
+
apiVersionToGroup(first.apiVersion),
|
|
792
|
+
),
|
|
793
|
+
namespace: first.namespace,
|
|
794
|
+
name: first.name,
|
|
795
|
+
group: apiVersionToGroup(first.apiVersion),
|
|
796
|
+
})
|
|
781
797
|
}}
|
|
782
798
|
className="flex items-center gap-2 hover:bg-theme-elevated/50 rounded px-1 -ml-1 transition-colors group"
|
|
783
799
|
>
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
} from 'lucide-react'
|
|
27
27
|
import type { TimelineEvent, Topology } from '../../types'
|
|
28
28
|
import type { NavigateToResource } from '../../utils/navigation'
|
|
29
|
-
import {
|
|
29
|
+
import { kindToPluralWithGroup, apiVersionToGroup } from '../../utils/navigation'
|
|
30
30
|
import { PaneLoader } from '../ui/PaneLoader'
|
|
31
31
|
import { TimelineToolbar } from './TimelineToolbar'
|
|
32
32
|
import { sortTimelineLanes, TIMELINE_SORT_DEFAULT, type TimelineSort } from './timeline-lane-sort'
|
|
@@ -770,7 +770,7 @@ export function TimelineSwimlanes({ events, isLoading, onResourceClick, viewMode
|
|
|
770
770
|
onNavigatePath(gitOpsPath)
|
|
771
771
|
return
|
|
772
772
|
}
|
|
773
|
-
onResourceClick?.({ kind:
|
|
773
|
+
onResourceClick?.({ kind: kindToPluralWithGroup(kind, group ?? ''), namespace, name, group })
|
|
774
774
|
}, [onNavigatePath, onResourceClick])
|
|
775
775
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
776
776
|
const [zoom, setZoom] = useState(1)
|
|
@@ -3097,7 +3097,15 @@ export function EventDetailPanel({ events, selectedId, onSelectId, onClose, onRe
|
|
|
3097
3097
|
// divider separates "the dot you clicked" from "what happened around it".
|
|
3098
3098
|
const neighborsFrom = events.length === 1 && railEvents.length > 1 ? 1 : -1
|
|
3099
3099
|
const openResource = () =>
|
|
3100
|
-
onResourceClick?.({
|
|
3100
|
+
onResourceClick?.({
|
|
3101
|
+
kind: kindToPluralWithGroup(
|
|
3102
|
+
selected.kind,
|
|
3103
|
+
apiVersionToGroup(selected.apiVersion),
|
|
3104
|
+
),
|
|
3105
|
+
namespace: selected.namespace,
|
|
3106
|
+
name: selected.name,
|
|
3107
|
+
group: apiVersionToGroup(selected.apiVersion),
|
|
3108
|
+
})
|
|
3101
3109
|
|
|
3102
3110
|
return (
|
|
3103
3111
|
<div
|
|
@@ -535,7 +535,7 @@ export function problemRows(
|
|
|
535
535
|
// entry can carry several findings, and one origin several failed routes.
|
|
536
536
|
// `k` is what `seen` just proved unique, so pairing them is what makes the
|
|
537
537
|
// React key safe rather than each caller having to get it right.
|
|
538
|
-
rows.push({ ...r, key: `${r.key}
|
|
538
|
+
rows.push({ ...r, key: `${r.key}\x00${k}` })
|
|
539
539
|
}
|
|
540
540
|
const refNode = (r?: ResourceRef) => (r ? `n:${r.kind}/${r.namespace ?? ''}/${r.name || 'pods'}` : '')
|
|
541
541
|
|
|
@@ -87,6 +87,7 @@ export interface InClusterCapability {
|
|
|
87
87
|
allowed: boolean
|
|
88
88
|
reason?: string
|
|
89
89
|
cluster?: string
|
|
90
|
+
clusterKey?: string
|
|
90
91
|
namespace: string
|
|
91
92
|
/** Per-call ceiling on probe Pods. Routes past it never run - ordering is
|
|
92
93
|
* deterministic, so re-running starts from the same first maxProbes. */
|
|
@@ -13,6 +13,7 @@ export interface ConsentRequest {
|
|
|
13
13
|
interface InClusterConsentDialogProps {
|
|
14
14
|
open: boolean
|
|
15
15
|
cluster?: string
|
|
16
|
+
clusterKey?: string
|
|
16
17
|
namespace: string
|
|
17
18
|
/** The requests this run will send, one per declared route. */
|
|
18
19
|
requests?: ConsentRequest[]
|
|
@@ -85,7 +86,7 @@ function cluster(name?: string): string {
|
|
|
85
86
|
// Confirms the mutating in-cluster reachability test before it spawns a Job/pod,
|
|
86
87
|
// naming the cluster it lands in. Permission is enforced upstream (the button only
|
|
87
88
|
// renders when the capability SSAR allows), so this is a safety confirm, not authz.
|
|
88
|
-
export function InClusterConsentDialog({ open, cluster, namespace, requests, untestedCount, maxProbes, onClose, onConfirm }: InClusterConsentDialogProps) {
|
|
89
|
+
export function InClusterConsentDialog({ open, cluster, clusterKey, namespace, requests, untestedCount, maxProbes, onClose, onConfirm }: InClusterConsentDialogProps) {
|
|
89
90
|
const [dontAskAgain, setDontAskAgain] = useState(false)
|
|
90
91
|
|
|
91
92
|
function handleClose() {
|
|
@@ -94,7 +95,7 @@ export function InClusterConsentDialog({ open, cluster, namespace, requests, unt
|
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
function handleConfirm() {
|
|
97
|
-
if (dontAskAgain &&
|
|
98
|
+
if (dontAskAgain && clusterKey) rememberInClusterConsent(clusterKey)
|
|
98
99
|
setDontAskAgain(false)
|
|
99
100
|
onConfirm()
|
|
100
101
|
}
|
|
@@ -117,11 +118,11 @@ export function InClusterConsentDialog({ open, cluster, namespace, requests, unt
|
|
|
117
118
|
cancelLabel="Cancel"
|
|
118
119
|
variant="warning"
|
|
119
120
|
>
|
|
120
|
-
{/* "Don't ask again" is stored per cluster. Without a
|
|
121
|
+
{/* "Don't ask again" is stored per cluster source. Without a source identity
|
|
121
122
|
there is nothing safe to key it on (one origin can front many
|
|
122
123
|
clusters), so consent is never remembered - hide the checkbox rather
|
|
123
124
|
than promise a persistence that doesn't stick. */}
|
|
124
|
-
{
|
|
125
|
+
{clusterKey ? (
|
|
125
126
|
<label className="flex items-center gap-2 text-sm text-theme-text-secondary">
|
|
126
127
|
<input
|
|
127
128
|
type="checkbox"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react'
|
|
2
|
-
import { Check, ChevronDown } from 'lucide-react'
|
|
1
|
+
import { useEffect, useId, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { Check, ChevronDown, Search } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
|
|
5
5
|
export interface SelectMenuOption {
|
|
@@ -13,67 +13,241 @@ export function SelectMenu({
|
|
|
13
13
|
onChange,
|
|
14
14
|
ariaLabel,
|
|
15
15
|
className,
|
|
16
|
+
searchPlaceholder,
|
|
16
17
|
}: {
|
|
17
18
|
value: string
|
|
18
19
|
options: SelectMenuOption[]
|
|
19
20
|
onChange: (value: string) => void
|
|
20
21
|
ariaLabel: string
|
|
21
22
|
className?: string
|
|
23
|
+
searchPlaceholder?: string
|
|
22
24
|
}) {
|
|
23
25
|
const [open, setOpen] = useState(false)
|
|
26
|
+
const [query, setQuery] = useState('')
|
|
27
|
+
const [highlightedIndex, setHighlightedIndex] = useState(0)
|
|
28
|
+
const listboxId = useId()
|
|
24
29
|
const rootRef = useRef<HTMLDivElement>(null)
|
|
30
|
+
const triggerRef = useRef<HTMLButtonElement>(null)
|
|
31
|
+
const searchInputRef = useRef<HTMLInputElement>(null)
|
|
32
|
+
const listRef = useRef<HTMLDivElement>(null)
|
|
33
|
+
const pointerDownInsideRef = useRef(false)
|
|
25
34
|
const selected = options.find((option) => option.value === value) ?? options[0]
|
|
35
|
+
const filteredOptions = useMemo(() => {
|
|
36
|
+
const normalized = query.trim().toLowerCase()
|
|
37
|
+
if (!normalized) return options
|
|
38
|
+
return options.filter((option) => option.label.toLowerCase().includes(normalized))
|
|
39
|
+
}, [options, query])
|
|
40
|
+
const selectedIsVisible = filteredOptions.some((option) => option.value === value)
|
|
41
|
+
|
|
42
|
+
const selectOption = (nextValue: string) => {
|
|
43
|
+
onChange(nextValue)
|
|
44
|
+
setOpen(false)
|
|
45
|
+
triggerRef.current?.focus()
|
|
46
|
+
}
|
|
26
47
|
|
|
27
48
|
useEffect(() => {
|
|
28
49
|
if (!open) return
|
|
29
50
|
const onPointerDown = (event: MouseEvent) => {
|
|
30
51
|
if (!rootRef.current?.contains(event.target as Node)) setOpen(false)
|
|
31
52
|
}
|
|
32
|
-
const onKeyDown = (event: KeyboardEvent) => {
|
|
33
|
-
if (event.key === 'Escape') setOpen(false)
|
|
34
|
-
}
|
|
35
53
|
document.addEventListener('mousedown', onPointerDown)
|
|
36
|
-
document.addEventListener('keydown', onKeyDown)
|
|
37
54
|
return () => {
|
|
38
55
|
document.removeEventListener('mousedown', onPointerDown)
|
|
39
|
-
document.removeEventListener('keydown', onKeyDown)
|
|
40
56
|
}
|
|
41
57
|
}, [open])
|
|
42
58
|
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!open) setQuery('')
|
|
61
|
+
}, [open])
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!open || !query) return
|
|
65
|
+
if (listRef.current) listRef.current.scrollTop = 0
|
|
66
|
+
}, [open, query])
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!open) return
|
|
70
|
+
if (!searchPlaceholder) {
|
|
71
|
+
triggerRef.current?.focus()
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
listRef.current
|
|
75
|
+
?.querySelector<HTMLElement>('[role="option"][aria-selected="true"]')
|
|
76
|
+
?.scrollIntoView({ block: 'nearest' })
|
|
77
|
+
}, [open, searchPlaceholder])
|
|
78
|
+
|
|
43
79
|
return (
|
|
44
|
-
<div
|
|
80
|
+
<div
|
|
81
|
+
ref={rootRef}
|
|
82
|
+
className={clsx('relative', className)}
|
|
83
|
+
onMouseDownCapture={() => {
|
|
84
|
+
pointerDownInsideRef.current = true
|
|
85
|
+
requestAnimationFrame(() => {
|
|
86
|
+
pointerDownInsideRef.current = false
|
|
87
|
+
})
|
|
88
|
+
}}
|
|
89
|
+
onKeyDown={(event) => {
|
|
90
|
+
if (event.key !== 'Escape' || !open) return
|
|
91
|
+
event.preventDefault()
|
|
92
|
+
event.stopPropagation()
|
|
93
|
+
setOpen(false)
|
|
94
|
+
triggerRef.current?.focus()
|
|
95
|
+
}}
|
|
96
|
+
onBlur={(event) => {
|
|
97
|
+
if (!open) return
|
|
98
|
+
if (event.relatedTarget && rootRef.current?.contains(event.relatedTarget)) return
|
|
99
|
+
if (!pointerDownInsideRef.current) {
|
|
100
|
+
setOpen(false)
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
requestAnimationFrame(() => {
|
|
104
|
+
if (!document.hasFocus() || !listRef.current) return
|
|
105
|
+
if (searchPlaceholder) searchInputRef.current?.focus()
|
|
106
|
+
else triggerRef.current?.focus()
|
|
107
|
+
})
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
45
110
|
<button
|
|
111
|
+
ref={triggerRef}
|
|
46
112
|
type="button"
|
|
47
113
|
aria-label={ariaLabel}
|
|
48
114
|
aria-haspopup="listbox"
|
|
49
115
|
aria-expanded={open}
|
|
50
|
-
|
|
116
|
+
aria-controls={open ? listboxId : undefined}
|
|
117
|
+
onClick={() => {
|
|
118
|
+
if (!open) {
|
|
119
|
+
setHighlightedIndex(
|
|
120
|
+
Math.max(
|
|
121
|
+
options.findIndex((option) => option.value === value),
|
|
122
|
+
0,
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
setOpen(!open)
|
|
127
|
+
}}
|
|
51
128
|
className="flex h-8 w-full items-center justify-between gap-2 rounded-md border border-theme-border bg-theme-elevated px-2.5 text-xs text-theme-text-primary transition-colors hover:bg-theme-hover"
|
|
52
129
|
>
|
|
53
130
|
<span className="truncate">{selected?.label}</span>
|
|
54
131
|
<ChevronDown className={clsx('h-3.5 w-3.5 shrink-0 text-theme-text-tertiary transition-transform', open && 'rotate-180')} />
|
|
55
132
|
</button>
|
|
56
133
|
{open && (
|
|
57
|
-
<div
|
|
58
|
-
{
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
134
|
+
<div
|
|
135
|
+
className={clsx(
|
|
136
|
+
'absolute top-full z-50 mt-1 min-w-full overflow-hidden rounded-md border border-theme-border bg-theme-surface shadow-theme-lg',
|
|
137
|
+
searchPlaceholder ? 'left-0 right-0' : 'right-0'
|
|
138
|
+
)}
|
|
139
|
+
>
|
|
140
|
+
{searchPlaceholder && (
|
|
141
|
+
<div
|
|
142
|
+
className="flex items-center gap-2 border-b border-theme-border px-2.5 py-2"
|
|
143
|
+
onMouseDown={(event) => {
|
|
144
|
+
if (event.target !== searchInputRef.current) event.preventDefault()
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
<Search className="h-3.5 w-3.5 shrink-0 text-theme-text-tertiary" />
|
|
148
|
+
<input
|
|
149
|
+
ref={searchInputRef}
|
|
150
|
+
autoFocus
|
|
151
|
+
value={query}
|
|
152
|
+
onChange={(event) => {
|
|
153
|
+
setQuery(event.target.value)
|
|
154
|
+
setHighlightedIndex(0)
|
|
69
155
|
}}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
156
|
+
onKeyDown={(event) => {
|
|
157
|
+
if (event.key === 'Enter' && filteredOptions.length > 0) {
|
|
158
|
+
event.preventDefault()
|
|
159
|
+
const nextIndex = Math.min(highlightedIndex, filteredOptions.length - 1)
|
|
160
|
+
selectOption(filteredOptions[nextIndex].value)
|
|
161
|
+
} else if (event.key === 'ArrowDown') {
|
|
162
|
+
event.preventDefault()
|
|
163
|
+
const optionElements = listRef.current?.querySelectorAll<HTMLButtonElement>('[role="option"]')
|
|
164
|
+
optionElements?.[Math.min(highlightedIndex, optionElements.length - 1)]?.focus()
|
|
165
|
+
}
|
|
166
|
+
}}
|
|
167
|
+
aria-label={searchPlaceholder}
|
|
168
|
+
role="combobox"
|
|
169
|
+
aria-autocomplete="list"
|
|
170
|
+
aria-controls={listboxId}
|
|
171
|
+
aria-expanded="true"
|
|
172
|
+
aria-activedescendant={
|
|
173
|
+
filteredOptions.length > 0
|
|
174
|
+
? `${listboxId}-option-${Math.min(highlightedIndex, filteredOptions.length - 1)}`
|
|
175
|
+
: undefined
|
|
176
|
+
}
|
|
177
|
+
placeholder={searchPlaceholder}
|
|
178
|
+
className="min-w-0 flex-1 bg-transparent text-xs text-theme-text-primary outline-none placeholder:text-theme-text-tertiary"
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
{searchPlaceholder && (
|
|
183
|
+
<span className="sr-only" aria-live="polite">
|
|
184
|
+
{filteredOptions.length === 0
|
|
185
|
+
? 'No matches.'
|
|
186
|
+
: `${filteredOptions.length} ${filteredOptions.length === 1 ? 'option' : 'options'} available.`}
|
|
187
|
+
</span>
|
|
188
|
+
)}
|
|
189
|
+
<div
|
|
190
|
+
id={listboxId}
|
|
191
|
+
ref={listRef}
|
|
192
|
+
role="listbox"
|
|
193
|
+
aria-label={ariaLabel}
|
|
194
|
+
onKeyDown={(event) => {
|
|
195
|
+
if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return
|
|
196
|
+
const optionElements = Array.from(
|
|
197
|
+
listRef.current?.querySelectorAll<HTMLButtonElement>('[role="option"]') ?? [],
|
|
198
|
+
)
|
|
199
|
+
const currentIndex = optionElements.indexOf(event.target as HTMLButtonElement)
|
|
200
|
+
if (currentIndex < 0) return
|
|
201
|
+
event.preventDefault()
|
|
202
|
+
if (event.key === 'ArrowUp' && currentIndex === 0 && searchPlaceholder) {
|
|
203
|
+
searchInputRef.current?.focus()
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
const nextIndex = event.key === 'Home'
|
|
207
|
+
? 0
|
|
208
|
+
: event.key === 'End'
|
|
209
|
+
? optionElements.length - 1
|
|
210
|
+
: event.key === 'ArrowDown'
|
|
211
|
+
? Math.min(currentIndex + 1, optionElements.length - 1)
|
|
212
|
+
: Math.max(currentIndex - 1, 0)
|
|
213
|
+
optionElements[nextIndex]?.focus()
|
|
214
|
+
}}
|
|
215
|
+
className={clsx(filteredOptions.length > 0 && 'py-1', searchPlaceholder && 'max-h-64 overflow-y-auto')}
|
|
216
|
+
>
|
|
217
|
+
{filteredOptions.map((option, index) => {
|
|
218
|
+
const active = option.value === value
|
|
219
|
+
return (
|
|
220
|
+
<button
|
|
221
|
+
key={option.value}
|
|
222
|
+
id={`${listboxId}-option-${index}`}
|
|
223
|
+
type="button"
|
|
224
|
+
role="option"
|
|
225
|
+
aria-selected={active}
|
|
226
|
+
tabIndex={
|
|
227
|
+
searchPlaceholder ? (index === highlightedIndex ? 0 : -1) : active || (!selectedIsVisible && index === 0) ? 0 : -1
|
|
228
|
+
}
|
|
229
|
+
onClick={() => selectOption(option.value)}
|
|
230
|
+
onFocus={() => setHighlightedIndex(index)}
|
|
231
|
+
className={clsx(
|
|
232
|
+
'flex w-full items-center gap-2 px-2.5 py-1.5 text-left text-xs text-theme-text-secondary transition-colors hover:bg-theme-hover hover:text-theme-text-primary',
|
|
233
|
+
searchPlaceholder && index === highlightedIndex && 'bg-theme-hover text-theme-text-primary',
|
|
234
|
+
!searchPlaceholder && 'whitespace-nowrap'
|
|
235
|
+
)}
|
|
236
|
+
>
|
|
237
|
+
<Check className={clsx('h-3.5 w-3.5 shrink-0 text-accent', !active && 'opacity-0')} />
|
|
238
|
+
<span className={clsx(searchPlaceholder && 'truncate')}>{option.label}</span>
|
|
239
|
+
</button>
|
|
240
|
+
)
|
|
241
|
+
})}
|
|
242
|
+
</div>
|
|
243
|
+
{filteredOptions.length === 0 && (
|
|
244
|
+
<p
|
|
245
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
246
|
+
className="px-3 py-2 text-xs text-theme-text-tertiary"
|
|
247
|
+
>
|
|
248
|
+
No matches.
|
|
249
|
+
</p>
|
|
250
|
+
)}
|
|
77
251
|
</div>
|
|
78
252
|
)}
|
|
79
253
|
</div>
|
|
@@ -733,6 +733,18 @@ export function formatKindName(kind: string): string {
|
|
|
733
733
|
resourceclaims: 'ResourceClaim', resourceclaimtemplates: 'ResourceClaimTemplate',
|
|
734
734
|
deviceclasses: 'DeviceClass', resourceslices: 'ResourceSlice',
|
|
735
735
|
clusterpolicies: 'ClusterPolicy', nvidiadrivers: 'NVIDIADriver',
|
|
736
|
+
clusterqueues: 'ClusterQueue', localqueues: 'LocalQueue', resourceflavors: 'ResourceFlavor',
|
|
737
|
+
admissionchecks: 'AdmissionCheck', provisioningrequests: 'ProvisioningRequest',
|
|
738
|
+
rayclusters: 'RayCluster', rayjobs: 'RayJob', rayservices: 'RayService', raycronjobs: 'RayCronJob',
|
|
739
|
+
leaderworkersets: 'LeaderWorkerSet', jobsets: 'JobSet',
|
|
740
|
+
inferenceservices: 'InferenceService', servingruntimes: 'ServingRuntime',
|
|
741
|
+
clusterservingruntimes: 'ClusterServingRuntime', inferencegraphs: 'InferenceGraph',
|
|
742
|
+
trainedmodels: 'TrainedModel', llminferenceservices: 'LLMInferenceService',
|
|
743
|
+
inferencepools: 'InferencePool', inferenceobjectives: 'InferenceObjective',
|
|
744
|
+
jobflows: 'JobFlow', jobtemplates: 'JobTemplate', ragengines: 'RAGEngine',
|
|
745
|
+
nimservices: 'NIMService', nimcaches: 'NIMCache', nimpipelines: 'NIMPipeline',
|
|
746
|
+
deviceconfigs: 'DeviceConfig',
|
|
747
|
+
pytorchjobs: 'PyTorchJob', tfjobs: 'TFJob', mpijobs: 'MPIJob', trainjobs: 'TrainJob',
|
|
736
748
|
}
|
|
737
749
|
if (names[k]) return names[k]
|
|
738
750
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { AlertTriangle, Check, Clock3, Loader2, Pause } from 'lucide-react'
|
|
2
|
+
import { Badge, type BadgeSeverity } from '../ui/Badge'
|
|
3
|
+
import type { WorkloadRolloutActivity } from '../../utils/workload-rollout'
|
|
4
|
+
|
|
5
|
+
export function WorkloadRolloutNotice({
|
|
6
|
+
activity,
|
|
7
|
+
recentImageSave = false,
|
|
8
|
+
className = '',
|
|
9
|
+
}: {
|
|
10
|
+
activity: WorkloadRolloutActivity
|
|
11
|
+
recentImageSave?: boolean
|
|
12
|
+
className?: string
|
|
13
|
+
}) {
|
|
14
|
+
if (activity.phase === 'idle' && !recentImageSave) return null
|
|
15
|
+
const shown = activity.phase === 'idle' && recentImageSave
|
|
16
|
+
? { ...activity, phase: 'applying' as const, active: true, label: 'Waiting for controller', detail: 'The template change was accepted; waiting for live rollout status' }
|
|
17
|
+
: activity
|
|
18
|
+
const severity: BadgeSeverity = shown.phase === 'stalled'
|
|
19
|
+
? 'error'
|
|
20
|
+
: shown.phase === 'paused' || shown.manual
|
|
21
|
+
? 'warning'
|
|
22
|
+
: 'info'
|
|
23
|
+
const Icon = shown.phase === 'stalled'
|
|
24
|
+
? AlertTriangle
|
|
25
|
+
: shown.phase === 'paused'
|
|
26
|
+
? Pause
|
|
27
|
+
: shown.phase === 'waiting'
|
|
28
|
+
? Clock3
|
|
29
|
+
: Loader2
|
|
30
|
+
const warning = shown.phase === 'paused' || shown.manual
|
|
31
|
+
const containerTone = shown.phase === 'stalled'
|
|
32
|
+
? 'border-semantic-error/30 bg-semantic-error/5'
|
|
33
|
+
: warning
|
|
34
|
+
? 'border-semantic-warning/30 bg-semantic-warning/5'
|
|
35
|
+
: 'border-theme-border bg-accent-muted'
|
|
36
|
+
const iconTone = shown.phase === 'stalled'
|
|
37
|
+
? 'text-semantic-error'
|
|
38
|
+
: warning
|
|
39
|
+
? 'text-semantic-warning'
|
|
40
|
+
: 'text-accent-text'
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className={`rounded-lg border px-3 py-2.5 shadow-theme-sm ${containerTone} ${className}`} role={shown.phase === 'stalled' ? 'alert' : 'status'}>
|
|
44
|
+
<div className="flex min-w-0 items-start gap-2.5">
|
|
45
|
+
<Icon className={`mt-0.5 h-4 w-4 shrink-0 ${iconTone} ${shown.active && (shown.phase === 'applying' || shown.phase === 'progressing') ? 'animate-spin' : ''}`} />
|
|
46
|
+
<div className="min-w-0 flex-1">
|
|
47
|
+
<div className="flex min-w-0 flex-wrap items-center gap-1.5">
|
|
48
|
+
{recentImageSave && (
|
|
49
|
+
<span className="inline-flex items-center gap-1 text-xs font-medium text-theme-text-secondary">
|
|
50
|
+
<Check className="h-3.5 w-3.5" /> Image saved <span aria-hidden>·</span>
|
|
51
|
+
</span>
|
|
52
|
+
)}
|
|
53
|
+
<Badge severity={severity}>{shown.label}</Badge>
|
|
54
|
+
</div>
|
|
55
|
+
{shown.detail && <div className="mt-1 text-xs leading-5 text-theme-text-secondary">{shown.detail}</div>}
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
}
|