@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.
Files changed (72) hide show
  1. package/package.json +3 -3
  2. package/src/components/audit/AuditFindingsTable.tsx +1 -1
  3. package/src/components/checks/ChecksView.tsx +8 -8
  4. package/src/components/compare/CompareResourcePicker.tsx +4 -3
  5. package/src/components/gitops/GitOpsDetailLayout.tsx +3 -1
  6. package/src/components/gitops/GitOpsHealthSourceNotice.tsx +12 -2
  7. package/src/components/gitops/RollbackDialog.tsx +4 -3
  8. package/src/components/gitops/SyncOptionsDialog.tsx +4 -3
  9. package/src/components/gitops/health-provenance.test.ts +1 -1
  10. package/src/components/gitops/index.ts +1 -0
  11. package/src/components/gitops/insights/ArgoResourceDiff.tsx +1 -0
  12. package/src/components/gitops/insights/GitOpsInsightViews.tsx +4 -1
  13. package/src/components/issues/IssuesView.tsx +2 -2
  14. package/src/components/logs/LogCore.tsx +131 -97
  15. package/src/components/logs/StructuredLogLine.tsx +18 -28
  16. package/src/components/logs/log-palette.ts +4 -0
  17. package/src/components/logs/useLogBuffer.ts +8 -39
  18. package/src/components/logs/useLogSearch.ts +52 -24
  19. package/src/components/namespace-switcher/NamespacePicker.test.tsx +140 -0
  20. package/src/components/namespace-switcher/NamespacePicker.tsx +26 -20
  21. package/src/components/resources/KueueAdmissionSection.test.tsx +183 -6
  22. package/src/components/resources/KueueAdmissionSection.tsx +140 -51
  23. package/src/components/resources/ResourcesView.tsx +4 -0
  24. package/src/components/resources/printer-columns.test.ts +1 -1
  25. package/src/components/resources/renderers/JobRenderer.tsx +5 -1
  26. package/src/components/resources/renderers/JobSetRenderer.tsx +4 -1
  27. package/src/components/resources/renderers/KueueQueueNavigation.test.tsx +2 -2
  28. package/src/components/resources/renderers/KueueWorkloadRenderer.tsx +2 -15
  29. package/src/components/resources/renderers/PodRenderer.test.tsx +15 -0
  30. package/src/components/resources/renderers/PodRenderer.tsx +10 -2
  31. package/src/components/resources/renderers/PolicySection.test.tsx +42 -0
  32. package/src/components/resources/renderers/PolicySection.tsx +10 -1
  33. package/src/components/resources/renderers/RayClusterRenderer.tsx +7 -7
  34. package/src/components/resources/renderers/RayJobRenderer.test.tsx +71 -0
  35. package/src/components/resources/renderers/RayJobRenderer.tsx +134 -0
  36. package/src/components/resources/renderers/ReflectorSection.test.tsx +4 -2
  37. package/src/components/resources/renderers/RolloutRenderer.test.tsx +1 -1
  38. package/src/components/resources/renderers/WorkloadRenderer.tsx +7 -3
  39. package/src/components/resources/renderers/index.ts +2 -0
  40. package/src/components/resources/resource-utils-kueue.ts +14 -0
  41. package/src/components/resources/resource-utils-ray.ts +14 -4
  42. package/src/components/shared/CreateResourceDialog.tsx +6 -2
  43. package/src/components/shared/DrainPlanDialog.test.tsx +10 -0
  44. package/src/components/shared/DrainPlanDialog.tsx +14 -3
  45. package/src/components/shared/ResourceActionsBar.tsx +18 -11
  46. package/src/components/shared/ResourceRendererDispatch.test.tsx +12 -5
  47. package/src/components/shared/ResourceRendererDispatch.tsx +15 -6
  48. package/src/components/shared/SetImageDialog.tsx +4 -2
  49. package/src/components/shared/revision-role-badges.test.ts +1 -1
  50. package/src/components/trace/reachGraphModel.test.ts +1 -0
  51. package/src/components/trace/reachInspector.test.ts +4 -9
  52. package/src/components/ui/ConfirmDialog.tsx +5 -3
  53. package/src/components/ui/DialogPortal.test.tsx +279 -0
  54. package/src/components/ui/DialogPortal.tsx +147 -9
  55. package/src/components/ui/FilterPill.tsx +1 -1
  56. package/src/components/ui/RadarUpgradeNote.click.test.tsx +24 -0
  57. package/src/components/ui/RadarUpgradeNote.test.tsx +66 -0
  58. package/src/components/ui/RadarUpgradeNote.tsx +129 -0
  59. package/src/components/ui/ResourceBar.tsx +12 -6
  60. package/src/components/ui/SelectMenu.tsx +62 -19
  61. package/src/components/ui/YamlReview.tsx +4 -1
  62. package/src/components/ui/drawer-components.test.tsx +14 -1
  63. package/src/components/ui/drawer-components.tsx +50 -40
  64. package/src/components/ui/index.ts +8 -0
  65. package/src/components/workload/WorkloadView.tsx +109 -3
  66. package/src/types/core.ts +24 -4
  67. package/src/types/fetch-error.ts +33 -0
  68. package/src/utils/log-format.ts +1 -1
  69. package/src/utils/log-level.test.ts +269 -0
  70. package/src/utils/log-level.ts +301 -0
  71. package/src/utils/resource-hierarchy.test.ts +69 -0
  72. 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="border-b-subtle pb-4 last:border-0">
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="flex items-center gap-2 w-full text-left mb-2 hover:text-theme-text-primary transition-colors"
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="text-sm font-medium text-theme-text-secondary">{title}</span>
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="space-y-2 max-h-64 overflow-y-auto">
1091
- {visible.map((event, i) => (
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
- /** All timeline events for this resource's namespace */
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 incomplete
1440
- * for a reason retrying cannot change, so show the warning next to them and
1441
- * do not refetch. */
1442
- warningKind?: 'transient' | 'partial'
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
@@ -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
+ }
@@ -3,7 +3,7 @@
3
3
  * Shared between LogsViewer and WorkloadLogsViewer components.
4
4
  */
5
5
 
6
- import type { LogLevel } from '../components/logs/useLogBuffer'
6
+ import type { LogLevel } from './log-level'
7
7
 
8
8
  export type TimestampFormat =
9
9
  | 'time-local'