@skyhook-io/radar-app 1.14.6 → 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 (128) hide show
  1. package/README.md +2 -0
  2. package/package.json +6 -5
  3. package/src/App.tsx +65 -17
  4. package/src/RadarApp.tsx +51 -21
  5. package/src/api/apiResources.test.ts +19 -1
  6. package/src/api/apiResources.ts +18 -3
  7. package/src/api/client.admission.test.ts +34 -0
  8. package/src/api/client.capacity.test.ts +15 -0
  9. package/src/api/client.trace.test.ts +9 -1
  10. package/src/api/client.ts +269 -204
  11. package/src/api/diagnose.test.ts +102 -1
  12. package/src/api/diagnose.ts +42 -11
  13. package/src/api/drain-plan.test.ts +0 -10
  14. package/src/api/httpErrors.test.ts +95 -0
  15. package/src/api/httpErrors.ts +101 -0
  16. package/src/api/policy.ts +33 -5
  17. package/src/api/radarFeatures.render.test.tsx +190 -0
  18. package/src/api/radarFeatures.test.ts +133 -0
  19. package/src/api/radarFeatures.ts +147 -0
  20. package/src/api/timelineSource.test.ts +2 -10
  21. package/src/api/timelineSource.ts +3 -2
  22. package/src/api/traffic.ts +4 -3
  23. package/src/api/usage-data.fixtures.ts +33 -0
  24. package/src/api/usage-data.ts +165 -0
  25. package/src/components/CloudFunnelButton.test.tsx +2 -2
  26. package/src/components/CloudFunnelButton.tsx +99 -55
  27. package/src/components/NamespaceSwitcher.test.tsx +47 -0
  28. package/src/components/NamespaceSwitcher.tsx +17 -1
  29. package/src/components/SelfManagedStart.tsx +95 -0
  30. package/src/components/applications/ApplicationsView.tsx +20 -6
  31. package/src/components/audit/UpgradeReadinessView.test.ts +14 -6
  32. package/src/components/audit/UpgradeReadinessView.tsx +9 -13
  33. package/src/components/capacity/CapacityView.test.tsx +13 -4
  34. package/src/components/capacity/shared.tsx +14 -13
  35. package/src/components/cloudConnectHandoff.test.ts +1 -1
  36. package/src/components/cloudConnectHandoff.ts +1 -1
  37. package/src/components/cost/ApplicationCostTab.tsx +9 -1
  38. package/src/components/cost/CostConnectionAction.tsx +30 -0
  39. package/src/components/cost/CostTrendChart.tsx +2 -0
  40. package/src/components/cost/CostView.tsx +4 -24
  41. package/src/components/cost/WorkloadCostTab.tsx +7 -1
  42. package/src/components/cost/source.test.ts +30 -3
  43. package/src/components/cost/source.ts +37 -2
  44. package/src/components/curl/ServiceCurlButton.tsx +7 -3
  45. package/src/components/diagnose/ActivityTurn.tsx +3 -2
  46. package/src/components/diagnose/ApplyDialog.tsx +4 -2
  47. package/src/components/dialog-names.test.ts +69 -0
  48. package/src/components/dock/NodeTerminalTab.tsx +2 -1
  49. package/src/components/dock/TerminalTab.tsx +2 -1
  50. package/src/components/dock/TrafficFlowListTab.tsx +2 -2
  51. package/src/components/execution/BatchExecutionView.tsx +1 -1
  52. package/src/components/execution/JobSetAdmission.test.tsx +160 -37
  53. package/src/components/execution/JobSetAdmission.tsx +16 -7
  54. package/src/components/gitops/GitOpsView.tsx +12 -0
  55. package/src/components/helm/TrackChartSourceDialog.tsx +5 -3
  56. package/src/components/home/HomeView.tsx +7 -3
  57. package/src/components/home/MCPSetupDialog.tsx +37 -37
  58. package/src/components/home/RadarVersionLine.test.tsx +14 -0
  59. package/src/components/home/RadarVersionLine.tsx +28 -3
  60. package/src/components/nav/PrimaryNavRail.test.tsx +10 -1
  61. package/src/components/nav/PrimaryNavRail.tsx +25 -3
  62. package/src/components/resource/PVCUsageBar.render.test.tsx +16 -0
  63. package/src/components/resource/PVCUsageBar.tsx +4 -1
  64. package/src/components/resource/PrometheusChartsGrid.render.test.tsx +1 -0
  65. package/src/components/resource/PrometheusChartsGrid.tsx +4 -0
  66. package/src/components/resources/ImageFilesystemModal.tsx +2 -1
  67. package/src/components/resources/PodFilePreview.tsx +4 -1
  68. package/src/components/resources/PodFilesystemModal.tsx +3 -2
  69. package/src/components/resources/ResourcesView.tsx +2 -1
  70. package/src/components/resources/renderers/JobAdmissionRenderers.test.tsx +18 -0
  71. package/src/components/resources/renderers/JobAdmissionRenderers.tsx +18 -0
  72. package/src/components/resources/renderers/RayJobRenderer.test.tsx +40 -0
  73. package/src/components/resources/renderers/RayJobRenderer.tsx +45 -0
  74. package/src/components/rightsizing/RightsizingScanView.tsx +4 -1
  75. package/src/components/settings/ConnectionHeadersEditor.tsx +109 -0
  76. package/src/components/settings/FormSaveActions.tsx +76 -0
  77. package/src/components/settings/IntegrationConnectionForms.tsx +391 -0
  78. package/src/components/settings/LocalConfigurationDetails.tsx +389 -0
  79. package/src/components/settings/LocalConnectionSettings.test.tsx +189 -0
  80. package/src/components/settings/LocalConnectionSettings.tsx +1015 -0
  81. package/src/components/settings/LocalIntegrationStatus.tsx +50 -0
  82. package/src/components/settings/PrivacySection.test.tsx +84 -0
  83. package/src/components/settings/PrivacySection.tsx +140 -0
  84. package/src/components/settings/PrometheusConfigField.tsx +30 -0
  85. package/src/components/settings/PrometheusConnectionForm.tsx +295 -0
  86. package/src/components/settings/SettingsDialog.tsx +393 -571
  87. package/src/components/settings/controls.tsx +55 -0
  88. package/src/components/settings/currency-options.test.ts +1 -1
  89. package/src/components/settings/currency-options.ts +1 -1
  90. package/src/components/settings/settings-state.test.ts +17 -10
  91. package/src/components/settings/settings-state.ts +21 -12
  92. package/src/components/timeline/TimelineList.test.ts +56 -0
  93. package/src/components/timeline/TimelineList.tsx +7 -1
  94. package/src/components/traffic/TrafficFlowList.tsx +5 -21
  95. package/src/components/traffic/TrafficFlowListContext.tsx +6 -4
  96. package/src/components/traffic/TrafficGraph.tsx +79 -48
  97. package/src/components/traffic/TrafficView.tsx +61 -37
  98. package/src/components/traffic/TrafficWizard.tsx +4 -2
  99. package/src/components/traffic/trafficFilters.test.ts +154 -1
  100. package/src/components/traffic/trafficFilters.ts +141 -0
  101. package/src/components/ui/ErrorBoundary.tsx +5 -0
  102. package/src/components/ui/Omnibar.tsx +17 -1
  103. package/src/components/ui/UpdateNotification.tsx +70 -21
  104. package/src/components/ui/command-items.ts +14 -0
  105. package/src/components/usage-data/UsageDataAsk.test.tsx +41 -0
  106. package/src/components/usage-data/UsageDataAsk.tsx +106 -0
  107. package/src/components/usage-data/UsageDataPrompt.test.ts +53 -0
  108. package/src/components/usage-data/UsageDataPrompt.tsx +148 -0
  109. package/src/components/whats-new/WhatsNew.test.ts +110 -0
  110. package/src/components/whats-new/WhatsNew.tsx +423 -0
  111. package/src/components/whats-new/WhatsNewDialog.test.tsx +229 -0
  112. package/src/components/whats-new/check-whats-new.test.ts +65 -0
  113. package/src/components/whats-new/releaseNotes.ts +121 -0
  114. package/src/components/workload/WorkloadView.tsx +47 -14
  115. package/src/components/workload/historyPaging.test.ts +72 -0
  116. package/src/components/workload/historyPaging.ts +147 -0
  117. package/src/components/workload/useHistoryPaging.test.tsx +81 -0
  118. package/src/context/ConnectionContext.tsx +2 -1
  119. package/src/context/RadarUpgrade.tsx +30 -0
  120. package/src/context/RadarUpgradeHost.tsx +23 -0
  121. package/src/hooks/usePreviousIntegrationSettings.render.test.tsx +62 -0
  122. package/src/hooks/usePreviousIntegrationSettings.test.ts +62 -0
  123. package/src/hooks/usePreviousIntegrationSettings.ts +89 -0
  124. package/src/index.ts +1 -0
  125. package/src/k8s-ui-exports.test.ts +73 -0
  126. package/src/utils/navigation.test.ts +17 -1
  127. package/src/utils/navigation.ts +16 -0
  128. package/src/utils/version.ts +11 -0
