@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.
- package/README.md +2 -0
- package/package.json +6 -5
- package/src/App.tsx +65 -17
- package/src/RadarApp.tsx +51 -21
- package/src/api/apiResources.test.ts +19 -1
- package/src/api/apiResources.ts +18 -3
- package/src/api/client.admission.test.ts +34 -0
- package/src/api/client.capacity.test.ts +15 -0
- package/src/api/client.trace.test.ts +9 -1
- package/src/api/client.ts +269 -204
- package/src/api/diagnose.test.ts +102 -1
- package/src/api/diagnose.ts +42 -11
- package/src/api/drain-plan.test.ts +0 -10
- package/src/api/httpErrors.test.ts +95 -0
- package/src/api/httpErrors.ts +101 -0
- package/src/api/policy.ts +33 -5
- package/src/api/radarFeatures.render.test.tsx +190 -0
- package/src/api/radarFeatures.test.ts +133 -0
- package/src/api/radarFeatures.ts +147 -0
- package/src/api/timelineSource.test.ts +2 -10
- package/src/api/timelineSource.ts +3 -2
- package/src/api/traffic.ts +4 -3
- package/src/api/usage-data.fixtures.ts +33 -0
- package/src/api/usage-data.ts +165 -0
- package/src/components/CloudFunnelButton.test.tsx +2 -2
- package/src/components/CloudFunnelButton.tsx +99 -55
- package/src/components/NamespaceSwitcher.test.tsx +47 -0
- package/src/components/NamespaceSwitcher.tsx +17 -1
- package/src/components/SelfManagedStart.tsx +95 -0
- package/src/components/applications/ApplicationsView.tsx +20 -6
- package/src/components/audit/UpgradeReadinessView.test.ts +14 -6
- package/src/components/audit/UpgradeReadinessView.tsx +9 -13
- package/src/components/capacity/CapacityView.test.tsx +13 -4
- package/src/components/capacity/shared.tsx +14 -13
- package/src/components/cloudConnectHandoff.test.ts +1 -1
- package/src/components/cloudConnectHandoff.ts +1 -1
- package/src/components/cost/ApplicationCostTab.tsx +9 -1
- package/src/components/cost/CostConnectionAction.tsx +30 -0
- package/src/components/cost/CostTrendChart.tsx +2 -0
- package/src/components/cost/CostView.tsx +4 -24
- package/src/components/cost/WorkloadCostTab.tsx +7 -1
- package/src/components/cost/source.test.ts +30 -3
- package/src/components/cost/source.ts +37 -2
- package/src/components/curl/ServiceCurlButton.tsx +7 -3
- package/src/components/diagnose/ActivityTurn.tsx +3 -2
- package/src/components/diagnose/ApplyDialog.tsx +4 -2
- package/src/components/dialog-names.test.ts +69 -0
- package/src/components/dock/NodeTerminalTab.tsx +2 -1
- package/src/components/dock/TerminalTab.tsx +2 -1
- package/src/components/dock/TrafficFlowListTab.tsx +2 -2
- package/src/components/execution/BatchExecutionView.tsx +1 -1
- package/src/components/execution/JobSetAdmission.test.tsx +160 -37
- package/src/components/execution/JobSetAdmission.tsx +16 -7
- package/src/components/gitops/GitOpsView.tsx +12 -0
- package/src/components/helm/TrackChartSourceDialog.tsx +5 -3
- package/src/components/home/HomeView.tsx +7 -3
- package/src/components/home/MCPSetupDialog.tsx +37 -37
- package/src/components/home/RadarVersionLine.test.tsx +14 -0
- package/src/components/home/RadarVersionLine.tsx +28 -3
- package/src/components/nav/PrimaryNavRail.test.tsx +10 -1
- package/src/components/nav/PrimaryNavRail.tsx +25 -3
- package/src/components/resource/PVCUsageBar.render.test.tsx +16 -0
- package/src/components/resource/PVCUsageBar.tsx +4 -1
- package/src/components/resource/PrometheusChartsGrid.render.test.tsx +1 -0
- package/src/components/resource/PrometheusChartsGrid.tsx +4 -0
- package/src/components/resources/ImageFilesystemModal.tsx +2 -1
- package/src/components/resources/PodFilePreview.tsx +4 -1
- package/src/components/resources/PodFilesystemModal.tsx +3 -2
- package/src/components/resources/ResourcesView.tsx +2 -1
- package/src/components/resources/renderers/JobAdmissionRenderers.test.tsx +18 -0
- package/src/components/resources/renderers/JobAdmissionRenderers.tsx +18 -0
- package/src/components/resources/renderers/RayJobRenderer.test.tsx +40 -0
- package/src/components/resources/renderers/RayJobRenderer.tsx +45 -0
- package/src/components/rightsizing/RightsizingScanView.tsx +4 -1
- package/src/components/settings/ConnectionHeadersEditor.tsx +109 -0
- package/src/components/settings/FormSaveActions.tsx +76 -0
- package/src/components/settings/IntegrationConnectionForms.tsx +391 -0
- package/src/components/settings/LocalConfigurationDetails.tsx +389 -0
- package/src/components/settings/LocalConnectionSettings.test.tsx +189 -0
- package/src/components/settings/LocalConnectionSettings.tsx +1015 -0
- package/src/components/settings/LocalIntegrationStatus.tsx +50 -0
- package/src/components/settings/PrivacySection.test.tsx +84 -0
- package/src/components/settings/PrivacySection.tsx +140 -0
- package/src/components/settings/PrometheusConfigField.tsx +30 -0
- package/src/components/settings/PrometheusConnectionForm.tsx +295 -0
- package/src/components/settings/SettingsDialog.tsx +393 -571
- package/src/components/settings/controls.tsx +55 -0
- package/src/components/settings/currency-options.test.ts +1 -1
- package/src/components/settings/currency-options.ts +1 -1
- package/src/components/settings/settings-state.test.ts +17 -10
- package/src/components/settings/settings-state.ts +21 -12
- package/src/components/timeline/TimelineList.test.ts +56 -0
- package/src/components/timeline/TimelineList.tsx +7 -1
- package/src/components/traffic/TrafficFlowList.tsx +5 -21
- package/src/components/traffic/TrafficFlowListContext.tsx +6 -4
- package/src/components/traffic/TrafficGraph.tsx +79 -48
- package/src/components/traffic/TrafficView.tsx +61 -37
- package/src/components/traffic/TrafficWizard.tsx +4 -2
- package/src/components/traffic/trafficFilters.test.ts +154 -1
- package/src/components/traffic/trafficFilters.ts +141 -0
- package/src/components/ui/ErrorBoundary.tsx +5 -0
- package/src/components/ui/Omnibar.tsx +17 -1
- package/src/components/ui/UpdateNotification.tsx +70 -21
- package/src/components/ui/command-items.ts +14 -0
- package/src/components/usage-data/UsageDataAsk.test.tsx +41 -0
- package/src/components/usage-data/UsageDataAsk.tsx +106 -0
- package/src/components/usage-data/UsageDataPrompt.test.ts +53 -0
- package/src/components/usage-data/UsageDataPrompt.tsx +148 -0
- package/src/components/whats-new/WhatsNew.test.ts +110 -0
- package/src/components/whats-new/WhatsNew.tsx +423 -0
- package/src/components/whats-new/WhatsNewDialog.test.tsx +229 -0
- package/src/components/whats-new/check-whats-new.test.ts +65 -0
- package/src/components/whats-new/releaseNotes.ts +121 -0
- package/src/components/workload/WorkloadView.tsx +47 -14
- package/src/components/workload/historyPaging.test.ts +72 -0
- package/src/components/workload/historyPaging.ts +147 -0
- package/src/components/workload/useHistoryPaging.test.tsx +81 -0
- package/src/context/ConnectionContext.tsx +2 -1
- package/src/context/RadarUpgrade.tsx +30 -0
- package/src/context/RadarUpgradeHost.tsx +23 -0
- package/src/hooks/usePreviousIntegrationSettings.render.test.tsx +62 -0
- package/src/hooks/usePreviousIntegrationSettings.test.ts +62 -0
- package/src/hooks/usePreviousIntegrationSettings.ts +89 -0
- package/src/index.ts +1 -0
- package/src/k8s-ui-exports.test.ts +73 -0
- package/src/utils/navigation.test.ts +17 -1
- package/src/utils/navigation.ts +16 -0
- 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: '
|
|
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: '
|
|
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
|
-
|
|
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("
|
|
27
|
-
it(
|
|
28
|
-
expect(
|
|
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
|
-
|
|
35
|
-
|
|
34
|
+
integrationDirty: true,
|
|
35
|
+
aiDirtyElsewhere: false,
|
|
36
36
|
hasSaveMessage: false,
|
|
37
37
|
}),
|
|
38
|
-
).toBe(
|
|
38
|
+
).toBe(true);
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
it("offers review from other sections and retains the close guard", () => {
|
|
42
|
-
expect(
|
|
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
|
-
|
|
49
|
-
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
23
|
-
|
|
30
|
+
integrationDirty: boolean
|
|
31
|
+
aiDirtyElsewhere: boolean
|
|
24
32
|
hasSaveMessage: boolean
|
|
25
33
|
}): boolean {
|
|
26
34
|
return (
|
|
27
|
-
input.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
}, [])
|