@skyhook-io/radar-app 1.8.6 → 1.8.7
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 +7 -1
- package/package.json +11 -9
- package/src/App.tsx +221 -181
- package/src/RadarApp.tsx +91 -20
- package/src/api/client.delta.test.ts +89 -0
- package/src/api/client.deltaSync.test.ts +216 -0
- package/src/api/client.ts +262 -34
- package/src/api/diagnose.ts +288 -0
- package/src/api/timelineSource.test.ts +217 -0
- package/src/api/timelineSource.ts +580 -0
- package/src/components/ConnectionErrorView.tsx +174 -70
- package/src/components/ContextSwitcher.tsx +13 -5
- package/src/components/applications/ApplicationsView.tsx +327 -26
- package/src/components/audit/AuditSettingsDialog.tsx +2 -2
- package/src/components/curl/ServiceCurlButton.tsx +2 -2
- package/src/components/diagnose/AISettings.tsx +121 -0
- package/src/components/diagnose/DiagnoseContext.tsx +491 -0
- package/src/components/diagnose/DiagnoseSurface.tsx +385 -0
- package/src/components/diagnose/Home.tsx +163 -0
- package/src/components/diagnose/InvestigationView.tsx +604 -0
- package/src/components/diagnose/LocalDiagnoseAction.tsx +150 -0
- package/src/components/diagnose/launch.ts +65 -0
- package/src/components/diagnose/parts.tsx +1689 -0
- package/src/components/dock/BottomDock.tsx +2 -3
- package/src/components/dock/WorkloadLogsTab.tsx +21 -5
- package/src/components/execution/BatchExecutionView.test.ts +170 -0
- package/src/components/execution/BatchExecutionView.tsx +1329 -0
- package/src/components/execution/batch-run-actions.test.ts +48 -0
- package/src/components/execution/batch-run-actions.ts +24 -0
- package/src/components/execution/batch-timeline.test.ts +57 -0
- package/src/components/execution/batch-timeline.ts +46 -0
- package/src/components/execution/execution-definition.test.ts +208 -0
- package/src/components/execution/execution-definition.ts +245 -0
- package/src/components/helm/ChartBrowser.tsx +2 -3
- package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +376 -43
- package/src/components/helm/HelmView.tsx +2 -3
- package/src/components/helm/InstallWizard.tsx +3 -5
- package/src/components/helm/TrackChartSourceDialog.tsx +48 -4
- package/src/components/helm/ValuesDiffPreview.tsx +15 -4
- package/src/components/home/HomeView.tsx +17 -15
- package/src/components/home/MCPSetupDialog.tsx +1 -1
- package/src/components/home/mcpToolCatalog.ts +2 -2
- package/src/components/issues/IssuesPane.tsx +9 -1
- package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
- package/src/components/portforward/PortForwardButton.tsx +2 -2
- package/src/components/portforward/PortForwardManager.tsx +19 -14
- package/src/components/resource/PrometheusChartsGrid.tsx +6 -80
- package/src/components/resource/RightsizingStrip.tsx +0 -3
- package/src/components/resources/ImageFilesystemModal.tsx +2 -2
- package/src/components/resources/PodFilesystemModal.tsx +2 -3
- package/src/components/resources/ResourceDetailDrawer.tsx +2 -0
- package/src/components/resources/ResourcesView.tsx +13 -4
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
- package/src/components/resources/renderers/index.ts +1 -0
- package/src/components/settings/SettingsDialog.tsx +80 -23
- package/src/components/shared/LargeClusterNamespacePicker.tsx +2 -2
- package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
- package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
- package/src/components/timeline/TimelineList.tsx +51 -10
- package/src/components/timeline/TimelineView.tsx +701 -15
- package/src/components/timeline/TimelineView.urlparams.test.ts +294 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +2 -2
- package/src/components/traffic/TrafficView.tsx +8 -4
- package/src/components/ui/CommandPalette.tsx +2 -2
- package/src/components/workload/WorkloadView.tsx +319 -35
- package/src/context/ConnectionContext.tsx +109 -11
- package/src/context/DiagnoseCustomization.tsx +42 -0
- package/src/context/TimelineSource.tsx +50 -0
- package/src/hooks/useClusterLoadState.ts +73 -0
- package/src/hooks/useEventSource.ts +6 -0
- package/src/index.css +157 -0
- package/src/index.ts +12 -0
- package/src/types/clusterLoadState.ts +33 -0
- package/src/utils/navigation.ts +10 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { ActivityFilterKey, TimelineGrouping, TimelineSort } from '@skyhook-io/k8s-ui'
|
|
3
|
+
import {
|
|
4
|
+
parseTimeMode,
|
|
5
|
+
writeTimelineParams,
|
|
6
|
+
onlyHighFreqDiffer,
|
|
7
|
+
timeModeEqual,
|
|
8
|
+
type TimelineMode,
|
|
9
|
+
type PersistedTimelineState,
|
|
10
|
+
type TimelineViewMode,
|
|
11
|
+
} from './TimelineView'
|
|
12
|
+
|
|
13
|
+
// These mirror the module-private defaults writeTimelineParams omits at (and
|
|
14
|
+
// parseTimeMode falls back to). Kept in sync deliberately: the round-trip tests
|
|
15
|
+
// below fail loudly if a module default drifts from what is asserted here.
|
|
16
|
+
const DEFAULT_LIVE_WIDTH_MS = 60 * 60 * 1000
|
|
17
|
+
const DAY_MS = 24 * 60 * 60 * 1000
|
|
18
|
+
const DEFAULT_VIEW: TimelineViewMode = 'swimlane'
|
|
19
|
+
const DEFAULT_GROUPING: TimelineGrouping = 'app'
|
|
20
|
+
const DEFAULT_SORT: TimelineSort = 'importance'
|
|
21
|
+
const ACTIVITY_KEYS: readonly ActivityFilterKey[] = ['changes', 'k8s_events', 'warnings', 'unhealthy']
|
|
22
|
+
const GROUPINGS: readonly TimelineGrouping[] = ['app', 'owner', 'flat']
|
|
23
|
+
const SORTS: readonly TimelineSort[] = ['importance', 'recent', 'name']
|
|
24
|
+
|
|
25
|
+
const live = (widthMs: number, all?: boolean): TimelineMode => (all ? { kind: 'live', widthMs, all } : { kind: 'live', widthMs })
|
|
26
|
+
const frozen = (fromMs: number, toMs: number): TimelineMode => ({ kind: 'frozen', fromMs, toMs })
|
|
27
|
+
const sp = (obj: Record<string, string>) => new URLSearchParams(obj)
|
|
28
|
+
|
|
29
|
+
// The inverse of writeTimelineParams, reconstructed from the component's own
|
|
30
|
+
// URL->state read (TimelineView's sync effect). Uses the exported parseTimeMode
|
|
31
|
+
// for the time mode; the remaining fields parse inline the same way the
|
|
32
|
+
// component does, so parse(write(state)) exercises the real deep-link contract.
|
|
33
|
+
const parseEnum = <T extends string>(value: string | null, allowed: readonly T[], fallback: T): T =>
|
|
34
|
+
value != null && (allowed as readonly string[]).includes(value) ? (value as T) : fallback
|
|
35
|
+
|
|
36
|
+
function parseState(
|
|
37
|
+
params: URLSearchParams,
|
|
38
|
+
opts: { isRetained: boolean; requiresNamespaceFilter: boolean; maxRangeDays?: number; hasPins: boolean },
|
|
39
|
+
): PersistedTimelineState {
|
|
40
|
+
const view = params.get('view')
|
|
41
|
+
const viewMode: TimelineViewMode = opts.requiresNamespaceFilter
|
|
42
|
+
? 'list'
|
|
43
|
+
: view === 'list' || view === 'swimlane'
|
|
44
|
+
? view
|
|
45
|
+
: DEFAULT_VIEW
|
|
46
|
+
const activityRaw = params.get('activity')
|
|
47
|
+
const activityFilter =
|
|
48
|
+
activityRaw == null
|
|
49
|
+
? []
|
|
50
|
+
: activityRaw
|
|
51
|
+
.split(',')
|
|
52
|
+
.map((s) => s.trim())
|
|
53
|
+
.filter((s): s is ActivityFilterKey => (ACTIVITY_KEYS as readonly string[]).includes(s))
|
|
54
|
+
const kindsRaw = params.get('kinds')
|
|
55
|
+
const kindFilter = kindsRaw ? kindsRaw.split(',').map((s) => s.trim()).filter(Boolean) : []
|
|
56
|
+
return {
|
|
57
|
+
viewMode,
|
|
58
|
+
mode: parseTimeMode(params, opts.isRetained, opts.maxRangeDays),
|
|
59
|
+
showDeleted: params.get('deleted') !== '0',
|
|
60
|
+
pinnedOnly: params.get('pinnedOnly') === '1' && opts.hasPins,
|
|
61
|
+
search: params.get('q') ?? '',
|
|
62
|
+
activityFilter,
|
|
63
|
+
kindFilter,
|
|
64
|
+
grouping: parseEnum(params.get('grouping'), GROUPINGS, DEFAULT_GROUPING),
|
|
65
|
+
sort: parseEnum(params.get('sort'), SORTS, DEFAULT_SORT),
|
|
66
|
+
selectedEventId: params.get('event'),
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const defaultState: PersistedTimelineState = {
|
|
71
|
+
viewMode: DEFAULT_VIEW,
|
|
72
|
+
mode: live(DEFAULT_LIVE_WIDTH_MS),
|
|
73
|
+
showDeleted: true,
|
|
74
|
+
pinnedOnly: false,
|
|
75
|
+
search: '',
|
|
76
|
+
activityFilter: [],
|
|
77
|
+
kindFilter: [],
|
|
78
|
+
grouping: DEFAULT_GROUPING,
|
|
79
|
+
sort: DEFAULT_SORT,
|
|
80
|
+
selectedEventId: null,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const sortedEntries = (params: URLSearchParams) => [...params.entries()].sort(([a], [b]) => a.localeCompare(b))
|
|
84
|
+
|
|
85
|
+
describe('parseTimeMode', () => {
|
|
86
|
+
it('round-trips the live-window presets encoded in ?window', () => {
|
|
87
|
+
expect(parseTimeMode(sp({ window: 'all' }), true)).toEqual(live(DEFAULT_LIVE_WIDTH_MS, true))
|
|
88
|
+
expect(parseTimeMode(sp({ window: '1800000' }), true)).toEqual(live(1_800_000))
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('parses an explicit from/to into a frozen window', () => {
|
|
92
|
+
const from = 1_700_000_000_000
|
|
93
|
+
const to = from + 2 * DAY_MS
|
|
94
|
+
expect(parseTimeMode(sp({ from: String(from), to: String(to) }), true)).toEqual(frozen(from, to))
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('rejects from >= to and falls back to the default live window', () => {
|
|
98
|
+
const t = 1_700_000_000_000
|
|
99
|
+
expect(parseTimeMode(sp({ from: String(t), to: String(t) }), true)).toEqual(live(DEFAULT_LIVE_WIDTH_MS))
|
|
100
|
+
expect(parseTimeMode(sp({ from: String(t + 1000), to: String(t) }), true)).toEqual(live(DEFAULT_LIVE_WIDTH_MS))
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('caps an out-of-retention from to maxRangeDays before to', () => {
|
|
104
|
+
const to = 10 * DAY_MS
|
|
105
|
+
expect(parseTimeMode(sp({ from: '1', to: String(to) }), true, 7)).toEqual(frozen(to - 7 * DAY_MS, to))
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('leaves from uncapped when already within maxRangeDays', () => {
|
|
109
|
+
const to = 10 * DAY_MS
|
|
110
|
+
const from = to - 3 * DAY_MS
|
|
111
|
+
expect(parseTimeMode(sp({ from: String(from), to: String(to) }), true, 7)).toEqual(frozen(from, to))
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('falls back to the default mode on garbage from/to/window', () => {
|
|
115
|
+
expect(parseTimeMode(sp({ from: 'abc', to: 'def' }), true)).toEqual(live(DEFAULT_LIVE_WIDTH_MS))
|
|
116
|
+
expect(parseTimeMode(sp({ from: '1.5', to: '9.5' }), true)).toEqual(live(DEFAULT_LIVE_WIDTH_MS))
|
|
117
|
+
expect(parseTimeMode(sp({ window: 'abc' }), true)).toEqual(live(DEFAULT_LIVE_WIDTH_MS))
|
|
118
|
+
expect(parseTimeMode(sp({ window: '0' }), true)).toEqual(live(DEFAULT_LIVE_WIDTH_MS))
|
|
119
|
+
expect(parseTimeMode(sp({ window: '-5' }), true)).toEqual(live(DEFAULT_LIVE_WIDTH_MS))
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('ignores from/to/window entirely when the source is not retained', () => {
|
|
123
|
+
const from = 1_700_000_000_000
|
|
124
|
+
expect(
|
|
125
|
+
parseTimeMode(sp({ from: String(from), to: String(from + DAY_MS), window: 'all' }), false),
|
|
126
|
+
).toEqual(live(DEFAULT_LIVE_WIDTH_MS))
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
describe('writeTimelineParams', () => {
|
|
131
|
+
const retainedOpts = { isRetained: true, requiresNamespaceFilter: false as boolean | undefined }
|
|
132
|
+
|
|
133
|
+
it('writes no params for the pristine default state', () => {
|
|
134
|
+
const written = writeTimelineParams(new URLSearchParams(), defaultState, retainedOpts)
|
|
135
|
+
expect(written.toString()).toBe('')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('writes exactly the expected keys for a fully non-default state', () => {
|
|
139
|
+
const state: PersistedTimelineState = {
|
|
140
|
+
viewMode: 'list',
|
|
141
|
+
mode: live(6 * 60 * 60 * 1000),
|
|
142
|
+
showDeleted: false,
|
|
143
|
+
pinnedOnly: true,
|
|
144
|
+
search: 'nginx',
|
|
145
|
+
activityFilter: ['warnings', 'unhealthy'],
|
|
146
|
+
kindFilter: ['Pod', 'Deployment'],
|
|
147
|
+
grouping: 'owner',
|
|
148
|
+
sort: 'recent',
|
|
149
|
+
selectedEventId: 'evt-42',
|
|
150
|
+
}
|
|
151
|
+
const written = writeTimelineParams(new URLSearchParams(), state, retainedOpts)
|
|
152
|
+
expect(sortedEntries(written)).toEqual([
|
|
153
|
+
['activity', 'warnings,unhealthy'],
|
|
154
|
+
['deleted', '0'],
|
|
155
|
+
['event', 'evt-42'],
|
|
156
|
+
['grouping', 'owner'],
|
|
157
|
+
['kinds', 'Pod,Deployment'],
|
|
158
|
+
['pinnedOnly', '1'],
|
|
159
|
+
['q', 'nginx'],
|
|
160
|
+
['sort', 'recent'],
|
|
161
|
+
['view', 'list'],
|
|
162
|
+
['window', '21600000'],
|
|
163
|
+
])
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('encodes a frozen window as from/to and omits window', () => {
|
|
167
|
+
const from = 1_700_000_000_000
|
|
168
|
+
const to = from + 2 * DAY_MS
|
|
169
|
+
const written = writeTimelineParams(new URLSearchParams(), { ...defaultState, mode: frozen(from, to) }, retainedOpts)
|
|
170
|
+
expect(written.get('from')).toBe(String(from))
|
|
171
|
+
expect(written.get('to')).toBe(String(to))
|
|
172
|
+
expect(written.has('window')).toBe(false)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('preserves foreign params and strips the legacy filter seed', () => {
|
|
176
|
+
const base = new URLSearchParams({ tab: 'topology', filter: 'warnings' })
|
|
177
|
+
const written = writeTimelineParams(base, defaultState, retainedOpts)
|
|
178
|
+
expect(written.get('tab')).toBe('topology')
|
|
179
|
+
expect(written.has('filter')).toBe(false)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('does not persist the list view a large cluster forces', () => {
|
|
183
|
+
const written = writeTimelineParams(new URLSearchParams(), { ...defaultState, viewMode: 'list' }, {
|
|
184
|
+
isRetained: true,
|
|
185
|
+
requiresNamespaceFilter: true,
|
|
186
|
+
})
|
|
187
|
+
expect(written.has('view')).toBe(false)
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
describe('parse(write(state)) round-trip', () => {
|
|
192
|
+
const opts = { isRetained: true, requiresNamespaceFilter: false, maxRangeDays: undefined, hasPins: true }
|
|
193
|
+
const roundTrip = (state: PersistedTimelineState) => {
|
|
194
|
+
const written = writeTimelineParams(new URLSearchParams(), state, {
|
|
195
|
+
isRetained: opts.isRetained,
|
|
196
|
+
requiresNamespaceFilter: opts.requiresNamespaceFilter,
|
|
197
|
+
})
|
|
198
|
+
return parseState(new URLSearchParams(written.toString()), opts)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
it('round-trips the pristine default state', () => {
|
|
202
|
+
expect(roundTrip(defaultState)).toEqual(defaultState)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('round-trips a live preset with every control set', () => {
|
|
206
|
+
const state: PersistedTimelineState = {
|
|
207
|
+
viewMode: 'list',
|
|
208
|
+
mode: live(6 * 60 * 60 * 1000),
|
|
209
|
+
showDeleted: false,
|
|
210
|
+
pinnedOnly: true,
|
|
211
|
+
search: 'nginx',
|
|
212
|
+
activityFilter: ['warnings', 'unhealthy'],
|
|
213
|
+
kindFilter: ['Pod', 'Deployment'],
|
|
214
|
+
grouping: 'owner',
|
|
215
|
+
sort: 'recent',
|
|
216
|
+
selectedEventId: 'evt-42',
|
|
217
|
+
}
|
|
218
|
+
expect(roundTrip(state)).toEqual(state)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('round-trips a frozen window with a selected event', () => {
|
|
222
|
+
const from = 1_700_000_000_000
|
|
223
|
+
const state: PersistedTimelineState = {
|
|
224
|
+
...defaultState,
|
|
225
|
+
mode: frozen(from, from + 2 * DAY_MS),
|
|
226
|
+
selectedEventId: 'evt-frozen',
|
|
227
|
+
}
|
|
228
|
+
expect(roundTrip(state)).toEqual(state)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('round-trips a whole-span live window and a search with a space', () => {
|
|
232
|
+
const state: PersistedTimelineState = {
|
|
233
|
+
...defaultState,
|
|
234
|
+
mode: live(DEFAULT_LIVE_WIDTH_MS, true),
|
|
235
|
+
search: 'foo bar',
|
|
236
|
+
activityFilter: ['changes'],
|
|
237
|
+
sort: 'name',
|
|
238
|
+
}
|
|
239
|
+
expect(roundTrip(state)).toEqual(state)
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
describe('timeModeEqual', () => {
|
|
244
|
+
it('is reflexive for live and frozen modes', () => {
|
|
245
|
+
expect(timeModeEqual(live(DEFAULT_LIVE_WIDTH_MS), live(DEFAULT_LIVE_WIDTH_MS))).toBe(true)
|
|
246
|
+
expect(timeModeEqual(frozen(1, 2), frozen(1, 2))).toBe(true)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('treats the whole-span all flag as significant', () => {
|
|
250
|
+
expect(timeModeEqual(live(DEFAULT_LIVE_WIDTH_MS), live(DEFAULT_LIVE_WIDTH_MS, true))).toBe(false)
|
|
251
|
+
expect(timeModeEqual(live(DEFAULT_LIVE_WIDTH_MS, true), live(DEFAULT_LIVE_WIDTH_MS, true))).toBe(true)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('is false across the live/frozen boundary', () => {
|
|
255
|
+
expect(timeModeEqual(live(DEFAULT_LIVE_WIDTH_MS), frozen(0, DEFAULT_LIVE_WIDTH_MS))).toBe(false)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('distinguishes different widths and different frozen windows', () => {
|
|
259
|
+
expect(timeModeEqual(live(DEFAULT_LIVE_WIDTH_MS), live(2 * DEFAULT_LIVE_WIDTH_MS))).toBe(false)
|
|
260
|
+
expect(timeModeEqual(frozen(1, 2), frozen(1, 3))).toBe(false)
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
describe('onlyHighFreqDiffer (history replace vs push)', () => {
|
|
265
|
+
it('replaces when only a high-frequency key differs (window pan/zoom, search, drawer event)', () => {
|
|
266
|
+
expect(onlyHighFreqDiffer('window=3600000', 'window=7200000')).toBe(true)
|
|
267
|
+
expect(onlyHighFreqDiffer('q=a', 'q=b')).toBe(true)
|
|
268
|
+
expect(onlyHighFreqDiffer('event=evt-1', 'event=evt-2')).toBe(true)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('replaces on a live<->frozen transition — every time-window key is high-frequency', () => {
|
|
272
|
+
expect(onlyHighFreqDiffer('window=7200000', 'from=1&to=2')).toBe(true)
|
|
273
|
+
expect(onlyHighFreqDiffer('', 'from=1&to=2')).toBe(true)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('pushes when a discrete control changes', () => {
|
|
277
|
+
expect(onlyHighFreqDiffer('view=list', 'view=swimlane')).toBe(false)
|
|
278
|
+
expect(onlyHighFreqDiffer('', 'grouping=owner')).toBe(false)
|
|
279
|
+
expect(onlyHighFreqDiffer('', 'sort=recent')).toBe(false)
|
|
280
|
+
expect(onlyHighFreqDiffer('', 'activity=warnings')).toBe(false)
|
|
281
|
+
expect(onlyHighFreqDiffer('', 'kinds=Pod')).toBe(false)
|
|
282
|
+
expect(onlyHighFreqDiffer('', 'deleted=0')).toBe(false)
|
|
283
|
+
expect(onlyHighFreqDiffer('', 'pinnedOnly=1')).toBe(false)
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('pushes when a diff mixes a high-frequency and a discrete key', () => {
|
|
287
|
+
expect(onlyHighFreqDiffer('', 'window=7200000&grouping=owner')).toBe(false)
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('is not replace-worthy when nothing differs', () => {
|
|
291
|
+
expect(onlyHighFreqDiffer('window=3600000', 'window=3600000')).toBe(false)
|
|
292
|
+
expect(onlyHighFreqDiffer('', '')).toBe(false)
|
|
293
|
+
})
|
|
294
|
+
})
|
|
@@ -16,6 +16,7 @@ import { SEVERITY_BADGE } from '@skyhook-io/k8s-ui/utils/badge-colors'
|
|
|
16
16
|
import type { AddonMode } from './TrafficView'
|
|
17
17
|
import { getNamespaceColor } from '../../utils/traffic-colors'
|
|
18
18
|
import { Tooltip } from '../ui/Tooltip'
|
|
19
|
+
import { Input } from '@skyhook-io/k8s-ui'
|
|
19
20
|
|
|
20
21
|
// Connection threshold options
|
|
21
22
|
const CONNECTION_THRESHOLDS = [
|
|
@@ -398,8 +399,7 @@ export const TrafficFilterSidebar = memo(function TrafficFilterSidebar({
|
|
|
398
399
|
{(l7Protocol === 'all' || l7Protocol === 'DNS') && (
|
|
399
400
|
<div>
|
|
400
401
|
<div className="text-[10px] text-theme-text-tertiary mb-1">DNS Query</div>
|
|
401
|
-
<
|
|
402
|
-
type="text"
|
|
402
|
+
<Input
|
|
403
403
|
value={dnsPattern}
|
|
404
404
|
onChange={(e) => setDnsPattern(e.target.value)}
|
|
405
405
|
placeholder="e.g. example.com"
|
|
@@ -958,7 +958,11 @@ export function TrafficView({ namespaces }: TrafficViewProps) {
|
|
|
958
958
|
}
|
|
959
959
|
}, [sourcesData, sourcesLoading])
|
|
960
960
|
|
|
961
|
-
// Shared connection handler — used by auto-connect and retry buttons
|
|
961
|
+
// Shared connection handler — used by auto-connect and retry buttons.
|
|
962
|
+
// Deliberately does NOT reset hasAutoConnectedRef on failure: that ref gates
|
|
963
|
+
// the auto-connect effect, and re-arming it would re-fire auto-connect on
|
|
964
|
+
// every failed attempt (unbounded loop). Retries come from the explicit
|
|
965
|
+
// Retry button, which calls handleConnect directly.
|
|
962
966
|
const handleConnect = useCallback(() => {
|
|
963
967
|
setIsConnecting(true)
|
|
964
968
|
setConnectionError(null)
|
|
@@ -969,18 +973,18 @@ export function TrafficView({ namespaces }: TrafficViewProps) {
|
|
|
969
973
|
setIsConnecting(false)
|
|
970
974
|
if (!data.connected && data.error) {
|
|
971
975
|
setConnectionError(data.error)
|
|
972
|
-
hasAutoConnectedRef.current = false // allow retry
|
|
973
976
|
}
|
|
974
977
|
},
|
|
975
978
|
onError: (error) => {
|
|
976
979
|
setIsConnecting(false)
|
|
977
980
|
setConnectionError(error.message)
|
|
978
|
-
hasAutoConnectedRef.current = false // allow retry
|
|
979
981
|
},
|
|
980
982
|
})
|
|
981
983
|
}, [connectMutation, queryClient])
|
|
982
984
|
|
|
983
|
-
// Auto-connect when source is detected
|
|
985
|
+
// Auto-connect once when a source is first detected. Strictly one-shot per
|
|
986
|
+
// mount / cluster (the ref resets on cluster change); failures surface a
|
|
987
|
+
// manual Retry rather than re-arming this effect.
|
|
984
988
|
useEffect(() => {
|
|
985
989
|
if (wizardState === 'ready' && !hasAutoConnectedRef.current && !isConnecting) {
|
|
986
990
|
hasAutoConnectedRef.current = true
|
|
@@ -3,6 +3,7 @@ import { TRANSITION_BACKDROP, TRANSITION_PANEL } from '../../utils/animation'
|
|
|
3
3
|
import { Search, X, ChevronRight } from 'lucide-react'
|
|
4
4
|
import { clsx } from 'clsx'
|
|
5
5
|
import { useCommandItems, bestScore, type CommandItem, type CommandItemCallbacks } from './command-items'
|
|
6
|
+
import { Input } from '@skyhook-io/k8s-ui'
|
|
6
7
|
|
|
7
8
|
interface CommandPaletteProps extends CommandItemCallbacks {
|
|
8
9
|
onClose: () => void
|
|
@@ -141,9 +142,8 @@ export function CommandPalette({ onClose, isOpen = true, ...callbacks }: Command
|
|
|
141
142
|
{/* Search input */}
|
|
142
143
|
<div className="flex items-center gap-3 px-4 py-3 border-b border-theme-border">
|
|
143
144
|
<Search className="w-5 h-5 text-theme-text-secondary shrink-0" />
|
|
144
|
-
<
|
|
145
|
+
<Input
|
|
145
146
|
ref={inputRef}
|
|
146
|
-
type="text"
|
|
147
147
|
value={query}
|
|
148
148
|
onChange={(e) => setQuery(e.target.value)}
|
|
149
149
|
onKeyDown={handleKeyDown}
|