@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
package/src/api/client.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'react'
|
|
2
|
-
import type { AppRow } from '@skyhook-io/k8s-ui'
|
|
2
|
+
import type { AppHistory, AppRow } from '@skyhook-io/k8s-ui'
|
|
3
3
|
import { useQuery, useMutation, useQueryClient, skipToken } from '@tanstack/react-query'
|
|
4
4
|
import { showApiError, showApiSuccess } from '../components/ui/Toast'
|
|
5
5
|
import { useCanHelmWrite } from '../contexts/CapabilitiesContext'
|
|
@@ -371,11 +371,12 @@ export interface DashboardCRDsResponse {
|
|
|
371
371
|
topCRDs: DashboardCRDCount[]
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
-
export function useDashboard(namespaces: string[] = []) {
|
|
374
|
+
export function useDashboard(namespaces: string[] = [], options?: { enabled?: boolean }) {
|
|
375
375
|
const params = namespaces.length > 0 ? `?namespaces=${namespaces.join(',')}` : ''
|
|
376
376
|
return useQuery<DashboardResponse>({
|
|
377
377
|
queryKey: ['dashboard', namespaces],
|
|
378
378
|
queryFn: () => fetchJSON(`/dashboard${params}`),
|
|
379
|
+
enabled: options?.enabled ?? true,
|
|
379
380
|
staleTime: 15000, // 15 seconds
|
|
380
381
|
refetchInterval: DASHBOARD_REFRESH_INTERVAL_MS,
|
|
381
382
|
})
|
|
@@ -982,34 +983,55 @@ export function useNamespaces() {
|
|
|
982
983
|
}
|
|
983
984
|
|
|
984
985
|
// Topology (for manual refresh)
|
|
985
|
-
export function useTopology(namespaces: string[], viewMode: string = 'resources', options?: { enabled?: boolean }) {
|
|
986
|
+
export function useTopology(namespaces: string[], viewMode: string = 'resources', options?: { enabled?: boolean; includeReplicaSets?: boolean; refetchInterval?: number | false }) {
|
|
986
987
|
const params = new URLSearchParams()
|
|
987
988
|
if (namespaces.length > 0) params.set('namespaces', namespaces.join(','))
|
|
988
989
|
if (viewMode) params.set('view', viewMode)
|
|
990
|
+
if (options?.includeReplicaSets) params.set('includeReplicaSets', 'true')
|
|
989
991
|
const queryString = params.toString()
|
|
990
992
|
|
|
991
993
|
return useQuery<Topology>({
|
|
992
|
-
queryKey: ['topology', namespaces, viewMode],
|
|
994
|
+
queryKey: ['topology', namespaces, viewMode, options?.includeReplicaSets ?? false],
|
|
993
995
|
queryFn: () => fetchJSON(`/topology${queryString ? `?${queryString}` : ''}`),
|
|
994
996
|
staleTime: 5000, // 5 seconds
|
|
995
997
|
enabled: options?.enabled !== false,
|
|
998
|
+
refetchInterval: options?.refetchInterval,
|
|
996
999
|
})
|
|
997
1000
|
}
|
|
998
1001
|
|
|
999
|
-
export function useApplications(namespaces: string[]) {
|
|
1002
|
+
export function useApplications(namespaces: string[], options?: { enabled?: boolean }) {
|
|
1000
1003
|
const params = new URLSearchParams()
|
|
1001
1004
|
if (namespaces.length > 0) params.set('namespaces', namespaces.join(','))
|
|
1002
1005
|
const queryString = params.toString()
|
|
1003
1006
|
|
|
1007
|
+
const enabled = options?.enabled !== false
|
|
1004
1008
|
return useQuery<{ applications: AppRow[] }>({
|
|
1005
1009
|
queryKey: ['applications', namespaces],
|
|
1006
1010
|
queryFn: () => fetchJSON(`/applications${queryString ? `?${queryString}` : ''}`),
|
|
1007
1011
|
staleTime: 30_000,
|
|
1012
|
+
// Only poll while a consumer needs the index; gated off it must not keep the
|
|
1013
|
+
// background refetch alive.
|
|
1014
|
+
enabled,
|
|
1015
|
+
refetchInterval: enabled ? APPLICATIONS_REFRESH_INTERVAL_MS : false,
|
|
1016
|
+
})
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
export function useApplicationHistory(appKey: string | undefined, namespaces: string[], options?: { enabled?: boolean }) {
|
|
1020
|
+
const params = new URLSearchParams()
|
|
1021
|
+
if (appKey) params.set('app', appKey)
|
|
1022
|
+
if (namespaces.length > 0) params.set('namespaces', namespaces.join(','))
|
|
1023
|
+
const queryString = params.toString()
|
|
1024
|
+
|
|
1025
|
+
return useQuery<AppHistory>({
|
|
1026
|
+
queryKey: ['application-history', appKey, namespaces],
|
|
1027
|
+
queryFn: appKey ? () => fetchJSON(`/applications/history?${queryString}`) : skipToken,
|
|
1028
|
+
enabled: Boolean(appKey) && (options?.enabled ?? true),
|
|
1029
|
+
staleTime: 15_000,
|
|
1008
1030
|
refetchInterval: APPLICATIONS_REFRESH_INTERVAL_MS,
|
|
1009
1031
|
})
|
|
1010
1032
|
}
|
|
1011
1033
|
|
|
1012
|
-
export function useGitOpsTree(kind: string, namespace: string, name: string, group?: string, namespaces: string[] = []) {
|
|
1034
|
+
export function useGitOpsTree(kind: string, namespace: string, name: string, group?: string, namespaces: string[] = [], options?: { enabled?: boolean }) {
|
|
1013
1035
|
const ns = namespace || '_'
|
|
1014
1036
|
const params = new URLSearchParams()
|
|
1015
1037
|
if (group) params.set('group', group)
|
|
@@ -1019,7 +1041,7 @@ export function useGitOpsTree(kind: string, namespace: string, name: string, gro
|
|
|
1019
1041
|
return useQuery<GitOpsResourceTree>({
|
|
1020
1042
|
queryKey: ['gitops-tree', kind, namespace, name, group, namespaces],
|
|
1021
1043
|
queryFn: () => fetchJSON(`/gitops/tree/${kind}/${ns}/${name}${queryString ? `?${queryString}` : ''}`),
|
|
1022
|
-
enabled: Boolean(kind && name),
|
|
1044
|
+
enabled: Boolean(kind && name) && (options?.enabled ?? true),
|
|
1023
1045
|
staleTime: 5000,
|
|
1024
1046
|
})
|
|
1025
1047
|
}
|
|
@@ -1051,7 +1073,7 @@ export function useGitOpsInsights(kind: string, namespace: string, name: string,
|
|
|
1051
1073
|
|
|
1052
1074
|
// Generic resource fetching - returns resource with relationships
|
|
1053
1075
|
// Uses '_' as placeholder for cluster-scoped resources (empty namespace)
|
|
1054
|
-
export function useResource<T>(kind: string, namespace: string, name: string, group?: string) {
|
|
1076
|
+
export function useResource<T>(kind: string, namespace: string, name: string, group?: string, options?: { enabled?: boolean; refetchInterval?: number | false }) {
|
|
1055
1077
|
// For cluster-scoped resources, use '_' as namespace placeholder
|
|
1056
1078
|
const ns = namespace || '_'
|
|
1057
1079
|
const params = new URLSearchParams()
|
|
@@ -1061,7 +1083,8 @@ export function useResource<T>(kind: string, namespace: string, name: string, gr
|
|
|
1061
1083
|
const query = useQuery<ResourceWithRelationships<T>>({
|
|
1062
1084
|
queryKey: ['resource', kind, namespace, name, group],
|
|
1063
1085
|
queryFn: () => fetchJSON(`/resources/${kind}/${ns}/${name}${queryString ? `?${queryString}` : ''}`),
|
|
1064
|
-
enabled: Boolean(kind && name), // namespace can be empty for cluster-scoped resources
|
|
1086
|
+
enabled: (options?.enabled ?? true) && Boolean(kind && name), // namespace can be empty for cluster-scoped resources
|
|
1087
|
+
refetchInterval: options?.refetchInterval,
|
|
1065
1088
|
})
|
|
1066
1089
|
|
|
1067
1090
|
// Extract resource and relationships from the response
|
|
@@ -1112,7 +1135,10 @@ export function useResources<T>(
|
|
|
1112
1135
|
// Timeline changes (unified view of changes + K8s events)
|
|
1113
1136
|
export interface UseChangesOptions {
|
|
1114
1137
|
namespaces?: string[]
|
|
1115
|
-
kind
|
|
1138
|
+
// Kind filter. The server narrows to a single kind (tighter result caps), so
|
|
1139
|
+
// exactly one selected kind is pushed server-side; a multi-kind selection
|
|
1140
|
+
// fetches unfiltered and is narrowed client-side by the caller.
|
|
1141
|
+
kinds?: string[]
|
|
1116
1142
|
timeRange?: TimeRange
|
|
1117
1143
|
filter?: string // Filter preset name ('default', 'all', 'warnings-only', 'workloads')
|
|
1118
1144
|
includeK8sEvents?: boolean
|
|
@@ -1120,6 +1146,122 @@ export interface UseChangesOptions {
|
|
|
1120
1146
|
includeDeleted?: boolean
|
|
1121
1147
|
limit?: number
|
|
1122
1148
|
enabled?: boolean
|
|
1149
|
+
// Cursor-aware refetches: after the first full load, refetches ask the
|
|
1150
|
+
// server only for events that arrived after the highest seq already cached
|
|
1151
|
+
// and merge them in, instead of re-pulling the whole ring. Intended for the
|
|
1152
|
+
// timeline's full-ring (10k) query, where every SSE nudge would otherwise
|
|
1153
|
+
// re-transfer megabytes for a handful of new events.
|
|
1154
|
+
deltaSync?: boolean
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// The store epoch guards delta cursors: a restarted store restarts seq
|
|
1158
|
+
// numbering, so an epoch change forces a full resync. A periodic full resync
|
|
1159
|
+
// also runs as anti-entropy for anything a dropped SSE connection or a
|
|
1160
|
+
// server-side eviction could leave behind in the cached copy.
|
|
1161
|
+
const FULL_RESYNC_MS = 5 * 60_000
|
|
1162
|
+
|
|
1163
|
+
export interface ChangesDeltaMeta {
|
|
1164
|
+
epoch: string
|
|
1165
|
+
lastFullMs: number
|
|
1166
|
+
// Highest seq observed in ANY response for this query — not just what
|
|
1167
|
+
// survived the cap. A delta event older than everything cached gets capped
|
|
1168
|
+
// out of the merge; deriving the cursor from cached rows alone would then
|
|
1169
|
+
// re-request that same event on every refetch until the next full resync.
|
|
1170
|
+
highWaterSeq: number
|
|
1171
|
+
}
|
|
1172
|
+
const changesDeltaMeta = new Map<string, ChangesDeltaMeta>()
|
|
1173
|
+
|
|
1174
|
+
// The since_seq cursor for the next refetch, or 0 for a full fetch. Delta
|
|
1175
|
+
// requires an epoch-stamped prior full load, a cached page to merge into, and
|
|
1176
|
+
// the anti-entropy full resync not being due.
|
|
1177
|
+
export function deltaFetchCursor(
|
|
1178
|
+
meta: ChangesDeltaMeta | undefined,
|
|
1179
|
+
cached: TimelineEvent[] | undefined,
|
|
1180
|
+
nowMs: number,
|
|
1181
|
+
): number {
|
|
1182
|
+
if (!meta?.epoch || !cached) return 0
|
|
1183
|
+
if (nowMs - meta.lastFullMs > FULL_RESYNC_MS) return 0
|
|
1184
|
+
return Math.max(meta.highWaterSeq, maxEventSeq(cached))
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
async function fetchChangesPage(
|
|
1188
|
+
path: string,
|
|
1189
|
+
signal?: AbortSignal,
|
|
1190
|
+
): Promise<{ events: TimelineEvent[]; epoch: string; maxSeq: number }> {
|
|
1191
|
+
const response = await apiFetch(`${getApiBase()}${path}`, signal ? { signal } : undefined)
|
|
1192
|
+
if (!response.ok) {
|
|
1193
|
+
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
1194
|
+
throw new ApiError(errorData.error || `HTTP ${response.status}`, response.status, errorData)
|
|
1195
|
+
}
|
|
1196
|
+
const events = (await response.json()) as TimelineEvent[]
|
|
1197
|
+
// maxSeq is the page's frontier computed before the server's
|
|
1198
|
+
// cluster-scoped-RBAC filter — rows dropped THERE still advance the cursor.
|
|
1199
|
+
// (Rows dropped by content filters inside the store query do not; see the
|
|
1200
|
+
// known limitation on the server's handleChanges.)
|
|
1201
|
+
const maxSeq = Number(response.headers.get('X-Radar-Timeline-Max-Seq') ?? '0') || 0
|
|
1202
|
+
return { events, epoch: response.headers.get('X-Radar-Timeline-Epoch') ?? '', maxSeq }
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Highest store-assigned arrival number in the cached page — the delta cursor.
|
|
1206
|
+
export function maxEventSeq(events: TimelineEvent[]): number {
|
|
1207
|
+
let max = 0
|
|
1208
|
+
for (const event of events) {
|
|
1209
|
+
if (event.seq && event.seq > max) max = event.seq
|
|
1210
|
+
}
|
|
1211
|
+
return max
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// Merge a delta page into the cached page: a delta row replaces its cached id
|
|
1215
|
+
// (a K8s Event count bump re-arrives under the same id), new ids are added,
|
|
1216
|
+
// order stays newest-first (arrival number breaks timestamp ties), and the
|
|
1217
|
+
// result is capped to the query's limit by dropping the oldest.
|
|
1218
|
+
export function mergeDeltaEvents(
|
|
1219
|
+
prev: TimelineEvent[],
|
|
1220
|
+
delta: TimelineEvent[],
|
|
1221
|
+
cap: number,
|
|
1222
|
+
): TimelineEvent[] {
|
|
1223
|
+
if (delta.length === 0) return prev
|
|
1224
|
+
const replaced = new Set(delta.map((event) => event.id))
|
|
1225
|
+
const merged = [...delta, ...prev.filter((event) => !replaced.has(event.id))]
|
|
1226
|
+
merged.sort((a, b) => {
|
|
1227
|
+
const byTime = new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
1228
|
+
if (byTime !== 0) return byTime
|
|
1229
|
+
return (b.seq ?? 0) - (a.seq ?? 0)
|
|
1230
|
+
})
|
|
1231
|
+
return merged.length > cap ? merged.slice(0, cap) : merged
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// Delta-sync orchestration for useChanges, extracted so the
|
|
1235
|
+
// full-fetch → delta-poll → epoch-mismatch-resync contract is exercisable
|
|
1236
|
+
// without a React render. State is passed in explicitly — the cached page and
|
|
1237
|
+
// the shared meta store — rather than closed over from module scope, so a
|
|
1238
|
+
// caller (and a test) drives it with fresh state each invocation.
|
|
1239
|
+
export async function runDeltaSyncFetch(deps: {
|
|
1240
|
+
path: string
|
|
1241
|
+
queryString: string
|
|
1242
|
+
limit: number
|
|
1243
|
+
metaKey: string
|
|
1244
|
+
cached: TimelineEvent[] | undefined
|
|
1245
|
+
metaStore: Map<string, ChangesDeltaMeta>
|
|
1246
|
+
now: number
|
|
1247
|
+
signal?: AbortSignal
|
|
1248
|
+
}): Promise<TimelineEvent[]> {
|
|
1249
|
+
const { path, queryString, limit, metaKey, cached, metaStore, now, signal } = deps
|
|
1250
|
+
const meta = metaStore.get(metaKey)
|
|
1251
|
+
const cursor = deltaFetchCursor(meta, cached, now)
|
|
1252
|
+
if (cursor > 0) {
|
|
1253
|
+
const delta = await fetchChangesPage(`${path}${queryString ? '&' : '?'}since_seq=${cursor}`, signal)
|
|
1254
|
+
if (delta.epoch && delta.epoch === meta!.epoch) {
|
|
1255
|
+
meta!.highWaterSeq = Math.max(meta!.highWaterSeq, delta.maxSeq, maxEventSeq(delta.events))
|
|
1256
|
+
// Returning the cached reference on an empty delta skips re-renders.
|
|
1257
|
+
return delta.events.length ? mergeDeltaEvents(cached!, delta.events, limit) : cached!
|
|
1258
|
+
}
|
|
1259
|
+
// Epoch changed — the store restarted and seq numbering reset, so the
|
|
1260
|
+
// cursor is meaningless. Fall through to a full resync.
|
|
1261
|
+
}
|
|
1262
|
+
const full = await fetchChangesPage(path, signal)
|
|
1263
|
+
metaStore.set(metaKey, { epoch: full.epoch, lastFullMs: now, highWaterSeq: Math.max(full.maxSeq, maxEventSeq(full.events)) })
|
|
1264
|
+
return full.events
|
|
1123
1265
|
}
|
|
1124
1266
|
|
|
1125
1267
|
function getTimeRangeDate(range: TimeRange): Date | null {
|
|
@@ -1136,17 +1278,26 @@ function getTimeRangeDate(range: TimeRange): Date | null {
|
|
|
1136
1278
|
return new Date(now.getTime() - 6 * 60 * 60 * 1000)
|
|
1137
1279
|
case '24h':
|
|
1138
1280
|
return new Date(now.getTime() - 24 * 60 * 60 * 1000)
|
|
1281
|
+
case '7d':
|
|
1282
|
+
return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
|
|
1283
|
+
case '30d':
|
|
1284
|
+
return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000)
|
|
1139
1285
|
default:
|
|
1140
1286
|
return null
|
|
1141
1287
|
}
|
|
1142
1288
|
}
|
|
1143
1289
|
|
|
1144
1290
|
export function useChanges(options: UseChangesOptions = {}) {
|
|
1145
|
-
const { namespaces = [],
|
|
1291
|
+
const { namespaces = [], kinds, timeRange = '1h', filter = 'all', includeK8sEvents = true, includeManaged = false, includeDeleted = true, limit = 200, enabled = true, deltaSync = false } = options
|
|
1292
|
+
const queryClient = useQueryClient()
|
|
1293
|
+
|
|
1294
|
+
// Only a single-kind selection narrows the server query; a multi-kind
|
|
1295
|
+
// selection is filtered client-side so the server cap isn't spent on one kind.
|
|
1296
|
+
const serverKind = kinds && kinds.length === 1 ? kinds[0] : undefined
|
|
1146
1297
|
|
|
1147
1298
|
const params = new URLSearchParams()
|
|
1148
1299
|
if (namespaces.length > 0) params.set('namespaces', namespaces.join(','))
|
|
1149
|
-
if (
|
|
1300
|
+
if (serverKind) params.set('kind', serverKind)
|
|
1150
1301
|
if (filter) params.set('filter', filter)
|
|
1151
1302
|
if (!includeK8sEvents) params.set('include_k8s_events', 'false')
|
|
1152
1303
|
if (includeManaged) params.set('include_managed', 'true')
|
|
@@ -1159,12 +1310,20 @@ export function useChanges(options: UseChangesOptions = {}) {
|
|
|
1159
1310
|
}
|
|
1160
1311
|
|
|
1161
1312
|
const queryString = params.toString()
|
|
1313
|
+
const path = `/changes${queryString ? `?${queryString}` : ''}`
|
|
1314
|
+
const queryKey = ['changes', namespaces, serverKind, timeRange, filter, includeK8sEvents, includeManaged, includeDeleted, limit]
|
|
1162
1315
|
|
|
1163
1316
|
return useQuery<TimelineEvent[]>({
|
|
1164
|
-
queryKey
|
|
1165
|
-
queryFn:
|
|
1317
|
+
queryKey,
|
|
1318
|
+
queryFn: async ({ signal }) => {
|
|
1319
|
+
if (!deltaSync) return fetchJSON(path, signal)
|
|
1320
|
+
|
|
1321
|
+
const metaKey = JSON.stringify(queryKey)
|
|
1322
|
+
const cached = queryClient.getQueryData<TimelineEvent[]>(queryKey)
|
|
1323
|
+
return runDeltaSyncFetch({ path, queryString, limit, metaKey, cached, metaStore: changesDeltaMeta, now: Date.now(), signal })
|
|
1324
|
+
},
|
|
1166
1325
|
staleTime: 5000, // Consider data stale after 5 seconds to ensure fresh data on navigation
|
|
1167
|
-
refetchInterval: CHANGES_REFRESH_INTERVAL_MS, // SSE handles real-time updates; this is
|
|
1326
|
+
refetchInterval: CHANGES_REFRESH_INTERVAL_MS, // SSE-driven invalidation handles real-time updates; this is the no-SSE fallback
|
|
1168
1327
|
enabled,
|
|
1169
1328
|
})
|
|
1170
1329
|
}
|
|
@@ -2155,6 +2314,17 @@ export function useApplyResource() {
|
|
|
2155
2314
|
// CronJob operations
|
|
2156
2315
|
// ============================================================================
|
|
2157
2316
|
|
|
2317
|
+
function invalidateCronJobOperationQueries(queryClient: ReturnType<typeof useQueryClient>, namespace: string, name: string) {
|
|
2318
|
+
queryClient.invalidateQueries({ queryKey: ['resources', 'cronjobs'] })
|
|
2319
|
+
queryClient.invalidateQueries({ queryKey: ['resources', 'jobs'] })
|
|
2320
|
+
queryClient.invalidateQueries({ queryKey: ['resource', 'cronjobs', namespace, name] })
|
|
2321
|
+
queryClient.invalidateQueries({ queryKey: ['workload-runs', 'cronjobs', namespace, name] })
|
|
2322
|
+
queryClient.invalidateQueries({ queryKey: ['applications'] })
|
|
2323
|
+
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
|
|
2324
|
+
queryClient.invalidateQueries({ queryKey: ['resource-counts'] })
|
|
2325
|
+
queryClient.invalidateQueries({ queryKey: ['topology'] })
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2158
2328
|
// Trigger a CronJob (create a Job from it)
|
|
2159
2329
|
export function useTriggerCronJob() {
|
|
2160
2330
|
const queryClient = useQueryClient()
|
|
@@ -2174,10 +2344,8 @@ export function useTriggerCronJob() {
|
|
|
2174
2344
|
errorMessage: 'Failed to trigger CronJob',
|
|
2175
2345
|
successMessage: 'CronJob triggered',
|
|
2176
2346
|
},
|
|
2177
|
-
onSuccess: () => {
|
|
2178
|
-
queryClient.
|
|
2179
|
-
queryClient.invalidateQueries({ queryKey: ['resources', 'jobs'] })
|
|
2180
|
-
queryClient.invalidateQueries({ queryKey: ['topology'] })
|
|
2347
|
+
onSuccess: (_, variables) => {
|
|
2348
|
+
invalidateCronJobOperationQueries(queryClient, variables.namespace, variables.name)
|
|
2181
2349
|
},
|
|
2182
2350
|
})
|
|
2183
2351
|
}
|
|
@@ -2201,9 +2369,8 @@ export function useSuspendCronJob() {
|
|
|
2201
2369
|
errorMessage: 'Failed to suspend CronJob',
|
|
2202
2370
|
successMessage: 'CronJob suspended',
|
|
2203
2371
|
},
|
|
2204
|
-
onSuccess: () => {
|
|
2205
|
-
queryClient.
|
|
2206
|
-
queryClient.invalidateQueries({ queryKey: ['topology'] })
|
|
2372
|
+
onSuccess: (_, variables) => {
|
|
2373
|
+
invalidateCronJobOperationQueries(queryClient, variables.namespace, variables.name)
|
|
2207
2374
|
},
|
|
2208
2375
|
})
|
|
2209
2376
|
}
|
|
@@ -2227,9 +2394,8 @@ export function useResumeCronJob() {
|
|
|
2227
2394
|
errorMessage: 'Failed to resume CronJob',
|
|
2228
2395
|
successMessage: 'CronJob resumed',
|
|
2229
2396
|
},
|
|
2230
|
-
onSuccess: () => {
|
|
2231
|
-
queryClient.
|
|
2232
|
-
queryClient.invalidateQueries({ queryKey: ['topology'] })
|
|
2397
|
+
onSuccess: (_, variables) => {
|
|
2398
|
+
invalidateCronJobOperationQueries(queryClient, variables.namespace, variables.name)
|
|
2233
2399
|
},
|
|
2234
2400
|
})
|
|
2235
2401
|
}
|
|
@@ -2462,11 +2628,11 @@ export function useHelmReleases(namespaces: string[] = []) {
|
|
|
2462
2628
|
}
|
|
2463
2629
|
|
|
2464
2630
|
// Get details for a specific Helm release
|
|
2465
|
-
export function useHelmRelease(namespace: string, name: string) {
|
|
2631
|
+
export function useHelmRelease(namespace: string, name: string, options?: { enabled?: boolean }) {
|
|
2466
2632
|
return useQuery<HelmReleaseDetail>({
|
|
2467
2633
|
queryKey: ['helm-release', namespace, name],
|
|
2468
2634
|
queryFn: () => fetchJSON(`/helm/releases/${namespace}/${name}`),
|
|
2469
|
-
enabled: Boolean(namespace && name),
|
|
2635
|
+
enabled: Boolean(namespace && name) && (options?.enabled ?? true),
|
|
2470
2636
|
staleTime: 5000,
|
|
2471
2637
|
refetchInterval: 10000, // Poll for live resource status updates (post-upgrade/rollback)
|
|
2472
2638
|
})
|
|
@@ -2753,19 +2919,24 @@ function streamHelmProgress(
|
|
|
2753
2919
|
})
|
|
2754
2920
|
}
|
|
2755
2921
|
|
|
2756
|
-
//
|
|
2922
|
+
// When `values` is provided, the upgrade applies exactly those edited values
|
|
2923
|
+
// instead of carrying the release's prior values over blindly.
|
|
2757
2924
|
export function upgradeWithProgress(
|
|
2758
2925
|
namespace: string,
|
|
2759
2926
|
name: string,
|
|
2760
2927
|
version: string,
|
|
2761
2928
|
repositoryName: string | undefined,
|
|
2762
|
-
onProgress: (event: InstallProgressEvent) => void
|
|
2929
|
+
onProgress: (event: InstallProgressEvent) => void,
|
|
2930
|
+
values?: Record<string, unknown>
|
|
2763
2931
|
): Promise<void> {
|
|
2764
2932
|
const params = new URLSearchParams({ version })
|
|
2765
2933
|
if (repositoryName) params.set('repository', repositoryName)
|
|
2934
|
+
const options: RequestInit = values
|
|
2935
|
+
? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ values }) }
|
|
2936
|
+
: { method: 'POST' }
|
|
2766
2937
|
return streamHelmProgress(
|
|
2767
2938
|
`${getApiBase()}/helm/releases/${namespace}/${name}/upgrade-stream?${params.toString()}`,
|
|
2768
|
-
|
|
2939
|
+
options,
|
|
2769
2940
|
onProgress,
|
|
2770
2941
|
'Upgrade failed',
|
|
2771
2942
|
).then(() => {})
|
|
@@ -2786,14 +2957,15 @@ export function rollbackWithProgress(
|
|
|
2786
2957
|
).then(() => {})
|
|
2787
2958
|
}
|
|
2788
2959
|
|
|
2789
|
-
//
|
|
2960
|
+
// When `version` is supplied, preview renders against that target chart version
|
|
2961
|
+
// instead of the release's current chart.
|
|
2790
2962
|
export function useHelmPreviewValues() {
|
|
2791
|
-
return useMutation<ValuesPreviewResponse, Error, { namespace: string; name: string; values: Record<string, unknown
|
|
2792
|
-
mutationFn: async ({ namespace, name, values }) => {
|
|
2963
|
+
return useMutation<ValuesPreviewResponse, Error, { namespace: string; name: string; values: Record<string, unknown>; version?: string; repository?: string }>({
|
|
2964
|
+
mutationFn: async ({ namespace, name, values, version, repository }) => {
|
|
2793
2965
|
const response = await apiFetch(`${getApiBase()}/helm/releases/${namespace}/${name}/values/preview`, {
|
|
2794
2966
|
method: 'POST',
|
|
2795
2967
|
headers: { 'Content-Type': 'application/json' },
|
|
2796
|
-
body: JSON.stringify({ values }),
|
|
2968
|
+
body: JSON.stringify({ values, version, repository }),
|
|
2797
2969
|
})
|
|
2798
2970
|
if (!response.ok) {
|
|
2799
2971
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
@@ -3544,6 +3716,44 @@ export interface WorkloadLogsResponse {
|
|
|
3544
3716
|
timestamp: string
|
|
3545
3717
|
content: string
|
|
3546
3718
|
}[]
|
|
3719
|
+
emptyReason?: string
|
|
3720
|
+
emptyMessage?: string
|
|
3721
|
+
command?: string
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3724
|
+
export interface WorkloadRun {
|
|
3725
|
+
kind: string
|
|
3726
|
+
namespace: string
|
|
3727
|
+
name: string
|
|
3728
|
+
phase: string
|
|
3729
|
+
active: boolean
|
|
3730
|
+
startedAt?: string
|
|
3731
|
+
finishedAt?: string
|
|
3732
|
+
scheduledAt?: string
|
|
3733
|
+
trigger?: 'manual' | 'schedule' | string
|
|
3734
|
+
message?: string
|
|
3735
|
+
succeeded?: number
|
|
3736
|
+
failed?: number
|
|
3737
|
+
running?: number
|
|
3738
|
+
desired?: number
|
|
3739
|
+
parallelism?: number
|
|
3740
|
+
progress?: string
|
|
3741
|
+
template?: string
|
|
3742
|
+
launcher?: {
|
|
3743
|
+
kind: string
|
|
3744
|
+
namespace?: string
|
|
3745
|
+
name: string
|
|
3746
|
+
group?: string
|
|
3747
|
+
}
|
|
3748
|
+
podTotal?: number
|
|
3749
|
+
podSucceeded?: number
|
|
3750
|
+
podFailed?: number
|
|
3751
|
+
podRunning?: number
|
|
3752
|
+
podPending?: number
|
|
3753
|
+
}
|
|
3754
|
+
|
|
3755
|
+
export interface WorkloadRunsResponse {
|
|
3756
|
+
runs: WorkloadRun[]
|
|
3547
3757
|
}
|
|
3548
3758
|
|
|
3549
3759
|
// Fetch pods for a workload
|
|
@@ -3556,6 +3766,24 @@ export function useWorkloadPods(kind: string, namespace: string, name: string) {
|
|
|
3556
3766
|
})
|
|
3557
3767
|
}
|
|
3558
3768
|
|
|
3769
|
+
export function useWorkloadRuns(kind: string, namespace: string, name: string, enabled = true, options?: { refetchActive?: boolean; clusterScoped?: boolean }) {
|
|
3770
|
+
const clusterScoped = options?.clusterScoped ?? false
|
|
3771
|
+
const ns = clusterScoped ? '_' : namespace
|
|
3772
|
+
const params = new URLSearchParams()
|
|
3773
|
+
if (clusterScoped) params.set('clusterScoped', 'true')
|
|
3774
|
+
const queryString = params.toString()
|
|
3775
|
+
|
|
3776
|
+
return useQuery<WorkloadRunsResponse>({
|
|
3777
|
+
queryKey: ['workload-runs', kind, namespace, name, clusterScoped],
|
|
3778
|
+
queryFn: () => fetchJSON(`/workloads/${kind}/${ns}/${name}/runs${queryString ? `?${queryString}` : ''}`),
|
|
3779
|
+
enabled: enabled && Boolean(kind && name && (namespace || clusterScoped)),
|
|
3780
|
+
staleTime: 10000,
|
|
3781
|
+
refetchInterval: options?.refetchActive
|
|
3782
|
+
? (query) => query.state.data?.runs?.some((run) => run.active) ? 5000 : 30000
|
|
3783
|
+
: false,
|
|
3784
|
+
})
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3559
3787
|
// Fetch logs for a workload (non-streaming)
|
|
3560
3788
|
export function useWorkloadLogs(
|
|
3561
3789
|
kind: string,
|