@skyhook-io/k8s-ui 1.15.0 → 1.16.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 +3 -3
- package/src/components/audit/AuditFindingsTable.tsx +1 -1
- package/src/components/checks/ChecksView.tsx +8 -8
- package/src/components/gitops/GitOpsDetailLayout.tsx +3 -1
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +12 -2
- package/src/components/gitops/index.ts +1 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +4 -1
- package/src/components/issues/IssuesView.tsx +2 -2
- package/src/components/logs/LogCore.tsx +131 -97
- package/src/components/logs/StructuredLogLine.tsx +18 -28
- package/src/components/logs/log-palette.ts +4 -0
- package/src/components/logs/useLogBuffer.ts +8 -39
- package/src/components/logs/useLogSearch.ts +52 -24
- package/src/components/namespace-switcher/NamespacePicker.test.tsx +140 -0
- package/src/components/namespace-switcher/NamespacePicker.tsx +26 -20
- package/src/components/resources/renderers/PodRenderer.test.tsx +15 -0
- package/src/components/resources/renderers/PodRenderer.tsx +10 -2
- package/src/components/resources/renderers/PolicySection.test.tsx +42 -0
- package/src/components/resources/renderers/PolicySection.tsx +10 -1
- package/src/components/shared/DrainPlanDialog.test.tsx +10 -0
- package/src/components/shared/DrainPlanDialog.tsx +14 -3
- package/src/components/shared/ResourceActionsBar.tsx +14 -9
- package/src/components/ui/FilterPill.tsx +1 -1
- package/src/components/ui/RadarUpgradeNote.click.test.tsx +24 -0
- package/src/components/ui/RadarUpgradeNote.test.tsx +66 -0
- package/src/components/ui/RadarUpgradeNote.tsx +129 -0
- package/src/components/ui/ResourceBar.tsx +12 -6
- package/src/components/ui/SelectMenu.tsx +62 -19
- package/src/components/ui/index.ts +8 -0
- package/src/components/workload/WorkloadView.tsx +109 -3
- package/src/types/core.ts +24 -4
- package/src/types/fetch-error.ts +33 -0
- package/src/utils/log-format.ts +1 -1
- package/src/utils/log-level.test.ts +269 -0
- package/src/utils/log-level.ts +301 -0
- package/src/utils/resource-hierarchy.test.ts +69 -0
- package/src/utils/resource-hierarchy.ts +87 -52
|
@@ -7,6 +7,8 @@ import { TRANSITION_MENU, overlayExitMs, overlayTransitionStyle } from '../../ut
|
|
|
7
7
|
export interface SelectMenuOption {
|
|
8
8
|
value: string
|
|
9
9
|
label: string
|
|
10
|
+
description?: string
|
|
11
|
+
disabled?: boolean
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export function SelectMenu({
|
|
@@ -17,6 +19,8 @@ export function SelectMenu({
|
|
|
17
19
|
ariaDescribedBy,
|
|
18
20
|
className,
|
|
19
21
|
searchPlaceholder,
|
|
22
|
+
placeholder,
|
|
23
|
+
variant = 'default',
|
|
20
24
|
disabled = false,
|
|
21
25
|
id,
|
|
22
26
|
}: {
|
|
@@ -27,6 +31,8 @@ export function SelectMenu({
|
|
|
27
31
|
ariaDescribedBy?: string
|
|
28
32
|
className?: string
|
|
29
33
|
searchPlaceholder?: string
|
|
34
|
+
placeholder?: string
|
|
35
|
+
variant?: 'default' | 'text'
|
|
30
36
|
disabled?: boolean
|
|
31
37
|
id?: string
|
|
32
38
|
}) {
|
|
@@ -40,15 +46,33 @@ export function SelectMenu({
|
|
|
40
46
|
const searchInputRef = useRef<HTMLInputElement>(null)
|
|
41
47
|
const listRef = useRef<HTMLDivElement>(null)
|
|
42
48
|
const pointerDownInsideRef = useRef(false)
|
|
43
|
-
|
|
49
|
+
// A keyboard open lands on the selected option, as a native select does; a
|
|
50
|
+
// pointer open keeps focus on the trigger.
|
|
51
|
+
const focusOptionOnOpenRef = useRef(false)
|
|
52
|
+
const selected = options.find((option) => option.value === value) ?? (placeholder ? undefined : options[0])
|
|
44
53
|
const filteredOptions = useMemo(() => {
|
|
45
54
|
const normalized = query.trim().toLowerCase()
|
|
46
55
|
if (!normalized) return options
|
|
47
|
-
return options.filter((option) => option.label.toLowerCase().includes(normalized))
|
|
56
|
+
return options.filter((option) => `${option.label} ${option.description ?? ''}`.toLowerCase().includes(normalized))
|
|
48
57
|
}, [options, query])
|
|
49
58
|
const selectedIsVisible = filteredOptions.some((option) => option.value === value)
|
|
50
59
|
|
|
60
|
+
const focusTabbableOption = () => {
|
|
61
|
+
listRef.current?.querySelector<HTMLElement>('[role="option"][tabindex="0"]')?.focus()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const openMenu = () => {
|
|
65
|
+
setHighlightedIndex(
|
|
66
|
+
Math.max(
|
|
67
|
+
options.findIndex((option) => option.value === value),
|
|
68
|
+
0,
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
setOpen(true)
|
|
72
|
+
}
|
|
73
|
+
|
|
51
74
|
const selectOption = (nextValue: string) => {
|
|
75
|
+
if (options.find((option) => option.value === nextValue)?.disabled) return
|
|
52
76
|
onChange(nextValue)
|
|
53
77
|
setOpen(false)
|
|
54
78
|
triggerRef.current?.focus()
|
|
@@ -81,7 +105,12 @@ export function SelectMenu({
|
|
|
81
105
|
useEffect(() => {
|
|
82
106
|
if (!open || !shouldRender) return
|
|
83
107
|
if (!searchPlaceholder) {
|
|
84
|
-
|
|
108
|
+
if (focusOptionOnOpenRef.current) {
|
|
109
|
+
focusOptionOnOpenRef.current = false
|
|
110
|
+
focusTabbableOption()
|
|
111
|
+
} else {
|
|
112
|
+
triggerRef.current?.focus()
|
|
113
|
+
}
|
|
85
114
|
return
|
|
86
115
|
}
|
|
87
116
|
listRef.current
|
|
@@ -130,28 +159,38 @@ export function SelectMenu({
|
|
|
130
159
|
aria-expanded={open}
|
|
131
160
|
aria-controls={open ? listboxId : undefined}
|
|
132
161
|
onClick={() => {
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
162
|
+
if (open) setOpen(false)
|
|
163
|
+
else openMenu()
|
|
164
|
+
}}
|
|
165
|
+
onKeyDown={(event) => {
|
|
166
|
+
if (searchPlaceholder) return
|
|
167
|
+
const arrow = event.key === 'ArrowDown' || event.key === 'ArrowUp'
|
|
168
|
+
if (!arrow && (open || (event.key !== 'Enter' && event.key !== ' '))) return
|
|
169
|
+
event.preventDefault()
|
|
170
|
+
if (open) {
|
|
171
|
+
focusTabbableOption()
|
|
172
|
+
return
|
|
140
173
|
}
|
|
141
|
-
|
|
174
|
+
focusOptionOnOpenRef.current = true
|
|
175
|
+
openMenu()
|
|
142
176
|
}}
|
|
143
177
|
disabled={disabled}
|
|
144
|
-
className=
|
|
178
|
+
className={clsx(
|
|
179
|
+
'flex items-center gap-1 text-xs disabled:cursor-not-allowed disabled:opacity-50',
|
|
180
|
+
variant === 'text'
|
|
181
|
+
? 'text-accent-text hover:underline'
|
|
182
|
+
: 'h-8 w-full justify-between gap-2 rounded-md border border-theme-border bg-theme-elevated px-2.5 text-theme-text-primary transition-colors hover:bg-theme-hover disabled:hover:bg-theme-elevated',
|
|
183
|
+
)}
|
|
145
184
|
>
|
|
146
|
-
<span className="truncate">{selected?.label}</span>
|
|
147
|
-
<ChevronDown className={clsx('h-3.5 w-3.5 shrink-0 text-theme-text-tertiary
|
|
185
|
+
<span className="truncate">{selected?.label ?? placeholder}</span>
|
|
186
|
+
<ChevronDown className={clsx('h-3.5 w-3.5 shrink-0 transition-transform', variant !== 'text' && 'text-theme-text-tertiary', open && 'rotate-180')} />
|
|
148
187
|
</button>
|
|
149
188
|
{shouldRender && (
|
|
150
189
|
<div
|
|
151
190
|
inert={!open}
|
|
152
191
|
className={clsx(
|
|
153
192
|
'absolute top-full z-50 mt-1 min-w-full overflow-hidden rounded-md border border-theme-border bg-theme-surface shadow-theme-lg',
|
|
154
|
-
searchPlaceholder ? 'left-0 right-0 origin-top-left' : 'right-0 origin-top-right',
|
|
193
|
+
variant === 'text' ? 'left-0 w-72 max-w-[calc(100vw-4rem)] origin-top-left' : searchPlaceholder ? 'left-0 right-0 origin-top-left' : 'right-0 origin-top-right',
|
|
155
194
|
TRANSITION_MENU,
|
|
156
195
|
isOpen ? 'opacity-100 translate-y-0 scale-100' : 'opacity-0 -translate-y-1 scale-[0.97]',
|
|
157
196
|
!open && 'pointer-events-none',
|
|
@@ -177,8 +216,7 @@ export function SelectMenu({
|
|
|
177
216
|
onKeyDown={(event) => {
|
|
178
217
|
if (event.key === 'Enter' && filteredOptions.length > 0) {
|
|
179
218
|
event.preventDefault()
|
|
180
|
-
|
|
181
|
-
selectOption(filteredOptions[nextIndex].value)
|
|
219
|
+
selectOption(filteredOptions[Math.min(highlightedIndex, filteredOptions.length - 1)].value)
|
|
182
220
|
} else if (event.key === 'ArrowDown') {
|
|
183
221
|
event.preventDefault()
|
|
184
222
|
const optionElements = listRef.current?.querySelectorAll<HTMLButtonElement>('[role="option"]')
|
|
@@ -244,19 +282,24 @@ export function SelectMenu({
|
|
|
244
282
|
type="button"
|
|
245
283
|
role="option"
|
|
246
284
|
aria-selected={active}
|
|
285
|
+
aria-disabled={option.disabled || undefined}
|
|
247
286
|
tabIndex={
|
|
248
287
|
searchPlaceholder ? (index === highlightedIndex ? 0 : -1) : active || (!selectedIsVisible && index === 0) ? 0 : -1
|
|
249
288
|
}
|
|
250
289
|
onClick={() => selectOption(option.value)}
|
|
251
290
|
onFocus={() => setHighlightedIndex(index)}
|
|
252
291
|
className={clsx(
|
|
253
|
-
'flex w-full items-center gap-2 px-2.5 py-1.5 text-left text-xs text-theme-text-secondary transition-colors
|
|
292
|
+
'flex w-full items-center gap-2 px-2.5 py-1.5 text-left text-xs text-theme-text-secondary transition-colors',
|
|
293
|
+
option.disabled ? 'cursor-not-allowed opacity-60' : 'hover:bg-theme-hover hover:text-theme-text-primary',
|
|
254
294
|
searchPlaceholder && index === highlightedIndex && 'bg-theme-hover text-theme-text-primary',
|
|
255
295
|
!searchPlaceholder && 'whitespace-nowrap'
|
|
256
296
|
)}
|
|
257
297
|
>
|
|
258
298
|
<Check className={clsx('h-3.5 w-3.5 shrink-0 text-accent', !active && 'opacity-0')} />
|
|
259
|
-
<span className={clsx(searchPlaceholder && 'truncate')}>
|
|
299
|
+
<span className={clsx('min-w-0', searchPlaceholder && !option.description && 'truncate')}>
|
|
300
|
+
<span className={clsx(option.description && 'block break-words font-medium')}>{option.label}</span>
|
|
301
|
+
{option.description && <span className="block break-all text-theme-text-tertiary">{option.description}</span>}
|
|
302
|
+
</span>
|
|
260
303
|
</button>
|
|
261
304
|
)
|
|
262
305
|
})}
|
|
@@ -74,3 +74,11 @@ export {
|
|
|
74
74
|
export type { CardSectionTone } from "./CardSection";
|
|
75
75
|
export { MultiSelectPicker } from "./MultiSelectPicker";
|
|
76
76
|
export type { MultiSelectPickerProps } from "./MultiSelectPicker";
|
|
77
|
+
export {
|
|
78
|
+
RadarUpgradeNote,
|
|
79
|
+
RadarUpgradeAction,
|
|
80
|
+
RadarUpgradeContext,
|
|
81
|
+
radarUpgradeDetail,
|
|
82
|
+
radarUpgradeHeadline,
|
|
83
|
+
} from "./RadarUpgradeNote";
|
|
84
|
+
export type { RadarUpgradeActions } from "./RadarUpgradeNote";
|
|
@@ -56,7 +56,7 @@ import { ResourceRendererDispatch, getResourceStatus, diagnoseHealthHint, type D
|
|
|
56
56
|
import type { ScalerDiagnosis } from '../resources/renderers/WorkloadRenderer'
|
|
57
57
|
import { DetailShell, type DetailShellTab } from '../shared/DetailShell'
|
|
58
58
|
import { HelmManagedByChip, ManagedByChip, type HelmOwnerRef } from '../shared/ManagedByChip'
|
|
59
|
-
import { getKindColorOutline, displayKindName, OperationalIssuesShownContext, ResourceRefBadge } from '../ui/drawer-components'
|
|
59
|
+
import { AlertBanner, getKindColorOutline, displayKindName, OperationalIssuesShownContext, ResourceRefBadge } from '../ui/drawer-components'
|
|
60
60
|
import { Badge, type BadgeSeverity } from '../ui/Badge'
|
|
61
61
|
import { Tooltip } from '../ui/Tooltip'
|
|
62
62
|
import { midTruncate } from '../../utils/format'
|
|
@@ -181,8 +181,23 @@ interface WorkloadViewProps {
|
|
|
181
181
|
refetch?: () => void
|
|
182
182
|
|
|
183
183
|
// ── Timeline data ────────────────────────────────────────────────────────
|
|
184
|
-
/**
|
|
184
|
+
/** Timeline events the history lanes are built from: this workload's own
|
|
185
|
+
* history, or events for its namespace from a host that has no scoped query. */
|
|
185
186
|
allEvents?: TimelineEvent[]
|
|
187
|
+
/** Older history exists beyond allEvents. */
|
|
188
|
+
historyTruncated?: boolean
|
|
189
|
+
/** Loads the next older page of history; omit when the host can't page. */
|
|
190
|
+
onLoadOlderHistory?: () => void
|
|
191
|
+
loadingOlderHistory?: boolean
|
|
192
|
+
/** The last Load older attempt failed. */
|
|
193
|
+
olderHistoryError?: Error | null
|
|
194
|
+
/** allEvents holds only this workload's own history (what it runs and the
|
|
195
|
+
* resources attached to it), not its whole namespace. */
|
|
196
|
+
historyScoped?: boolean
|
|
197
|
+
/** Some related resources' history isn't included: there were more than the host follows. */
|
|
198
|
+
historyIncomplete?: boolean
|
|
199
|
+
historyError?: Error | null
|
|
200
|
+
onRetryHistory?: () => void
|
|
186
201
|
/** Persisted lifecycle events reconstructed for resources related to this workload. */
|
|
187
202
|
relatedTimelineEvents?: TimelineEvent[]
|
|
188
203
|
/** Whether timeline events are loading */
|
|
@@ -384,6 +399,14 @@ export function WorkloadView({
|
|
|
384
399
|
refetch: refetchProp,
|
|
385
400
|
// Timeline
|
|
386
401
|
allEvents,
|
|
402
|
+
historyTruncated = false,
|
|
403
|
+
onLoadOlderHistory,
|
|
404
|
+
loadingOlderHistory = false,
|
|
405
|
+
olderHistoryError = null,
|
|
406
|
+
historyScoped = false,
|
|
407
|
+
historyIncomplete = false,
|
|
408
|
+
historyError = null,
|
|
409
|
+
onRetryHistory,
|
|
387
410
|
relatedTimelineEvents = [],
|
|
388
411
|
eventsLoading = false,
|
|
389
412
|
topology,
|
|
@@ -501,9 +524,10 @@ export function WorkloadView({
|
|
|
501
524
|
events: allEvents,
|
|
502
525
|
topology,
|
|
503
526
|
rootResource: { kind, group, namespace, name },
|
|
527
|
+
eventsScopedToRoot: historyScoped,
|
|
504
528
|
groupByApp: true,
|
|
505
529
|
})
|
|
506
|
-
}, [allEvents, topology, kind, group, namespace, name])
|
|
530
|
+
}, [allEvents, topology, kind, group, namespace, name, historyScoped])
|
|
507
531
|
|
|
508
532
|
// Topology tab — the seeded neighborhood around this one workload (its
|
|
509
533
|
// ownership core + attached Services/config/policies), not the whole namespace.
|
|
@@ -1145,6 +1169,14 @@ export function WorkloadView({
|
|
|
1145
1169
|
<EventsTab
|
|
1146
1170
|
events={resourceEvents}
|
|
1147
1171
|
isLoading={eventsLoading}
|
|
1172
|
+
truncated={historyTruncated}
|
|
1173
|
+
onLoadOlder={onLoadOlderHistory}
|
|
1174
|
+
loadingOlder={loadingOlderHistory}
|
|
1175
|
+
olderError={olderHistoryError}
|
|
1176
|
+
scoped={historyScoped}
|
|
1177
|
+
incomplete={historyIncomplete}
|
|
1178
|
+
error={historyError}
|
|
1179
|
+
onRetry={onRetryHistory}
|
|
1148
1180
|
selectedEventId={selectedEventId}
|
|
1149
1181
|
onSelectEvent={setSelectedEventId}
|
|
1150
1182
|
topology={topology}
|
|
@@ -1399,6 +1431,14 @@ function OwnershipHeading({
|
|
|
1399
1431
|
function EventsTab({
|
|
1400
1432
|
events,
|
|
1401
1433
|
isLoading,
|
|
1434
|
+
truncated,
|
|
1435
|
+
onLoadOlder,
|
|
1436
|
+
loadingOlder,
|
|
1437
|
+
olderError,
|
|
1438
|
+
scoped,
|
|
1439
|
+
incomplete,
|
|
1440
|
+
error,
|
|
1441
|
+
onRetry,
|
|
1402
1442
|
selectedEventId,
|
|
1403
1443
|
onSelectEvent,
|
|
1404
1444
|
topology,
|
|
@@ -1406,6 +1446,14 @@ function EventsTab({
|
|
|
1406
1446
|
}: {
|
|
1407
1447
|
events: TimelineEvent[]
|
|
1408
1448
|
isLoading: boolean
|
|
1449
|
+
truncated?: boolean
|
|
1450
|
+
onLoadOlder?: () => void
|
|
1451
|
+
loadingOlder?: boolean
|
|
1452
|
+
olderError?: Error | null
|
|
1453
|
+
scoped?: boolean
|
|
1454
|
+
incomplete?: boolean
|
|
1455
|
+
error?: Error | null
|
|
1456
|
+
onRetry?: () => void
|
|
1409
1457
|
selectedEventId: string | null
|
|
1410
1458
|
onSelectEvent: (id: string | null) => void
|
|
1411
1459
|
topology?: Topology
|
|
@@ -1500,8 +1548,66 @@ function EventsTab({
|
|
|
1500
1548
|
)
|
|
1501
1549
|
}
|
|
1502
1550
|
|
|
1551
|
+
if (error && events.length === 0) {
|
|
1552
|
+
return (
|
|
1553
|
+
<div className="p-4">
|
|
1554
|
+
<AlertBanner variant="error" title="Couldn't load this workload's history" message={error.message}>
|
|
1555
|
+
{onRetry && (
|
|
1556
|
+
<button
|
|
1557
|
+
type="button"
|
|
1558
|
+
onClick={onRetry}
|
|
1559
|
+
className="mt-2 rounded border border-theme-border bg-theme-surface px-2 py-1 text-xs text-theme-text-primary transition-colors hover:bg-theme-hover"
|
|
1560
|
+
>
|
|
1561
|
+
Try again
|
|
1562
|
+
</button>
|
|
1563
|
+
)}
|
|
1564
|
+
</AlertBanner>
|
|
1565
|
+
</div>
|
|
1566
|
+
)
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1503
1569
|
return (
|
|
1504
1570
|
<div className="h-full flex flex-col overflow-hidden">
|
|
1571
|
+
{error && (
|
|
1572
|
+
<div className="shrink-0 flex flex-wrap items-center gap-x-2 border-b border-theme-border bg-theme-base px-4 py-1.5 text-xs" role="alert">
|
|
1573
|
+
<span className="text-[var(--color-error)]" title={error.message}>
|
|
1574
|
+
Couldn't load this workload's history. Showing only what loaded.
|
|
1575
|
+
</span>
|
|
1576
|
+
{onRetry && (
|
|
1577
|
+
<button
|
|
1578
|
+
type="button"
|
|
1579
|
+
onClick={onRetry}
|
|
1580
|
+
className="underline decoration-theme-border underline-offset-2 text-theme-text-secondary hover:text-theme-text-primary"
|
|
1581
|
+
>
|
|
1582
|
+
Try again
|
|
1583
|
+
</button>
|
|
1584
|
+
)}
|
|
1585
|
+
</div>
|
|
1586
|
+
)}
|
|
1587
|
+
{(scoped || truncated || incomplete) && (
|
|
1588
|
+
<div className="shrink-0 flex flex-wrap items-center gap-x-2 gap-y-0.5 border-b border-theme-border bg-theme-base px-4 py-1.5 text-xs text-theme-text-secondary" role="note">
|
|
1589
|
+
{scoped && (
|
|
1590
|
+
<span className="text-theme-text-tertiary">
|
|
1591
|
+
This workload, what it runs, and the Services, Ingresses, config and scalers attached to it.
|
|
1592
|
+
</span>
|
|
1593
|
+
)}
|
|
1594
|
+
{incomplete && <span>This workload has more past runs and related resources than Radar follows, so some are left out.</span>}
|
|
1595
|
+
{truncated && <span>Showing the most recent events. Older history exists.</span>}
|
|
1596
|
+
{truncated && olderError && (
|
|
1597
|
+
<span className="text-[var(--color-error)]" title={olderError.message}>Couldn't load older events.</span>
|
|
1598
|
+
)}
|
|
1599
|
+
{truncated && onLoadOlder && (
|
|
1600
|
+
<button
|
|
1601
|
+
type="button"
|
|
1602
|
+
onClick={onLoadOlder}
|
|
1603
|
+
disabled={loadingOlder}
|
|
1604
|
+
className="underline decoration-theme-border underline-offset-2 hover:text-theme-text-primary disabled:opacity-60"
|
|
1605
|
+
>
|
|
1606
|
+
{loadingOlder ? 'Loading…' : olderError ? 'Try again' : 'Load older'}
|
|
1607
|
+
</button>
|
|
1608
|
+
)}
|
|
1609
|
+
</div>
|
|
1610
|
+
)}
|
|
1505
1611
|
{/* Swimlane — the shared TimelineSwimlanes widget (kind chips, top axis with
|
|
1506
1612
|
ticks + Now line, event clustering), flat + compact for a single subject.
|
|
1507
1613
|
Its drawer is suppressed (compact); the list below is the detail surface. */}
|
package/src/types/core.ts
CHANGED
|
@@ -163,6 +163,9 @@ export interface FeatureCapabilities {
|
|
|
163
163
|
yamlReview?: boolean
|
|
164
164
|
yamlSchemas?: boolean
|
|
165
165
|
workloadImages?: boolean
|
|
166
|
+
resourceIssues?: boolean
|
|
167
|
+
podEnvironment?: boolean
|
|
168
|
+
policyResource?: boolean
|
|
166
169
|
}
|
|
167
170
|
|
|
168
171
|
// DeploymentMode is the closed set of topologies Radar can run in.
|
|
@@ -1367,7 +1370,15 @@ export interface AggregatedFlow {
|
|
|
1367
1370
|
l7Protocol?: string // HTTP, gRPC, DNS
|
|
1368
1371
|
requestCount?: number
|
|
1369
1372
|
errorCount?: number
|
|
1373
|
+
/** Per-second rates from a metric-based source, unrounded. requestCount and
|
|
1374
|
+
* errorCount carry the same figures rounded with a floor of one, so their
|
|
1375
|
+
* ratio is meaningless at low rates. */
|
|
1376
|
+
requestRate?: number
|
|
1377
|
+
errorRate?: number
|
|
1370
1378
|
avgLatencyMs?: number
|
|
1379
|
+
/** How many measured responses the latency figures come from (sources that
|
|
1380
|
+
* report individual responses). Weights averages when edges are combined. */
|
|
1381
|
+
latencySamples?: number
|
|
1371
1382
|
latencyP50Ms?: number
|
|
1372
1383
|
latencyP95Ms?: number
|
|
1373
1384
|
latencyP99Ms?: number
|
|
@@ -1434,12 +1445,21 @@ export interface TrafficFlowsResponse {
|
|
|
1434
1445
|
timestamp: string
|
|
1435
1446
|
flows: TrafficFlow[]
|
|
1436
1447
|
aggregated: AggregatedFlow[]
|
|
1448
|
+
/** L7 responses run caller → callee on the server's port, like their request.
|
|
1449
|
+
* Absent from a Radar that sent them server → client. */
|
|
1450
|
+
l7ResponsesCallerOriented?: boolean
|
|
1451
|
+
/** When the flows start being complete: some node returned its full
|
|
1452
|
+
* nodeFlowLimit, so older traffic in the window did not fit. */
|
|
1453
|
+
coveredSince?: string
|
|
1454
|
+
nodeFlowLimit?: number
|
|
1437
1455
|
warning?: string // Non-fatal warning (e.g., query errors)
|
|
1438
1456
|
/** 'transient' (or absent) means the condition may clear on its own and a
|
|
1439
|
-
* retry is worthwhile. 'partial' means the flows are correct but
|
|
1440
|
-
*
|
|
1441
|
-
*
|
|
1442
|
-
|
|
1457
|
+
* retry is worthwhile. 'partial' means the flows are correct but some of
|
|
1458
|
+
* their values are missing or wrong, so show the warning next to them and do
|
|
1459
|
+
* not refetch. 'incomplete' means the fetch worked but could not see
|
|
1460
|
+
* everything — events lost, nodes unreachable — so flows may be missing; show
|
|
1461
|
+
* it, with or without flows, and do not retry at once. */
|
|
1462
|
+
warningKind?: 'transient' | 'partial' | 'incomplete'
|
|
1443
1463
|
}
|
|
1444
1464
|
|
|
1445
1465
|
// Wizard state for traffic setup
|
package/src/types/fetch-error.ts
CHANGED
|
@@ -18,3 +18,36 @@ export function isFetchError(error: unknown): error is FetchErrorShape {
|
|
|
18
18
|
export function isForbiddenError(error: unknown): boolean {
|
|
19
19
|
return isFetchError(error) && error.status === 403
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
// RadarUpgradeRequirement describes a feature the connected Radar is too old to
|
|
23
|
+
// serve. An embedding host (Radar Hub) runs a newer frontend against whatever
|
|
24
|
+
// Radar is installed in the cluster, so "this endpoint does not exist yet" is an
|
|
25
|
+
// expected state that deserves an upgrade prompt, not a red error.
|
|
26
|
+
export interface RadarUpgradeRequirement {
|
|
27
|
+
/** What the user is missing, phrased to start a sentence ("Policy results"). */
|
|
28
|
+
feature: string
|
|
29
|
+
/**
|
|
30
|
+
* First Radar release that serves the feature, e.g. "v1.10.0". Absent when
|
|
31
|
+
* it isn't known yet (a feature still awaiting its release); the note then
|
|
32
|
+
* asks for the latest Radar instead.
|
|
33
|
+
*/
|
|
34
|
+
minimumVersion?: string
|
|
35
|
+
/** The connected Radar's version, when known. */
|
|
36
|
+
currentVersion?: string
|
|
37
|
+
/** Newest Radar release, when known. */
|
|
38
|
+
latestVersion?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Reads the upgrade requirement off an error, duck-typed like FetchErrorShape so
|
|
43
|
+
* the host's error class (web/'s or Radar Hub's) never has to be imported here.
|
|
44
|
+
*/
|
|
45
|
+
export function getRadarUpgradeRequirement(error: unknown): RadarUpgradeRequirement | null {
|
|
46
|
+
if (typeof error !== 'object' || error === null) return null
|
|
47
|
+
const requirement = (error as { radarUpgrade?: unknown }).radarUpgrade
|
|
48
|
+
if (typeof requirement !== 'object' || requirement === null) return null
|
|
49
|
+
const r = requirement as Record<string, unknown>
|
|
50
|
+
if (typeof r.feature !== 'string') return null
|
|
51
|
+
if (r.minimumVersion !== undefined && typeof r.minimumVersion !== 'string') return null
|
|
52
|
+
return requirement as RadarUpgradeRequirement
|
|
53
|
+
}
|
package/src/utils/log-format.ts
CHANGED