@skyhook-io/k8s-ui 1.12.0 → 1.13.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 +4 -4
- package/src/components/applications/ApplicationDetail.tsx +2 -2
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +32 -7
- package/src/components/issues/severity.ts +1 -0
- package/src/components/resources/ResourcesView.empty-note.test.ts +41 -0
- package/src/components/resources/ResourcesView.tsx +419 -9
- package/src/components/resources/index.ts +27 -0
- package/src/components/resources/kyverno-legacy-action.test.ts +98 -0
- package/src/components/resources/renderers/AnalysisRunRenderer.test.tsx +241 -0
- package/src/components/resources/renderers/AnalysisRunRenderer.tsx +166 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +31 -2
- package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +33 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +47 -4
- package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +243 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.test.tsx +99 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +235 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +23 -1
- package/src/components/resources/renderers/CNPGShared.tsx +27 -0
- package/src/components/resources/renderers/CalicoHostEndpointRenderer.tsx +71 -0
- package/src/components/resources/renderers/CalicoIPPoolRenderer.tsx +32 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.test.tsx +205 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.tsx +537 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.test.tsx +223 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.tsx +670 -0
- package/src/components/resources/renderers/CalicoTierRenderer.tsx +19 -0
- package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +13 -6
- package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +3 -2
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.test.ts +31 -0
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +124 -0
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +28 -2
- package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +303 -0
- package/src/components/resources/renderers/LookupFailureNote.test.tsx +60 -0
- package/src/components/resources/renderers/LookupFailureNote.tsx +42 -0
- package/src/components/resources/renderers/NamespaceRenderer.test.ts +30 -0
- package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -3
- package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +27 -26
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +6 -3
- package/src/components/resources/renderers/PolicyCoverageSection.test.tsx +348 -0
- package/src/components/resources/renderers/PolicyCoverageSection.tsx +939 -0
- package/src/components/resources/renderers/PolicySection.test.tsx +59 -2
- package/src/components/resources/renderers/PolicySection.tsx +32 -5
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +491 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +391 -92
- package/src/components/resources/renderers/VeleroBSLRenderer.test.tsx +374 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +179 -3
- package/src/components/resources/renderers/VeleroBackupRenderer.test.tsx +221 -0
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +119 -6
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.test.tsx +69 -0
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +96 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.test.tsx +55 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +22 -4
- package/src/components/resources/renderers/VeleroRunMessages.test.tsx +152 -0
- package/src/components/resources/renderers/VeleroRunMessages.tsx +139 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.test.tsx +88 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +16 -6
- package/src/components/resources/renderers/calico-cells.tsx +177 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +100 -0
- package/src/components/resources/renderers/index.ts +16 -1
- package/src/components/resources/renderers/kyverno-cells.tsx +81 -0
- package/src/components/resources/renderers/network-policy-peer-styles.ts +25 -0
- package/src/components/resources/renderers/velero-cells.tsx +29 -7
- package/src/components/resources/renderers/velero-dead-ends.test.tsx +142 -0
- package/src/components/resources/resource-utils-calico.test.ts +215 -0
- package/src/components/resources/resource-utils-calico.ts +261 -0
- package/src/components/resources/resource-utils-cnpg-declarative.test.ts +104 -0
- package/src/components/resources/resource-utils-cnpg-objectstore.test.ts +122 -0
- package/src/components/resources/resource-utils-cnpg.test.ts +54 -0
- package/src/components/resources/resource-utils-cnpg.ts +264 -3
- package/src/components/resources/resource-utils-kyverno-modern.ts +24 -0
- package/src/components/resources/resource-utils-kyverno-queue.test.ts +164 -0
- package/src/components/resources/resource-utils-kyverno-queue.ts +198 -0
- package/src/components/resources/resource-utils-kyverno.ts +43 -7
- package/src/components/resources/resource-utils-velero.test.ts +26 -0
- package/src/components/resources/resource-utils-velero.ts +68 -0
- package/src/components/resources/resource-utils.ts +98 -0
- package/src/components/shared/ResourceActionsBar.tsx +101 -16
- package/src/components/shared/ResourceRendererDispatch.test.tsx +379 -3
- package/src/components/shared/ResourceRendererDispatch.tsx +199 -22
- package/src/components/shared/index.ts +1 -1
- package/src/components/shared/revision-role-badges.test.ts +120 -0
- package/src/components/topology/K8sResourceNode.test.ts +10 -0
- package/src/components/topology/K8sResourceNode.tsx +22 -0
- package/src/components/topology/TopologyFilterSidebar.test.tsx +31 -0
- package/src/components/topology/TopologyFilterSidebar.tsx +5 -0
- package/src/components/topology/TopologyGraph.tsx +6 -6
- package/src/components/topology/TopologySearch.tsx +7 -0
- package/src/components/topology/layout.ts +3 -0
- package/src/components/trace/ReachabilityView.tsx +1 -0
- package/src/components/trace/TracePanel.tsx +13 -2
- package/src/components/trace/reachFixtures.ts +26 -1
- package/src/components/trace/reachGraphModel.test.ts +20 -0
- package/src/components/trace/reachGraphModel.ts +8 -1
- package/src/components/trace/reachOrigins.test.ts +31 -0
- package/src/components/trace/reachOrigins.ts +11 -1
- package/src/components/ui/Badge.tsx +10 -0
- package/src/components/ui/InClusterConsentDialog.tsx +5 -4
- package/src/components/ui/ScheduleValue.test.tsx +80 -0
- package/src/components/ui/ScheduleValue.tsx +85 -0
- package/src/components/ui/SelectMenu.tsx +202 -28
- package/src/components/ui/drawer-components.test.tsx +17 -1
- package/src/components/ui/drawer-components.tsx +9 -2
- package/src/components/ui/index.ts +2 -0
- package/src/components/workload/WorkloadView.tsx +5 -2
- package/src/types/core.ts +36 -4
- package/src/types/policy.ts +153 -2
- 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 +5 -0
- package/src/utils/navigation.ts +5 -0
- package/src/utils/resource-icons.test.ts +49 -2
- package/src/utils/resource-icons.ts +29 -2
|
@@ -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>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import { renderToString } from 'react-dom/server'
|
|
3
|
-
import { ProblemAlerts, OperationalIssuesShownContext } from './drawer-components'
|
|
3
|
+
import { ProblemAlerts, OperationalIssuesShownContext, RelatedResourcesSection } from './drawer-components'
|
|
4
4
|
|
|
5
5
|
const problems = [
|
|
6
6
|
{ color: 'red' as const, message: 'Application is Degraded' },
|
|
@@ -33,3 +33,19 @@ describe('ProblemAlerts', () => {
|
|
|
33
33
|
expect(html).toContain('Application is OutOfSync')
|
|
34
34
|
})
|
|
35
35
|
})
|
|
36
|
+
|
|
37
|
+
describe('RelatedResourcesSection', () => {
|
|
38
|
+
it('renders an associated Node', () => {
|
|
39
|
+
const html = renderToString(
|
|
40
|
+
<RelatedResourcesSection
|
|
41
|
+
relationships={{ node: { kind: 'Node', namespace: '', name: 'worker-1' } }}
|
|
42
|
+
onNavigate={() => {}}
|
|
43
|
+
/>,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
expect(html).toContain('Related Resources')
|
|
47
|
+
expect(html).toContain('Node')
|
|
48
|
+
expect(html).toContain('worker-1')
|
|
49
|
+
expect(html).toContain('<button')
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -167,7 +167,10 @@ export function Property({ label, value, copyable, onCopy, copied }: PropertyPro
|
|
|
167
167
|
return (
|
|
168
168
|
<div className="flex items-start gap-2 text-sm">
|
|
169
169
|
<span className="text-theme-text-tertiary w-40 shrink-0">{label}</span>
|
|
170
|
-
|
|
170
|
+
{/* break-words, not break-all: a long unbroken token (image ref, UID)
|
|
171
|
+
still wraps because it has to, while a sentence keeps its words
|
|
172
|
+
intact. break-all split "PostgreSQL" across two lines. */}
|
|
173
|
+
<span className={clsx('text-theme-text-primary flex-1 min-w-0', isReactElement(value) ? 'break-normal' : 'break-words')}>{displayValue}</span>
|
|
171
174
|
{copyable && onCopy && !isReactElement(value) && (
|
|
172
175
|
<button
|
|
173
176
|
onClick={() => onCopy(strValue, labelKey)}
|
|
@@ -789,6 +792,7 @@ export function RelatedResourcesSection({ relationships, onNavigate }: RelatedRe
|
|
|
789
792
|
const hasRelationships =
|
|
790
793
|
relationships.owner ||
|
|
791
794
|
relationships.deployment ||
|
|
795
|
+
relationships.node ||
|
|
792
796
|
(relationships.children && relationships.children.length > 0) ||
|
|
793
797
|
(relationships.services && relationships.services.length > 0) ||
|
|
794
798
|
(relationships.ingresses && relationships.ingresses.length > 0) ||
|
|
@@ -815,6 +819,9 @@ export function RelatedResourcesSection({ relationships, onNavigate }: RelatedRe
|
|
|
815
819
|
{relationships.deployment && (
|
|
816
820
|
<RelationshipGroup label="Deployment" refs={[relationships.deployment]} onNavigate={onNavigate} />
|
|
817
821
|
)}
|
|
822
|
+
{relationships.node && (
|
|
823
|
+
<RelationshipGroup label="Node" refs={[relationships.node]} onNavigate={onNavigate} />
|
|
824
|
+
)}
|
|
818
825
|
{relationships.children && relationships.children.length > 0 && (
|
|
819
826
|
<RelationshipGroup label="Children" refs={dedupeRefs(relationships.children)} onNavigate={onNavigate} />
|
|
820
827
|
)}
|
|
@@ -870,7 +877,7 @@ interface RelationshipGroupProps {
|
|
|
870
877
|
|
|
871
878
|
const RELATIONSHIP_TRUNCATE_LIMIT = 10
|
|
872
879
|
|
|
873
|
-
function RelationshipGroup({ label, refs, onNavigate }: RelationshipGroupProps) {
|
|
880
|
+
export function RelationshipGroup({ label, refs, onNavigate }: RelationshipGroupProps) {
|
|
874
881
|
const [showAll, setShowAll] = useState(false)
|
|
875
882
|
if (!refs || refs.length === 0) return null
|
|
876
883
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { Tooltip, WithTooltip } from './Tooltip'
|
|
2
|
+
export { CronValue, TimeValue } from './ScheduleValue'
|
|
3
|
+
export type { CronDialect } from './ScheduleValue'
|
|
2
4
|
export { Badge } from './Badge'
|
|
3
5
|
export type { BadgeSeverity, BadgeSize, BadgeTone } from './Badge'
|
|
4
6
|
export { FreshnessControl } from './FreshnessControl'
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
BarChart3,
|
|
31
31
|
Network,
|
|
32
32
|
Stethoscope,
|
|
33
|
-
|
|
33
|
+
Coins,
|
|
34
34
|
} from 'lucide-react'
|
|
35
35
|
import type { TimelineEvent, ResourceRef, Relationships, SelectedResource, ResolvedEnvFrom, Topology, TopologyNode, HPADiagnosis, WorkloadPodInfo } from '../../types'
|
|
36
36
|
import type { GitOpsStatus } from '../../types/gitops'
|
|
@@ -506,6 +506,9 @@ export function WorkloadView({
|
|
|
506
506
|
Service: 1, Ingress: 1, HTTPRoute: 1,
|
|
507
507
|
ConfigMap: 2, Secret: 2,
|
|
508
508
|
HorizontalPodAutoscaler: 3, PodDisruptionBudget: 3, NetworkPolicy: 3,
|
|
509
|
+
CalicoNetworkPolicy: 3, CalicoGlobalNetworkPolicy: 3,
|
|
510
|
+
CalicoStagedNetworkPolicy: 3, CalicoStagedGlobalNetworkPolicy: 3,
|
|
511
|
+
CalicoStagedKubernetesNetworkPolicy: 3,
|
|
509
512
|
ReplicaSet: 4, Pod: 5,
|
|
510
513
|
}
|
|
511
514
|
return neighborhood.nodes
|
|
@@ -719,7 +722,7 @@ export function WorkloadView({
|
|
|
719
722
|
icon: <Stethoscope className="w-4 h-4" />,
|
|
720
723
|
hidden: !(renderDiagnoseTab && (isDiagnoseKind(apiKind, group) || (reachableVia?.length ?? 0) > 0)),
|
|
721
724
|
},
|
|
722
|
-
{ id: 'cost', label: 'Cost', icon: <
|
|
725
|
+
{ id: 'cost', label: 'Cost', icon: <Coins className="w-4 h-4" />, hidden: !costTabVisible },
|
|
723
726
|
{ id: 'yaml', label: 'YAML', icon: <FileText className="w-4 h-4" /> },
|
|
724
727
|
]
|
|
725
728
|
const requestedTabAvailable = tabs.some((tab) => tab.id === requestedTab && !tab.hidden)
|
package/src/types/core.ts
CHANGED
|
@@ -202,6 +202,11 @@ export type CoreNodeKind =
|
|
|
202
202
|
| 'KubeadmControlPlane' // Cluster API KubeadmControlPlane
|
|
203
203
|
| 'ClusterClass' // Cluster API ClusterClass
|
|
204
204
|
| 'MachineHealthCheck' // Cluster API MachineHealthCheck
|
|
205
|
+
| 'CalicoNetworkPolicy'
|
|
206
|
+
| 'CalicoGlobalNetworkPolicy'
|
|
207
|
+
| 'CalicoStagedNetworkPolicy'
|
|
208
|
+
| 'CalicoStagedGlobalNetworkPolicy'
|
|
209
|
+
| 'CalicoStagedKubernetesNetworkPolicy'
|
|
205
210
|
|
|
206
211
|
// NodeKind can be a core kind or any arbitrary CRD kind string
|
|
207
212
|
export type NodeKind = CoreNodeKind | (string & {})
|
|
@@ -236,6 +241,11 @@ export function displayKind(kind: string): string {
|
|
|
236
241
|
KubeadmControlPlane: 'Control Plane',
|
|
237
242
|
ClusterClass: 'ClusterClass',
|
|
238
243
|
MachineHealthCheck: 'Health Check',
|
|
244
|
+
CalicoNetworkPolicy: 'Calico NetworkPolicy',
|
|
245
|
+
CalicoGlobalNetworkPolicy: 'Calico GlobalNetworkPolicy',
|
|
246
|
+
CalicoStagedNetworkPolicy: 'Calico StagedNetworkPolicy',
|
|
247
|
+
CalicoStagedGlobalNetworkPolicy: 'Calico StagedGlobalNetworkPolicy',
|
|
248
|
+
CalicoStagedKubernetesNetworkPolicy: 'Calico StagedKubernetesNetworkPolicy',
|
|
239
249
|
}
|
|
240
250
|
return shortNames[kind] || kind
|
|
241
251
|
}
|
|
@@ -258,6 +268,7 @@ export interface TopologyEdge {
|
|
|
258
268
|
target: string
|
|
259
269
|
type: EdgeType
|
|
260
270
|
label?: string
|
|
271
|
+
partial?: boolean
|
|
261
272
|
/** Hover tooltip for the edge label. Used by the Reachability view to keep the
|
|
262
273
|
* DECLARED route path available when the label shows an overridden tested path. */
|
|
263
274
|
labelTitle?: string
|
|
@@ -444,12 +455,15 @@ export interface ClusterInfo {
|
|
|
444
455
|
// Context info for context switching
|
|
445
456
|
export interface ContextInfo {
|
|
446
457
|
name: string
|
|
458
|
+
/** Original context name inside its source file. Set for isolated sources
|
|
459
|
+
* so display code need not infer backend-added collision qualifiers. */
|
|
460
|
+
originalName?: string
|
|
447
461
|
cluster: string
|
|
448
462
|
user: string
|
|
449
463
|
namespace: string
|
|
450
464
|
isCurrent: boolean
|
|
451
|
-
/** Source kubeconfig label (e.g. "kube-cluster-paris"). Set by backend
|
|
452
|
-
*
|
|
465
|
+
/** Source kubeconfig label (e.g. "kube-cluster-paris"). Set by the backend
|
|
466
|
+
* for contexts loaded through the isolated source registry. */
|
|
453
467
|
source?: string
|
|
454
468
|
}
|
|
455
469
|
|
|
@@ -1204,6 +1218,13 @@ export interface TrafficFlow {
|
|
|
1204
1218
|
bytesSent: number
|
|
1205
1219
|
bytesRecv: number
|
|
1206
1220
|
connections: number
|
|
1221
|
+
/** The conversation's initiator could not be established, so source and
|
|
1222
|
+
* destination are ordered arbitrarily rather than describing a caller. */
|
|
1223
|
+
directionUnknown?: boolean
|
|
1224
|
+
/** 5xx responses per second. How a rate-based source reports failures: it
|
|
1225
|
+
* measures a rate rather than observing individual responses, so it has a
|
|
1226
|
+
* status code for no single flow. */
|
|
1227
|
+
errorRate?: number
|
|
1207
1228
|
verdict: string // forwarded, dropped, error
|
|
1208
1229
|
lastSeen: string // ISO date string
|
|
1209
1230
|
}
|
|
@@ -1236,6 +1257,9 @@ export interface AggregatedFlow {
|
|
|
1236
1257
|
bytesRecv: number
|
|
1237
1258
|
connections: number
|
|
1238
1259
|
lastSeen: string
|
|
1260
|
+
/** The conversation's initiator could not be established, so the endpoints are
|
|
1261
|
+
* ordered arbitrarily and the edge is drawn without an arrowhead. */
|
|
1262
|
+
directionUnknown?: boolean
|
|
1239
1263
|
l7Protocol?: string // HTTP, gRPC, DNS
|
|
1240
1264
|
requestCount?: number
|
|
1241
1265
|
errorCount?: number
|
|
@@ -1252,8 +1276,8 @@ export interface AggregatedFlow {
|
|
|
1252
1276
|
|
|
1253
1277
|
// Cluster info for traffic detection
|
|
1254
1278
|
export interface TrafficClusterInfo {
|
|
1255
|
-
platform: string // gke, eks, aks, generic
|
|
1256
|
-
cni: string // cilium, calico, flannel, vpc-cni, azure-cni
|
|
1279
|
+
platform: string // rke2, gke, eks, aks, minikube, kind, docker-desktop, openshift, rancher, generic
|
|
1280
|
+
cni: string // cilium, canal, calico, flannel, vpc-cni, azure-cni, gke-native, unknown
|
|
1257
1281
|
dataplaneV2: boolean
|
|
1258
1282
|
clusterName?: string
|
|
1259
1283
|
k8sVersion?: string
|
|
@@ -1307,6 +1331,11 @@ export interface TrafficFlowsResponse {
|
|
|
1307
1331
|
flows: TrafficFlow[]
|
|
1308
1332
|
aggregated: AggregatedFlow[]
|
|
1309
1333
|
warning?: string // Non-fatal warning (e.g., query errors)
|
|
1334
|
+
/** 'transient' (or absent) means the condition may clear on its own and a
|
|
1335
|
+
* retry is worthwhile. 'partial' means the flows are correct but incomplete
|
|
1336
|
+
* for a reason retrying cannot change, so show the warning next to them and
|
|
1337
|
+
* do not refetch. */
|
|
1338
|
+
warningKind?: 'transient' | 'partial'
|
|
1310
1339
|
}
|
|
1311
1340
|
|
|
1312
1341
|
// Wizard state for traffic setup
|
|
@@ -1424,6 +1453,9 @@ export interface WorkloadRevision {
|
|
|
1424
1453
|
isCurrent: boolean
|
|
1425
1454
|
replicas: number
|
|
1426
1455
|
template?: string // Pod template spec as YAML (for revision diff)
|
|
1456
|
+
// Rollouts only: mid-canary isCurrent (rolling out) and isStable (serving traffic) differ.
|
|
1457
|
+
isStable?: boolean
|
|
1458
|
+
podHash?: string
|
|
1427
1459
|
}
|
|
1428
1460
|
|
|
1429
1461
|
// Workload log stream event data
|
package/src/types/policy.ts
CHANGED
|
@@ -26,17 +26,168 @@ export interface PolicyResourceCounts {
|
|
|
26
26
|
skip: number
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/** One resource a policy recorded an outcome for, from
|
|
30
|
+
* GET /api/policy/policies/{policy}. */
|
|
31
|
+
export interface PolicyCoverageSubject {
|
|
32
|
+
group?: string
|
|
33
|
+
kind: string
|
|
34
|
+
namespace?: string
|
|
35
|
+
name: string
|
|
36
|
+
result: PolicyResult
|
|
37
|
+
message?: string
|
|
38
|
+
/** Matched through a rule Kyverno synthesised from an authored Pod rule,
|
|
39
|
+
* not through a rule the user wrote. */
|
|
40
|
+
autogen?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface PolicyCoverageRule {
|
|
44
|
+
/** Empty for the modern CEL families, which have no named rules. */
|
|
45
|
+
rule: string
|
|
46
|
+
counts: PolicyResourceCounts
|
|
47
|
+
subjects: PolicyCoverageSubject[]
|
|
48
|
+
total: number
|
|
49
|
+
truncated?: boolean
|
|
50
|
+
/** How many of `total` the namespace view filter held back. `counts` still
|
|
51
|
+
* describe every outcome, so the two must be read together. */
|
|
52
|
+
hiddenByFilter?: number
|
|
53
|
+
/** The non-passing part of `hiddenByFilter`. Only non-passing subjects are
|
|
54
|
+
* listed, so this is what separates a row the server cap dropped from one
|
|
55
|
+
* the view filter held back. */
|
|
56
|
+
hiddenNotable?: number
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** What one policy decided about the cluster — the inverse of
|
|
60
|
+
* PolicyResourceResponse.
|
|
61
|
+
*
|
|
62
|
+
* Carries one partiality the per-resource view never needed: a resource may be
|
|
63
|
+
* missing because the caller cannot read its namespace, so an absent subject is
|
|
64
|
+
* not evidence of absence. `withheldNamespaces` is how the UI says so. */
|
|
65
|
+
export interface PolicyCoverageResponse {
|
|
66
|
+
evaluated: boolean
|
|
67
|
+
status: string
|
|
68
|
+
reasonCode?: string
|
|
69
|
+
deniedGroups?: string[]
|
|
70
|
+
liveUpdates: boolean
|
|
71
|
+
/** The report-side name that matched — namespaced policies report as
|
|
72
|
+
* "namespace/name". */
|
|
73
|
+
policy: string
|
|
74
|
+
counts: PolicyResourceCounts
|
|
75
|
+
scopeNamespaces: number
|
|
76
|
+
withheldNamespaces: number
|
|
77
|
+
clusterScoped?: boolean
|
|
78
|
+
/** Separate from withheldNamespaces — a cluster-scoped subject has no
|
|
79
|
+
* namespace to name. */
|
|
80
|
+
withheldClusterScoped?: boolean
|
|
81
|
+
/** Outcomes dropped because they came only from a report family this caller
|
|
82
|
+
* cannot read. The server drops them before counting, so the note can state
|
|
83
|
+
* how many were left out rather than hedging that some might have been. */
|
|
84
|
+
withheldByFamily?: number
|
|
85
|
+
/** Report families the CALLER cannot read. Distinct from deniedGroups, which
|
|
86
|
+
* are the ones radar's own probe could not read. */
|
|
87
|
+
unreadableFamilies?: string[]
|
|
88
|
+
/** How many distinct resources these outcomes describe, and how many of those
|
|
89
|
+
* have at least one non-passing outcome. One resource matched by two rules
|
|
90
|
+
* produces two outcomes, so any sentence saying "resources" needs these —
|
|
91
|
+
* `counts` and `examined` are checks. Not derivable client-side: the per-rule
|
|
92
|
+
* subject lists are capped. */
|
|
93
|
+
subjects?: number
|
|
94
|
+
subjectsFailing?: number
|
|
95
|
+
/** How many outcomes back these counts. */
|
|
96
|
+
examined: number
|
|
97
|
+
/** How many subjects the namespace view filter held back, across all rules. */
|
|
98
|
+
hiddenByFilter?: number
|
|
99
|
+
engines?: string[]
|
|
100
|
+
rules: PolicyCoverageRule[]
|
|
101
|
+
}
|
|
102
|
+
|
|
29
103
|
export interface PolicyResourceResponse {
|
|
30
104
|
/** False whenever the answer is "we could not check". Never treat an empty
|
|
31
105
|
* findings array as "clean" without consulting this first. */
|
|
32
106
|
evaluated: boolean
|
|
33
107
|
status: string
|
|
34
108
|
reasonCode?: string
|
|
35
|
-
/** Report families
|
|
36
|
-
* the
|
|
109
|
+
/** Report families RADAR's own probe could not read, so nobody's answer
|
|
110
|
+
* carries them — not the same as `withheldByFamily`, which is what THIS
|
|
111
|
+
* caller may not see. Present even when evaluated: the answer is real but
|
|
112
|
+
* incomplete. */
|
|
37
113
|
deniedGroups?: string[]
|
|
38
114
|
/** False when the index is frozen at its initial contents. */
|
|
39
115
|
liveUpdates: boolean
|
|
40
116
|
counts: PolicyResourceCounts
|
|
41
117
|
findings: PolicyResourceFinding[]
|
|
118
|
+
/** Findings dropped because they came only from a report family this caller
|
|
119
|
+
* cannot read. `counts` excludes them too, so an all-passing count with this
|
|
120
|
+
* set is not an all-clear — it is the part of the answer they may see. */
|
|
121
|
+
withheldByFamily?: number
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Background work Kyverno currently has in flight for one policy, from
|
|
125
|
+
* GET /api/policy/policies/{policy}/queued. Computed server-side: the requests
|
|
126
|
+
* live in the engine's namespace, not the policy's. */
|
|
127
|
+
export interface PolicyQueuedResponse {
|
|
128
|
+
/** Zero is a real answer — Kyverno deletes these seconds after they complete. */
|
|
129
|
+
requests: number
|
|
130
|
+
byState?: Record<string, number>
|
|
131
|
+
/** Separates healthy churn from a stopped controller. */
|
|
132
|
+
oldestPending?: string
|
|
133
|
+
/** The only diagnosis these objects carry. */
|
|
134
|
+
messages?: string[]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** One Cluster pinned to a CloudNativePG image catalog. */
|
|
138
|
+
export interface CNPGCatalogUser {
|
|
139
|
+
namespace: string
|
|
140
|
+
name: string
|
|
141
|
+
/** Absent when the reference carries no major — the screen must not render
|
|
142
|
+
* "asks for PostgreSQL 0". */
|
|
143
|
+
major?: number
|
|
144
|
+
/** A catalog-pinned Cluster has no spec.imageName, so this is the only place
|
|
145
|
+
* the image it actually resolved appears. */
|
|
146
|
+
image?: string
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Clusters referencing one image catalog, from the CNPG catalog-users endpoint. */
|
|
150
|
+
export interface CNPGCatalogUsersResponse {
|
|
151
|
+
clusters: CNPGCatalogUser[]
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** What one Velero BackupStorageLocation holds, from the stored-backups lookup. */
|
|
155
|
+
export interface VeleroStoredBackupsResponse {
|
|
156
|
+
backups: Array<{
|
|
157
|
+
namespace: string
|
|
158
|
+
name: string
|
|
159
|
+
phase?: string
|
|
160
|
+
expiration?: string
|
|
161
|
+
completed?: string
|
|
162
|
+
}>
|
|
163
|
+
/** How many reached Completed and are still inside their retention — the rest
|
|
164
|
+
* are stored here but are not something to restore from. Counted over the
|
|
165
|
+
* whole location, like `stored`: recomputing it from the capped list above
|
|
166
|
+
* undercounts every location big enough to be capped. */
|
|
167
|
+
restorable: number
|
|
168
|
+
/** How many reached Completed but have aged out. Same scope as `restorable`. */
|
|
169
|
+
expired?: number
|
|
170
|
+
/** How many the location holds in total, counted before the list was capped. */
|
|
171
|
+
stored?: number
|
|
172
|
+
/** The list above is a page, not the whole location. */
|
|
173
|
+
truncated?: boolean
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** One warning or error behind a Velero run's counts, with the scope Velero
|
|
177
|
+
* filed it under: "velero", "cluster", or a namespace name. */
|
|
178
|
+
export interface VeleroMessage {
|
|
179
|
+
scope: string
|
|
180
|
+
message: string
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** What a Backup or Restore actually reported, fetched on demand.
|
|
184
|
+
*
|
|
185
|
+
* The counts live on the CR; these messages do not — they are in a results
|
|
186
|
+
* file in object storage that only Velero's controller can hand out a link to.
|
|
187
|
+
* So this is never present until someone asks for it. */
|
|
188
|
+
export interface VeleroRunMessagesResponse {
|
|
189
|
+
warnings: VeleroMessage[]
|
|
190
|
+
errors: VeleroMessage[]
|
|
191
|
+
/** Set when the results file held more than were returned. */
|
|
192
|
+
truncated?: boolean
|
|
42
193
|
}
|