@@ -0,0 +1,55 @@
1
+ import type { ReactNode } from 'react'
2
+ import { clsx } from 'clsx'
3
+
4
+ export function SubHeading({ children }: { children: ReactNode }) {
5
+ return (
6
+ <h4 className="text-xs font-semibold uppercase tracking-wider text-theme-text-tertiary">
7
+ {children}
8
+ </h4>
9
+ )
10
+ }
11
+
12
+ export function ConfigToggle({
13
+ label,
14
+ description,
15
+ value,
16
+ disabled,
17
+ onChange,
18
+ }: {
19
+ label: string
20
+ description?: ReactNode
21
+ value: boolean
22
+ disabled?: boolean
23
+ onChange: (value: boolean) => void
24
+ }) {
25
+ return (
26
+ <label className={clsx('flex items-start justify-between gap-3 py-1', disabled ? 'cursor-not-allowed' : 'cursor-pointer')}>
27
+ <span className="min-w-0">
28
+ <span className={clsx('block text-sm', disabled ? 'text-theme-text-secondary' : 'text-theme-text-primary')}>{label}</span>
29
+ {description && (
30
+ <span className="mt-0.5 block text-xs text-theme-text-tertiary">{description}</span>
31
+ )}
32
+ </span>
33
+ <button
34
+ type="button"
35
+ role="switch"
36
+ aria-checked={value}
37
+ aria-label={label}
38
+ disabled={disabled}
39
+ onClick={() => onChange(!value)}
40
+ className={clsx(
41
+ 'relative w-9 h-5 shrink-0 rounded-full transition-colors',
42
+ value ? 'bg-skyhook-600' : 'bg-theme-elevated border border-theme-border',
43
+ disabled && 'opacity-50'
44
+ )}
45
+ >
46
+ <span
47
+ className={clsx(
48
+ 'absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white transition-transform shadow-sm',
49
+ value && 'translate-x-4'
50
+ )}
51
+ />
52
+ </button>
53
+ </label>
54
+ )
55
+ }
@@ -9,7 +9,7 @@ describe('currency options', () => {
9
9
  it('offers Auto followed by named ISO 4217 currencies', () => {
10
10
  expect(CURRENCY_OPTIONS[0]).toEqual({
11
11
  value: '',
12
- label: 'Auto (detect from OpenCost/Kubecost)',
12
+ label: 'Automatic (from the cost source)',
13
13
  })
14
14
  expect(CURRENCY_OPTIONS).toContainEqual({
15
15
  value: 'EUR',
@@ -19,7 +19,7 @@ const currencyCodes = [
19
19
  ]
20
20
 
21
21
  export const CURRENCY_OPTIONS: SelectMenuOption[] = [
22
- { value: '', label: 'Auto (detect from OpenCost/Kubecost)' },
22
+ { value: '', label: 'Automatic (from the cost source)' },
23
23
  ...currencyCodes
24
24
  .filter((code) => code !== 'XTS' && code !== 'XXX')
25
25
  .map((code) => {
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
2
2
  import {
3
3
  costSourceApplyLabel,
4
4
  prometheusHeadersFromRows,
5
- shouldOfferCostReview,
5
+ pendingSections,
6
6
  shouldShowSettingsFooter,
7
7
  } from "./settings-state";
8
8
 
@@ -23,35 +23,42 @@ describe("Prometheus header edits", () => {
23
23
  });
24
24
  });
25
25
 
26
- describe("Cost settings state", () => {
27
- it("keeps source drafts inline while the Cost section is open", () => {
28
- expect(shouldOfferCostReview(true, "cost")).toBe(false);
26
+ describe("Integration settings state", () => {
27
+ it.each(['prometheus', 'cost', 'argocd'] as const)("offers discard for a %s draft", (section) => {
28
+ expect(pendingSections({ prometheus: false, cost: false, argocd: false, ai: false, [section]: true })).toEqual([section]);
29
29
  expect(
30
30
  shouldShowSettingsFooter({
31
31
  canEditConfig: true,
32
32
  confirmingClose: false,
33
33
  configDirty: false,
34
- costIntegrationDirty: true,
35
- section: "cost",
34
+ integrationDirty: true,
35
+ aiDirtyElsewhere: false,
36
36
  hasSaveMessage: false,
37
37
  }),
38
- ).toBe(false);
38
+ ).toBe(true);
39
39
  });
40
40
 
41
41
  it("offers review from other sections and retains the close guard", () => {
42
- expect(shouldOfferCostReview(true, "overview")).toBe(true);
42
+ expect(pendingSections({ prometheus: true, cost: true, argocd: true, ai: true })).toEqual(['prometheus', 'cost', 'argocd', 'ai']);
43
43
  expect(
44
44
  shouldShowSettingsFooter({
45
45
  canEditConfig: true,
46
46
  confirmingClose: true,
47
47
  configDirty: false,
48
- costIntegrationDirty: true,
49
- section: "cost",
48
+ integrationDirty: true,
49
+ aiDirtyElsewhere: false,
50
50
  hasSaveMessage: false,
51
51
  }),
52
52
  ).toBe(true);
53
53
  });
54
54
 
55
+ it("shows an AI draft to users without owner access, but not their startup or integration drafts", () => {
56
+ const base = { canEditConfig: false, confirmingClose: false, configDirty: true, integrationDirty: true, aiDirtyElsewhere: false, hasSaveMessage: true };
57
+ expect(shouldShowSettingsFooter(base)).toBe(false);
58
+ expect(shouldShowSettingsFooter({ ...base, aiDirtyElsewhere: true })).toBe(true);
59
+ expect(shouldShowSettingsFooter({ ...base, confirmingClose: true })).toBe(true);
60
+ });
61
+
55
62
  it("only claims to test sources that the backend probes", () => {
56
63
  expect(costSourceApplyLabel("auto")).toBe("Test & apply source");
57
64
  expect(costSourceApplyLabel("kubecost")).toBe("Test & apply source");
@@ -7,28 +7,37 @@ export type SettingsSectionId =
7
7
  | 'argocd'
8
8
  | 'ai'
9
9
  | 'advanced'
10
+ | 'privacy'
10
11
 
11
- export function shouldOfferCostReview(
12
- costIntegrationDirty: boolean,
13
- section: SettingsSectionId,
14
- ): boolean {
15
- return costIntegrationDirty && section !== 'cost'
12
+ export type IntegrationSectionId = 'prometheus' | 'cost' | 'argocd'
13
+ // Tabs whose drafts save through their own form, not the startup footer.
14
+ export type PendingSectionId = IntegrationSectionId | 'ai'
15
+
16
+ export const pendingSectionLabels: Record<PendingSectionId, string> = {
17
+ prometheus: 'Metrics', cost: 'Cost', argocd: 'Argo CD', ai: 'AI investigations',
18
+ }
19
+
20
+ export function pendingSections(dirty: Record<PendingSectionId, boolean>): PendingSectionId[] {
21
+ return (['prometheus', 'cost', 'argocd', 'ai'] as const).filter(section => dirty[section])
16
22
  }
17
23
 
24
+ // AI preferences are open to every user, so an AI draft pending in another tab
25
+ // shows the footer even without owner access.
18
26
  export function shouldShowSettingsFooter(input: {
19
27
  canEditConfig: boolean
20
28
  confirmingClose: boolean
21
29
  configDirty: boolean
22
- costIntegrationDirty: boolean
23
- section: SettingsSectionId
30
+ integrationDirty: boolean
31
+ aiDirtyElsewhere: boolean
24
32
  hasSaveMessage: boolean
25
33
  }): boolean {
26
34
  return (
27
- input.canEditConfig &&
28
- (input.confirmingClose ||
29
- input.configDirty ||
30
- shouldOfferCostReview(input.costIntegrationDirty, input.section) ||
31
- input.hasSaveMessage)
35
+ input.confirmingClose ||
36
+ input.aiDirtyElsewhere ||
37
+ (input.canEditConfig &&
38
+ (input.configDirty ||
39
+ input.integrationDirty ||
40
+ input.hasSaveMessage))
32
41
  )
33
42
  }
34
43
 
@@ -0,0 +1,56 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { applyClientFilters } from '../../api/timelineSource'
3
+ import type { TimelineEvent } from '../../types'
4
+ import { listIncludesManaged } from './TimelineList'
5
+
6
+ function ev(over: Partial<TimelineEvent> & { id: string }): TimelineEvent {
7
+ return {
8
+ timestamp: '2024-01-01T00:00:00.000Z',
9
+ source: 'informer',
10
+ kind: 'Deployment',
11
+ namespace: 'ns-a',
12
+ name: over.id,
13
+ eventType: 'update',
14
+ ...over,
15
+ }
16
+ }
17
+
18
+ // Mirrors the host: the source filter runs first, then the UI keeps the selected kinds.
19
+ function listRows(events: TimelineEvent[], kinds: string[]): string[] {
20
+ return applyClientFilters(events, { includeManaged: listIncludesManaged(false, kinds) })
21
+ .filter((e) => kinds.length === 0 || kinds.includes(e.kind))
22
+ .map((e) => e.id)
23
+ }
24
+
25
+ describe('listIncludesManaged', () => {
26
+ it('includes managed rows when app-scoped or when any kind is selected', () => {
27
+ expect(listIncludesManaged(true, [])).toBe(true)
28
+ expect(listIncludesManaged(false, ['Pod'])).toBe(true)
29
+ expect(listIncludesManaged(false, ['Job'])).toBe(true)
30
+ })
31
+
32
+ it('keeps hiding managed rows for the unfiltered list', () => {
33
+ expect(listIncludesManaged(false, [])).toBe(false)
34
+ })
35
+ })
36
+
37
+ describe('timeline list rows', () => {
38
+ const events = [
39
+ ev({ id: 'dep', kind: 'Deployment' }),
40
+ ev({ id: 'pod', kind: 'Pod' }),
41
+ ev({ id: 'job', kind: 'Job' }),
42
+ ev({ id: 'cron-job', kind: 'Job', owner: { kind: 'CronJob', name: 'nightly' } }),
43
+ ]
44
+
45
+ it('lists pod rows for a Pod selection', () => {
46
+ expect(listRows(events, ['Pod'])).toEqual(['pod'])
47
+ })
48
+
49
+ it('lists owned rows of an unmanaged kind for that kind', () => {
50
+ expect(listRows(events, ['Job']).sort()).toEqual(['cron-job', 'job'])
51
+ })
52
+
53
+ it('hides pod and owned rows when no kind is selected', () => {
54
+ expect(listRows(events, []).sort()).toEqual(['dep', 'job'])
55
+ })
56
+ })
@@ -24,6 +24,12 @@ export type { ActivityTypeFilter, ActivityFilterKey }
24
24
  const LIST_FETCH_LIMIT = 2000
25
25
  const APP_SCOPED_FETCH_LIMIT = 10000
26
26
 
27
+ // The source drops managed rows (Pod, ReplicaSet, Event, or any owned row) before
28
+ // the kind filter runs, so an explicit kind selection must keep them or it lists nothing.
29
+ export function listIncludesManaged(appScoped: boolean, kinds: string[]): boolean {
30
+ return appScoped || kinds.length > 0
31
+ }
32
+
27
33
  interface TimelineListProps {
28
34
  namespaces: string[]
29
35
  onViewChange?: (view: 'list' | 'swimlane') => void
@@ -76,7 +82,7 @@ export function TimelineList({ namespaces, onViewChange, currentView, onResource
76
82
  kinds: queryParams.kinds,
77
83
  timeRange: queryParams.timeRange,
78
84
  includeK8sEvents: true,
79
- includeManaged: appScoped,
85
+ includeManaged: listIncludesManaged(appScoped, queryParams.kinds),
80
86
  includeDeleted: showDeleted,
81
87
  limit: fetchLimit,
82
88
  fromMs: selectionWindow?.fromMs,
@@ -2,6 +2,7 @@ import { useState, useMemo } from 'react'
2
2
  import { Virtuoso } from 'react-virtuoso'
3
3
  import type { TrafficFlow, TrafficEndpoint } from '../../types'
4
4
  import { clsx } from 'clsx'
5
+ import { dedupeHTTPPairs } from './trafficFilters'
5
6
  import { ChevronDown, ChevronUp, ShieldCheck } from 'lucide-react'
6
7
  import { SEVERITY_BADGE, SEVERITY_TEXT } from '@skyhook-io/k8s-ui/utils/badge-colors'
7
8
  import { pluralize, StatusDot } from '@skyhook-io/k8s-ui'
@@ -53,9 +54,11 @@ type SortDir = 'asc' | 'desc'
53
54
 
54
55
  interface TrafficFlowListProps {
55
56
  flows: TrafficFlow[]
57
+ /** The server said its L7 responses run caller → callee; see dedupeHTTPPairs. */
58
+ responsesCallerOriented: boolean
56
59
  }
57
60
 
58
- export function TrafficFlowList({ flows }: TrafficFlowListProps) {
61
+ export function TrafficFlowList({ flows, responsesCallerOriented }: TrafficFlowListProps) {
59
62
  const [search] = useFlowSearch()
60
63
  const [sortField, setSortField] = useState<SortField>('time')
61
64
  const [sortDir, setSortDir] = useState<SortDir>('desc')
@@ -70,26 +73,7 @@ export function TrafficFlowList({ flows }: TrafficFlowListProps) {
70
73
  }
71
74
  }
72
75
 
73
- // Deduplicate HTTP REQUEST/RESPONSE pairs: prefer RESPONSE (has status + latency).
74
- // Keep orphan REQUESTs (no matching RESPONSE) as they indicate missing responses.
75
- // Deduplicate HTTP REQUEST/RESPONSE pairs: prefer RESPONSE (has status + latency).
76
- // REQUEST goes client→server, RESPONSE goes server→client (src/dst swapped).
77
- const deduped = useMemo(() => {
78
- // RESPONSE goes server→client, REQUEST goes client→server (src/dst swapped).
79
- // Normalize key: always client|server|method|path
80
- const responseKeys = new Set<string>()
81
- for (const f of flows) {
82
- if (f.l7Protocol === 'HTTP' && f.l7Type === 'RESPONSE') {
83
- responseKeys.add(`${f.destination.name}|${f.source.name}|${f.httpMethod}|${f.httpPath}`)
84
- }
85
- }
86
- return flows.filter(f => {
87
- if (f.l7Protocol === 'HTTP' && f.l7Type === 'REQUEST') {
88
- return !responseKeys.has(`${f.source.name}|${f.destination.name}|${f.httpMethod}|${f.httpPath}`)
89
- }
90
- return true
91
- })
92
- }, [flows])
76
+ const deduped = useMemo(() => dedupeHTTPPairs(flows, responsesCallerOriented), [flows, responsesCallerOriented])
93
77
 
94
78
  const filtered = useMemo(() => {
95
79
  if (!search) return deduped
@@ -4,12 +4,13 @@ import type { TrafficGraphSelection } from './TrafficGraph'
4
4
 
5
5
  interface TrafficFlowListContextValue {
6
6
  flows: TrafficFlow[]
7
+ responsesCallerOriented: boolean
7
8
  graphSelection: TrafficGraphSelection | null
8
9
  clearSelection: () => void
9
10
  }
10
11
 
11
12
  // Module-level store — TrafficView writes, dock tab reads.
12
- let currentValue: TrafficFlowListContextValue = { flows: [], graphSelection: null, clearSelection: () => {} }
13
+ let currentValue: TrafficFlowListContextValue = { flows: [], responsesCallerOriented: false, graphSelection: null, clearSelection: () => {} }
13
14
  const listeners = new Set<() => void>()
14
15
 
15
16
  function setValue(val: TrafficFlowListContextValue) {
@@ -49,17 +50,18 @@ export function useFlowSearch(): [string, (val: string) => void] {
49
50
  // Provider — call this from TrafficView to publish flow data
50
51
  export function TrafficFlowListProvider({
51
52
  flows,
53
+ responsesCallerOriented,
52
54
  graphSelection,
53
55
  clearSelection,
54
56
  children,
55
57
  }: TrafficFlowListContextValue & { children: ReactNode }) {
56
58
  useEffect(() => {
57
- setValue({ flows, graphSelection, clearSelection })
58
- }, [flows, graphSelection, clearSelection])
59
+ setValue({ flows, responsesCallerOriented, graphSelection, clearSelection })
60
+ }, [flows, responsesCallerOriented, graphSelection, clearSelection])
59
61
 
60
62
  useEffect(() => {
61
63
  return () => {
62
- setValue({ flows: [], graphSelection: null, clearSelection: () => {} })
64
+ setValue({ flows: [], responsesCallerOriented: false, graphSelection: null, clearSelection: () => {} })
63
65
  setFlowSearch('')
64
66
  }
65
67
  }, [])