@skyhook-io/k8s-ui 1.14.12 → 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/compare/CompareResourcePicker.tsx +4 -3
- package/src/components/gitops/GitOpsDetailLayout.tsx +3 -1
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +12 -2
- package/src/components/gitops/RollbackDialog.tsx +4 -3
- package/src/components/gitops/SyncOptionsDialog.tsx +4 -3
- package/src/components/gitops/health-provenance.test.ts +1 -1
- package/src/components/gitops/index.ts +1 -0
- package/src/components/gitops/insights/ArgoResourceDiff.tsx +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/KueueAdmissionSection.test.tsx +183 -6
- package/src/components/resources/KueueAdmissionSection.tsx +140 -51
- package/src/components/resources/ResourcesView.tsx +4 -0
- package/src/components/resources/printer-columns.test.ts +1 -1
- package/src/components/resources/renderers/JobRenderer.tsx +5 -1
- package/src/components/resources/renderers/JobSetRenderer.tsx +4 -1
- package/src/components/resources/renderers/KueueQueueNavigation.test.tsx +2 -2
- package/src/components/resources/renderers/KueueWorkloadRenderer.tsx +2 -15
- 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/resources/renderers/RayClusterRenderer.tsx +7 -7
- package/src/components/resources/renderers/RayJobRenderer.test.tsx +71 -0
- package/src/components/resources/renderers/RayJobRenderer.tsx +134 -0
- package/src/components/resources/renderers/ReflectorSection.test.tsx +4 -2
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +1 -1
- package/src/components/resources/renderers/WorkloadRenderer.tsx +7 -3
- package/src/components/resources/renderers/index.ts +2 -0
- package/src/components/resources/resource-utils-kueue.ts +14 -0
- package/src/components/resources/resource-utils-ray.ts +14 -4
- package/src/components/shared/CreateResourceDialog.tsx +6 -2
- package/src/components/shared/DrainPlanDialog.test.tsx +10 -0
- package/src/components/shared/DrainPlanDialog.tsx +14 -3
- package/src/components/shared/ResourceActionsBar.tsx +18 -11
- package/src/components/shared/ResourceRendererDispatch.test.tsx +12 -5
- package/src/components/shared/ResourceRendererDispatch.tsx +15 -6
- package/src/components/shared/SetImageDialog.tsx +4 -2
- package/src/components/shared/revision-role-badges.test.ts +1 -1
- package/src/components/trace/reachGraphModel.test.ts +1 -0
- package/src/components/trace/reachInspector.test.ts +4 -9
- package/src/components/ui/ConfirmDialog.tsx +5 -3
- package/src/components/ui/DialogPortal.test.tsx +279 -0
- package/src/components/ui/DialogPortal.tsx +147 -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/YamlReview.tsx +4 -1
- package/src/components/ui/drawer-components.test.tsx +14 -1
- package/src/components/ui/drawer-components.tsx +50 -40
- 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
|
@@ -34,6 +34,8 @@ export interface YamlReviewProps {
|
|
|
34
34
|
onClose?: () => void
|
|
35
35
|
onBack: () => void
|
|
36
36
|
onApply: () => void
|
|
37
|
+
/** Set when the review replaces a dialog's own title, which names the dialog. */
|
|
38
|
+
titleId?: string
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
function splitYaml(content: string) {
|
|
@@ -82,6 +84,7 @@ export function YamlReview({
|
|
|
82
84
|
onClose,
|
|
83
85
|
onBack,
|
|
84
86
|
onApply,
|
|
87
|
+
titleId,
|
|
85
88
|
}: YamlReviewProps) {
|
|
86
89
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
87
90
|
const reviewRef = useRef<HTMLDivElement>(null)
|
|
@@ -177,7 +180,7 @@ export function YamlReview({
|
|
|
177
180
|
</button>
|
|
178
181
|
<div className="h-4 w-px bg-theme-border" />
|
|
179
182
|
<div>
|
|
180
|
-
<div className="text-sm font-semibold text-theme-text-primary">
|
|
183
|
+
<div id={titleId} className="text-sm font-semibold text-theme-text-primary">
|
|
181
184
|
Review cluster-admitted changes
|
|
182
185
|
</div>
|
|
183
186
|
<div className="text-xs text-theme-text-tertiary">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import { renderToString } from 'react-dom/server'
|
|
3
|
-
import { ProblemAlerts, OperationalIssuesShownContext, RelatedResourcesSection } from './drawer-components'
|
|
3
|
+
import { EventsSection, ProblemAlerts, OperationalIssuesShownContext, RelatedResourcesSection } from './drawer-components'
|
|
4
4
|
|
|
5
5
|
const problems = [
|
|
6
6
|
{ color: 'red' as const, message: 'Application is Degraded' },
|
|
@@ -49,3 +49,16 @@ describe('RelatedResourcesSection', () => {
|
|
|
49
49
|
expect(html).toContain('<button')
|
|
50
50
|
})
|
|
51
51
|
})
|
|
52
|
+
|
|
53
|
+
describe('Recent Events layout', () => {
|
|
54
|
+
const events = Array.from({ length: 24 }, (_, i) => ({ id: String(i), source: 'k8s_event', eventType: 'Normal', reason: `Reason${i}`, timestamp: '2026-09-27T00:00:00Z' })) as any
|
|
55
|
+
it('keeps drawer scrolling but lets fullscreen events expand in page flow', () => {
|
|
56
|
+
const drawer = renderToString(<EventsSection events={events} />)
|
|
57
|
+
const fullscreen = renderToString(<EventsSection events={events} fullscreen />)
|
|
58
|
+
expect(drawer).toContain('max-h-64')
|
|
59
|
+
expect(drawer).not.toContain('Show 16 more events')
|
|
60
|
+
expect(fullscreen).not.toContain('max-h-64')
|
|
61
|
+
expect(fullscreen).toContain('Show 16 more events')
|
|
62
|
+
expect(fullscreen).toContain('Reason23')
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Disclosure } from './Disclosure'
|
|
1
2
|
import { createContext, useContext, useState } from 'react'
|
|
2
3
|
import { Copy, Check, Tag, AlertTriangle, CheckCircle, ExternalLink, Layers, X, Minus } from 'lucide-react'
|
|
3
4
|
import { clsx } from 'clsx'
|
|
@@ -83,25 +84,26 @@ interface SectionProps {
|
|
|
83
84
|
children: React.ReactNode
|
|
84
85
|
defaultExpanded?: boolean
|
|
85
86
|
contentClassName?: string
|
|
87
|
+
variant?: 'plain' | 'card'
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
export function Section({ title, icon: Icon, children, defaultExpanded = true, contentClassName }: SectionProps) {
|
|
90
|
+
export function Section({ title, icon: Icon, children, defaultExpanded = true, contentClassName, variant = 'plain' }: SectionProps) {
|
|
89
91
|
const [expanded, setExpanded] = useState(defaultExpanded)
|
|
90
92
|
const { panelId, buttonProps } = useDisclosure(expanded)
|
|
91
93
|
|
|
92
94
|
return (
|
|
93
|
-
<div className=
|
|
95
|
+
<div className={variant === 'card' ? 'min-w-0 overflow-hidden rounded-lg border border-theme-border bg-theme-surface' : 'border-b-subtle pb-4 last:border-0'}>
|
|
94
96
|
<button
|
|
95
97
|
{...buttonProps}
|
|
96
98
|
onClick={() => setExpanded(!expanded)}
|
|
97
|
-
className=
|
|
99
|
+
className={clsx('flex items-center gap-2 w-full text-left hover:text-theme-text-primary transition-colors', variant === 'card' ? clsx('border-theme-border bg-theme-elevated px-3 py-2.5', expanded && 'border-b') : 'mb-2')}
|
|
98
100
|
>
|
|
99
101
|
<CollapseChevron open={expanded} className="w-4 h-4" />
|
|
100
102
|
{Icon && <Icon className="w-4 h-4 text-theme-text-secondary" />}
|
|
101
|
-
<span className="
|
|
103
|
+
<span className={clsx("font-medium text-theme-text-secondary", variant === 'card' ? 'text-xs uppercase tracking-wider' : 'text-sm')}>{title}</span>
|
|
102
104
|
</button>
|
|
103
105
|
<Collapse open={expanded} id={panelId}>
|
|
104
|
-
<div className={contentClassName ?? 'pl-6'}>{children}</div>
|
|
106
|
+
<div className={contentClassName ?? (variant === 'card' ? 'p-3' : 'pl-6')}>{children}</div>
|
|
105
107
|
</Collapse>
|
|
106
108
|
</div>
|
|
107
109
|
)
|
|
@@ -293,8 +295,10 @@ export function ConditionsSection({
|
|
|
293
295
|
conditions,
|
|
294
296
|
getConditionTone,
|
|
295
297
|
defaultExpanded,
|
|
298
|
+
variant,
|
|
296
299
|
}: {
|
|
297
300
|
conditions?: any[]
|
|
301
|
+
variant?: 'plain' | 'card'
|
|
298
302
|
defaultExpanded?: boolean
|
|
299
303
|
getConditionTone?: (condition: any) => ConditionTone | undefined
|
|
300
304
|
}) {
|
|
@@ -313,6 +317,7 @@ export function ConditionsSection({
|
|
|
313
317
|
|
|
314
318
|
return (
|
|
315
319
|
<Section
|
|
320
|
+
variant={variant}
|
|
316
321
|
title={`Conditions (${conditions.length})${failCount > 0 ? ` · ${failCount} failing` : ''}`}
|
|
317
322
|
defaultExpanded={defaultExpanded ?? conditions.length <= 6}
|
|
318
323
|
>
|
|
@@ -889,7 +894,6 @@ const RELATIONSHIP_TRUNCATE_LIMIT = 10
|
|
|
889
894
|
export function RelationshipGroup({ label, refs, onNavigate }: RelationshipGroupProps) {
|
|
890
895
|
const [showAll, setShowAll] = useState(false)
|
|
891
896
|
if (!refs || refs.length === 0) return null
|
|
892
|
-
|
|
893
897
|
const truncated = !showAll && refs.length > RELATIONSHIP_TRUNCATE_LIMIT
|
|
894
898
|
const visibleRefs = truncated ? refs.slice(0, RELATIONSHIP_TRUNCATE_LIMIT) : refs
|
|
895
899
|
|
|
@@ -1027,10 +1031,12 @@ interface EventsSectionProps {
|
|
|
1027
1031
|
updatesError?: Error | null
|
|
1028
1032
|
/** Optional hint shown below the event list (e.g. "See Timeline tab for related resources") */
|
|
1029
1033
|
hint?: React.ReactNode
|
|
1034
|
+
fullscreen?: boolean
|
|
1030
1035
|
}
|
|
1031
1036
|
|
|
1032
|
-
export function EventsSection({ events, updates = [], isLoading, eventsError, updatesError, hint }: EventsSectionProps) {
|
|
1037
|
+
export function EventsSection({ events, updates = [], isLoading, eventsError, updatesError, hint, fullscreen = false }: EventsSectionProps) {
|
|
1033
1038
|
const [showUpdates, setShowUpdates] = useState(false)
|
|
1039
|
+
const [showAllEvents, setShowAllEvents] = useState(false)
|
|
1034
1040
|
|
|
1035
1041
|
if (isLoading) {
|
|
1036
1042
|
return (
|
|
@@ -1085,42 +1091,46 @@ export function EventsSection({ events, updates = [], isLoading, eventsError, up
|
|
|
1085
1091
|
)
|
|
1086
1092
|
}
|
|
1087
1093
|
|
|
1094
|
+
const renderEvent = (event: TimelineEvent, i: number) => (
|
|
1095
|
+
<div
|
|
1096
|
+
key={`${event.id}-${i}`}
|
|
1097
|
+
className={clsx(
|
|
1098
|
+
'p-2 rounded text-sm border-l-2',
|
|
1099
|
+
event.eventType === 'Warning' || (isChangeEvent(event) && event.eventType === 'delete')
|
|
1100
|
+
? 'bg-red-500/10 border-red-500'
|
|
1101
|
+
: isK8sEvent(event)
|
|
1102
|
+
? 'bg-blue-500/10 border-blue-500'
|
|
1103
|
+
: 'bg-theme-elevated/30 border-theme-border'
|
|
1104
|
+
)}
|
|
1105
|
+
>
|
|
1106
|
+
<div className="flex items-center justify-between gap-2">
|
|
1107
|
+
<span className="font-medium text-theme-text-primary">
|
|
1108
|
+
{isK8sEvent(event) ? event.reason : event.eventType}
|
|
1109
|
+
</span>
|
|
1110
|
+
<span className="text-xs text-theme-text-tertiary">
|
|
1111
|
+
{formatEventTime(event.timestamp)}
|
|
1112
|
+
</span>
|
|
1113
|
+
</div>
|
|
1114
|
+
{event.message && (
|
|
1115
|
+
<div className="text-xs text-theme-text-secondary mt-1 line-clamp-2">
|
|
1116
|
+
{event.message}
|
|
1117
|
+
</div>
|
|
1118
|
+
)}
|
|
1119
|
+
{isChangeEvent(event) && event.diff?.summary && (
|
|
1120
|
+
<div className="text-xs text-theme-text-secondary mt-1">
|
|
1121
|
+
{event.diff.summary}
|
|
1122
|
+
</div>
|
|
1123
|
+
)}
|
|
1124
|
+
</div>
|
|
1125
|
+
)
|
|
1088
1126
|
return (
|
|
1089
1127
|
<Section title={`Recent Events (${visible.length})`} defaultExpanded>
|
|
1090
|
-
<div className=
|
|
1091
|
-
{visible.
|
|
1092
|
-
<div
|
|
1093
|
-
key={`${event.id}-${i}`}
|
|
1094
|
-
className={clsx(
|
|
1095
|
-
'p-2 rounded text-sm border-l-2',
|
|
1096
|
-
event.eventType === 'Warning' || (isChangeEvent(event) && event.eventType === 'delete')
|
|
1097
|
-
? 'bg-red-500/10 border-red-500'
|
|
1098
|
-
: isK8sEvent(event)
|
|
1099
|
-
? 'bg-blue-500/10 border-blue-500'
|
|
1100
|
-
: 'bg-theme-elevated/30 border-theme-border'
|
|
1101
|
-
)}
|
|
1102
|
-
>
|
|
1103
|
-
<div className="flex items-center justify-between gap-2">
|
|
1104
|
-
<span className="font-medium text-theme-text-primary">
|
|
1105
|
-
{isK8sEvent(event) ? event.reason : event.eventType}
|
|
1106
|
-
</span>
|
|
1107
|
-
<span className="text-xs text-theme-text-tertiary">
|
|
1108
|
-
{formatEventTime(event.timestamp)}
|
|
1109
|
-
</span>
|
|
1110
|
-
</div>
|
|
1111
|
-
{event.message && (
|
|
1112
|
-
<div className="text-xs text-theme-text-secondary mt-1 line-clamp-2">
|
|
1113
|
-
{event.message}
|
|
1114
|
-
</div>
|
|
1115
|
-
)}
|
|
1116
|
-
{isChangeEvent(event) && event.diff?.summary && (
|
|
1117
|
-
<div className="text-xs text-theme-text-secondary mt-1">
|
|
1118
|
-
{event.diff.summary}
|
|
1119
|
-
</div>
|
|
1120
|
-
)}
|
|
1121
|
-
</div>
|
|
1122
|
-
))}
|
|
1128
|
+
<div tabIndex={fullscreen ? undefined : 0} className={clsx('space-y-2', !fullscreen && 'max-h-64 overflow-y-auto')}>
|
|
1129
|
+
{(fullscreen ? visible.slice(0, 8) : visible).map(renderEvent)}
|
|
1123
1130
|
</div>
|
|
1131
|
+
{fullscreen && visible.length > 8 && <Disclosure open={showAllEvents} onOpenChange={setShowAllEvents} summary={showAllEvents ? 'Show fewer events' : `Show ${visible.length - 8} more events`} className="mt-2" summaryClassName="text-xs text-accent-text">
|
|
1132
|
+
<div className="mt-2 space-y-2">{visible.slice(8).map(renderEvent)}</div>
|
|
1133
|
+
</Disclosure>}
|
|
1124
1134
|
{errors}
|
|
1125
1135
|
{toggle && <div className="mt-2">{toggle}</div>}
|
|
1126
1136
|
{hint && <div className="mt-2">{hint}</div>}
|
|
@@ -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