@skyhook-io/radar-app 1.8.5 → 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 +229 -183
- 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.metrics.test.ts +106 -0
- package/src/api/client.ts +377 -48
- 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 +336 -32
- 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 +22 -18
- 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/NodeRenderer.tsx +10 -4
- package/src/components/resources/renderers/PodRenderer.tsx +10 -4
- 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'
|
|
@@ -103,6 +103,47 @@ export function isForbiddenError(error: unknown): boolean {
|
|
|
103
103
|
return error instanceof ApiError && error.status === 403
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
const METRICS_API_GROUP_TOKENS = ['metrics', 'k8s', 'io'] as const
|
|
107
|
+
|
|
108
|
+
function mentionsMetricsAPIGroup(message: string): boolean {
|
|
109
|
+
const tokens = message.split(/[^a-z0-9]+/).filter(Boolean)
|
|
110
|
+
return tokens.some((token, index) => (
|
|
111
|
+
token === METRICS_API_GROUP_TOKENS[0] &&
|
|
112
|
+
tokens[index + 1] === METRICS_API_GROUP_TOKENS[1] &&
|
|
113
|
+
tokens[index + 2] === METRICS_API_GROUP_TOKENS[2]
|
|
114
|
+
))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function hasMetricsUnavailablePhrase(message: string): boolean {
|
|
118
|
+
return (
|
|
119
|
+
message.includes('may not be installed') ||
|
|
120
|
+
message.includes('not found') ||
|
|
121
|
+
message.includes('could not find the requested resource') ||
|
|
122
|
+
message.includes('no matches for kind') ||
|
|
123
|
+
message.includes('no resource matches') ||
|
|
124
|
+
message.includes('no metrics known') ||
|
|
125
|
+
message.includes('not available') ||
|
|
126
|
+
message.includes('unable to fetch metrics') ||
|
|
127
|
+
message.includes('currently unable to handle the request')
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function isMetricsUnavailableError(error: unknown): boolean {
|
|
132
|
+
if (!(error instanceof ApiError)) return false
|
|
133
|
+
if (error.status !== 404 && error.status !== 500) return false
|
|
134
|
+
return [error.message, error.data?.error].some((message) => {
|
|
135
|
+
if (typeof message !== 'string') return false
|
|
136
|
+
const normalized = message.toLowerCase()
|
|
137
|
+
const hasMetricsSignal = (
|
|
138
|
+
normalized.includes('metrics-server') ||
|
|
139
|
+
mentionsMetricsAPIGroup(normalized) ||
|
|
140
|
+
normalized.includes('pod metrics') ||
|
|
141
|
+
normalized.includes('node metrics')
|
|
142
|
+
)
|
|
143
|
+
return hasMetricsSignal && hasMetricsUnavailablePhrase(normalized)
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
106
147
|
export async function fetchJSON<T>(path: string, signal?: AbortSignal): Promise<T> {
|
|
107
148
|
const response = await apiFetch(`${getApiBase()}${path}`, signal ? { signal } : undefined)
|
|
108
149
|
if (!response.ok) {
|
|
@@ -330,11 +371,12 @@ export interface DashboardCRDsResponse {
|
|
|
330
371
|
topCRDs: DashboardCRDCount[]
|
|
331
372
|
}
|
|
332
373
|
|
|
333
|
-
export function useDashboard(namespaces: string[] = []) {
|
|
374
|
+
export function useDashboard(namespaces: string[] = [], options?: { enabled?: boolean }) {
|
|
334
375
|
const params = namespaces.length > 0 ? `?namespaces=${namespaces.join(',')}` : ''
|
|
335
376
|
return useQuery<DashboardResponse>({
|
|
336
377
|
queryKey: ['dashboard', namespaces],
|
|
337
378
|
queryFn: () => fetchJSON(`/dashboard${params}`),
|
|
379
|
+
enabled: options?.enabled ?? true,
|
|
338
380
|
staleTime: 15000, // 15 seconds
|
|
339
381
|
refetchInterval: DASHBOARD_REFRESH_INTERVAL_MS,
|
|
340
382
|
})
|
|
@@ -941,34 +983,55 @@ export function useNamespaces() {
|
|
|
941
983
|
}
|
|
942
984
|
|
|
943
985
|
// Topology (for manual refresh)
|
|
944
|
-
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 }) {
|
|
945
987
|
const params = new URLSearchParams()
|
|
946
988
|
if (namespaces.length > 0) params.set('namespaces', namespaces.join(','))
|
|
947
989
|
if (viewMode) params.set('view', viewMode)
|
|
990
|
+
if (options?.includeReplicaSets) params.set('includeReplicaSets', 'true')
|
|
948
991
|
const queryString = params.toString()
|
|
949
992
|
|
|
950
993
|
return useQuery<Topology>({
|
|
951
|
-
queryKey: ['topology', namespaces, viewMode],
|
|
994
|
+
queryKey: ['topology', namespaces, viewMode, options?.includeReplicaSets ?? false],
|
|
952
995
|
queryFn: () => fetchJSON(`/topology${queryString ? `?${queryString}` : ''}`),
|
|
953
996
|
staleTime: 5000, // 5 seconds
|
|
954
997
|
enabled: options?.enabled !== false,
|
|
998
|
+
refetchInterval: options?.refetchInterval,
|
|
955
999
|
})
|
|
956
1000
|
}
|
|
957
1001
|
|
|
958
|
-
export function useApplications(namespaces: string[]) {
|
|
1002
|
+
export function useApplications(namespaces: string[], options?: { enabled?: boolean }) {
|
|
959
1003
|
const params = new URLSearchParams()
|
|
960
1004
|
if (namespaces.length > 0) params.set('namespaces', namespaces.join(','))
|
|
961
1005
|
const queryString = params.toString()
|
|
962
1006
|
|
|
1007
|
+
const enabled = options?.enabled !== false
|
|
963
1008
|
return useQuery<{ applications: AppRow[] }>({
|
|
964
1009
|
queryKey: ['applications', namespaces],
|
|
965
1010
|
queryFn: () => fetchJSON(`/applications${queryString ? `?${queryString}` : ''}`),
|
|
966
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,
|
|
967
1030
|
refetchInterval: APPLICATIONS_REFRESH_INTERVAL_MS,
|
|
968
1031
|
})
|
|
969
1032
|
}
|
|
970
1033
|
|
|
971
|
-
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 }) {
|
|
972
1035
|
const ns = namespace || '_'
|
|
973
1036
|
const params = new URLSearchParams()
|
|
974
1037
|
if (group) params.set('group', group)
|
|
@@ -978,7 +1041,7 @@ export function useGitOpsTree(kind: string, namespace: string, name: string, gro
|
|
|
978
1041
|
return useQuery<GitOpsResourceTree>({
|
|
979
1042
|
queryKey: ['gitops-tree', kind, namespace, name, group, namespaces],
|
|
980
1043
|
queryFn: () => fetchJSON(`/gitops/tree/${kind}/${ns}/${name}${queryString ? `?${queryString}` : ''}`),
|
|
981
|
-
enabled: Boolean(kind && name),
|
|
1044
|
+
enabled: Boolean(kind && name) && (options?.enabled ?? true),
|
|
982
1045
|
staleTime: 5000,
|
|
983
1046
|
})
|
|
984
1047
|
}
|
|
@@ -1010,7 +1073,7 @@ export function useGitOpsInsights(kind: string, namespace: string, name: string,
|
|
|
1010
1073
|
|
|
1011
1074
|
// Generic resource fetching - returns resource with relationships
|
|
1012
1075
|
// Uses '_' as placeholder for cluster-scoped resources (empty namespace)
|
|
1013
|
-
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 }) {
|
|
1014
1077
|
// For cluster-scoped resources, use '_' as namespace placeholder
|
|
1015
1078
|
const ns = namespace || '_'
|
|
1016
1079
|
const params = new URLSearchParams()
|
|
@@ -1020,7 +1083,8 @@ export function useResource<T>(kind: string, namespace: string, name: string, gr
|
|
|
1020
1083
|
const query = useQuery<ResourceWithRelationships<T>>({
|
|
1021
1084
|
queryKey: ['resource', kind, namespace, name, group],
|
|
1022
1085
|
queryFn: () => fetchJSON(`/resources/${kind}/${ns}/${name}${queryString ? `?${queryString}` : ''}`),
|
|
1023
|
-
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,
|
|
1024
1088
|
})
|
|
1025
1089
|
|
|
1026
1090
|
// Extract resource and relationships from the response
|
|
@@ -1071,7 +1135,10 @@ export function useResources<T>(
|
|
|
1071
1135
|
// Timeline changes (unified view of changes + K8s events)
|
|
1072
1136
|
export interface UseChangesOptions {
|
|
1073
1137
|
namespaces?: string[]
|
|
1074
|
-
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[]
|
|
1075
1142
|
timeRange?: TimeRange
|
|
1076
1143
|
filter?: string // Filter preset name ('default', 'all', 'warnings-only', 'workloads')
|
|
1077
1144
|
includeK8sEvents?: boolean
|
|
@@ -1079,6 +1146,122 @@ export interface UseChangesOptions {
|
|
|
1079
1146
|
includeDeleted?: boolean
|
|
1080
1147
|
limit?: number
|
|
1081
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
|
|
1082
1265
|
}
|
|
1083
1266
|
|
|
1084
1267
|
function getTimeRangeDate(range: TimeRange): Date | null {
|
|
@@ -1095,17 +1278,26 @@ function getTimeRangeDate(range: TimeRange): Date | null {
|
|
|
1095
1278
|
return new Date(now.getTime() - 6 * 60 * 60 * 1000)
|
|
1096
1279
|
case '24h':
|
|
1097
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)
|
|
1098
1285
|
default:
|
|
1099
1286
|
return null
|
|
1100
1287
|
}
|
|
1101
1288
|
}
|
|
1102
1289
|
|
|
1103
1290
|
export function useChanges(options: UseChangesOptions = {}) {
|
|
1104
|
-
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
|
|
1105
1297
|
|
|
1106
1298
|
const params = new URLSearchParams()
|
|
1107
1299
|
if (namespaces.length > 0) params.set('namespaces', namespaces.join(','))
|
|
1108
|
-
if (
|
|
1300
|
+
if (serverKind) params.set('kind', serverKind)
|
|
1109
1301
|
if (filter) params.set('filter', filter)
|
|
1110
1302
|
if (!includeK8sEvents) params.set('include_k8s_events', 'false')
|
|
1111
1303
|
if (includeManaged) params.set('include_managed', 'true')
|
|
@@ -1118,12 +1310,20 @@ export function useChanges(options: UseChangesOptions = {}) {
|
|
|
1118
1310
|
}
|
|
1119
1311
|
|
|
1120
1312
|
const queryString = params.toString()
|
|
1313
|
+
const path = `/changes${queryString ? `?${queryString}` : ''}`
|
|
1314
|
+
const queryKey = ['changes', namespaces, serverKind, timeRange, filter, includeK8sEvents, includeManaged, includeDeleted, limit]
|
|
1121
1315
|
|
|
1122
1316
|
return useQuery<TimelineEvent[]>({
|
|
1123
|
-
queryKey
|
|
1124
|
-
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
|
+
},
|
|
1125
1325
|
staleTime: 5000, // Consider data stale after 5 seconds to ensure fresh data on navigation
|
|
1126
|
-
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
|
|
1127
1327
|
enabled,
|
|
1128
1328
|
})
|
|
1129
1329
|
}
|
|
@@ -1253,25 +1453,44 @@ export interface NodeMetrics {
|
|
|
1253
1453
|
}
|
|
1254
1454
|
}
|
|
1255
1455
|
|
|
1456
|
+
async function fetchMetricsOrNull<T>(path: string): Promise<T | null> {
|
|
1457
|
+
try {
|
|
1458
|
+
return await fetchJSON<T>(path)
|
|
1459
|
+
} catch (error) {
|
|
1460
|
+
if (isMetricsUnavailableError(error)) return null
|
|
1461
|
+
throw error
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
function retryMetricsQuery(failureCount: number, error: unknown): boolean {
|
|
1466
|
+
return !isMetricsUnavailableError(error) && failureCount < 1
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1256
1469
|
// Fetch metrics for a specific pod
|
|
1257
|
-
export function usePodMetrics(namespace: string, podName: string) {
|
|
1258
|
-
return useQuery<PodMetrics>({
|
|
1470
|
+
export function usePodMetrics(namespace: string, podName: string, options?: { enabled?: boolean }) {
|
|
1471
|
+
return useQuery<PodMetrics | null>({
|
|
1259
1472
|
queryKey: ['pod-metrics', namespace, podName],
|
|
1260
|
-
queryFn: () =>
|
|
1261
|
-
enabled: Boolean(namespace && podName),
|
|
1262
|
-
staleTime: 15000,
|
|
1263
|
-
refetchInterval: 30000,
|
|
1473
|
+
queryFn: () => fetchMetricsOrNull<PodMetrics>(`/metrics/pods/${namespace}/${podName}`),
|
|
1474
|
+
enabled: Boolean(namespace && podName) && (options?.enabled ?? true),
|
|
1475
|
+
staleTime: 15000,
|
|
1476
|
+
refetchInterval: 30000,
|
|
1477
|
+
refetchOnMount: 'always',
|
|
1478
|
+
refetchOnReconnect: 'always',
|
|
1479
|
+
retry: retryMetricsQuery,
|
|
1264
1480
|
})
|
|
1265
1481
|
}
|
|
1266
1482
|
|
|
1267
1483
|
// Fetch metrics for a specific node
|
|
1268
|
-
export function useNodeMetrics(nodeName: string) {
|
|
1269
|
-
return useQuery<NodeMetrics>({
|
|
1484
|
+
export function useNodeMetrics(nodeName: string, options?: { enabled?: boolean }) {
|
|
1485
|
+
return useQuery<NodeMetrics | null>({
|
|
1270
1486
|
queryKey: ['node-metrics', nodeName],
|
|
1271
|
-
queryFn: () =>
|
|
1272
|
-
enabled: Boolean(nodeName),
|
|
1487
|
+
queryFn: () => fetchMetricsOrNull<NodeMetrics>(`/metrics/nodes/${nodeName}`),
|
|
1488
|
+
enabled: Boolean(nodeName) && (options?.enabled ?? true),
|
|
1273
1489
|
staleTime: 15000,
|
|
1274
1490
|
refetchInterval: 30000,
|
|
1491
|
+
refetchOnMount: 'always',
|
|
1492
|
+
refetchOnReconnect: 'always',
|
|
1493
|
+
retry: retryMetricsQuery,
|
|
1275
1494
|
})
|
|
1276
1495
|
}
|
|
1277
1496
|
|
|
@@ -1295,19 +1514,57 @@ export interface PodMetricsHistory {
|
|
|
1295
1514
|
name: string
|
|
1296
1515
|
containers: ContainerMetricsHistory[]
|
|
1297
1516
|
collectionError?: string
|
|
1517
|
+
rawCollectionError?: string
|
|
1518
|
+
metricsUnavailableDiagnosis?: string
|
|
1519
|
+
metricsUnavailable?: boolean
|
|
1520
|
+
metricsUnavailableReason?: string
|
|
1298
1521
|
}
|
|
1299
1522
|
|
|
1300
1523
|
export interface NodeMetricsHistory {
|
|
1301
1524
|
name: string
|
|
1302
1525
|
dataPoints: MetricsDataPoint[]
|
|
1303
1526
|
collectionError?: string
|
|
1527
|
+
rawCollectionError?: string
|
|
1528
|
+
metricsUnavailableDiagnosis?: string
|
|
1529
|
+
metricsUnavailable?: boolean
|
|
1530
|
+
metricsUnavailableReason?: string
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
function withoutCollectionError<T extends { collectionError?: string; rawCollectionError?: string }>(history: T): T {
|
|
1534
|
+
const next = { ...history }
|
|
1535
|
+
delete next.collectionError
|
|
1536
|
+
delete next.rawCollectionError
|
|
1537
|
+
return next
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
export function normalizePodMetricsHistory(history: PodMetricsHistory): PodMetricsHistory {
|
|
1541
|
+
if (history.metricsUnavailable !== true) return history
|
|
1542
|
+
return { ...withoutCollectionError(history), metricsUnavailable: true, metricsUnavailableReason: history.rawCollectionError || history.collectionError }
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
export function normalizeNodeMetricsHistory(history: NodeMetricsHistory): NodeMetricsHistory {
|
|
1546
|
+
if (history.metricsUnavailable !== true) return history
|
|
1547
|
+
return { ...withoutCollectionError(history), metricsUnavailable: true, metricsUnavailableReason: history.rawCollectionError || history.collectionError }
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
export function shouldFetchLiveMetrics(historySettled: boolean, metricsUnavailable: boolean): boolean {
|
|
1551
|
+
return historySettled && !metricsUnavailable
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
export function isLiveMetricsUnavailable(liveMetricsEnabled: boolean, metrics: unknown): boolean {
|
|
1555
|
+
return liveMetricsEnabled && metrics === null
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
export function getVisibleLiveMetrics<T>(liveMetricsEnabled: boolean, metricsUnavailable: boolean, metrics: T | null | undefined): T | undefined {
|
|
1559
|
+
if (!liveMetricsEnabled || metricsUnavailable) return undefined
|
|
1560
|
+
return metrics ?? undefined
|
|
1304
1561
|
}
|
|
1305
1562
|
|
|
1306
1563
|
// Fetch historical metrics for a pod (last ~1 hour)
|
|
1307
1564
|
export function usePodMetricsHistory(namespace: string, podName: string) {
|
|
1308
1565
|
return useQuery<PodMetricsHistory>({
|
|
1309
1566
|
queryKey: ['pod-metrics-history', namespace, podName],
|
|
1310
|
-
queryFn: () => fetchJSON(`/metrics/pods/${namespace}/${podName}/history`),
|
|
1567
|
+
queryFn: async () => normalizePodMetricsHistory(await fetchJSON<PodMetricsHistory>(`/metrics/pods/${namespace}/${podName}/history`)),
|
|
1311
1568
|
enabled: Boolean(namespace && podName),
|
|
1312
1569
|
staleTime: 25000, // Slightly less than poll interval
|
|
1313
1570
|
refetchInterval: 30000, // Match the backend poll interval
|
|
@@ -1318,7 +1575,7 @@ export function usePodMetricsHistory(namespace: string, podName: string) {
|
|
|
1318
1575
|
export function useNodeMetricsHistory(nodeName: string) {
|
|
1319
1576
|
return useQuery<NodeMetricsHistory>({
|
|
1320
1577
|
queryKey: ['node-metrics-history', nodeName],
|
|
1321
|
-
queryFn: () => fetchJSON(`/metrics/nodes/${nodeName}/history`),
|
|
1578
|
+
queryFn: async () => normalizeNodeMetricsHistory(await fetchJSON<NodeMetricsHistory>(`/metrics/nodes/${nodeName}/history`)),
|
|
1322
1579
|
enabled: Boolean(nodeName),
|
|
1323
1580
|
staleTime: 25000,
|
|
1324
1581
|
refetchInterval: 30000,
|
|
@@ -1555,9 +1812,12 @@ export function useAutoPromConnect(): void {
|
|
|
1555
1812
|
const timeout = window.setTimeout(() => {
|
|
1556
1813
|
// Direct apiFetch (not via the usePrometheusConnect mutation) so the
|
|
1557
1814
|
// meta-driven toast handler stays silent — the user didn't click anything.
|
|
1558
|
-
apiFetch(`${getApiBase()}/prometheus/connect`, { method: 'POST' })
|
|
1559
|
-
.then(resp => {
|
|
1815
|
+
apiFetch(`${getApiBase()}/prometheus/connect?optional=true`, { method: 'POST' })
|
|
1816
|
+
.then(async resp => {
|
|
1560
1817
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
|
1818
|
+
const nextStatus = await resp.json() as PrometheusStatus
|
|
1819
|
+
queryClient.setQueryData(['prometheus-status'], nextStatus)
|
|
1820
|
+
if (!nextStatus.connected) throw new Error(nextStatus.error || 'Prometheus unavailable')
|
|
1561
1821
|
queryClient.invalidateQueries({ queryKey: ['prometheus-status'] })
|
|
1562
1822
|
})
|
|
1563
1823
|
.catch(() => {
|
|
@@ -2054,6 +2314,17 @@ export function useApplyResource() {
|
|
|
2054
2314
|
// CronJob operations
|
|
2055
2315
|
// ============================================================================
|
|
2056
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
|
+
|
|
2057
2328
|
// Trigger a CronJob (create a Job from it)
|
|
2058
2329
|
export function useTriggerCronJob() {
|
|
2059
2330
|
const queryClient = useQueryClient()
|
|
@@ -2073,10 +2344,8 @@ export function useTriggerCronJob() {
|
|
|
2073
2344
|
errorMessage: 'Failed to trigger CronJob',
|
|
2074
2345
|
successMessage: 'CronJob triggered',
|
|
2075
2346
|
},
|
|
2076
|
-
onSuccess: () => {
|
|
2077
|
-
queryClient.
|
|
2078
|
-
queryClient.invalidateQueries({ queryKey: ['resources', 'jobs'] })
|
|
2079
|
-
queryClient.invalidateQueries({ queryKey: ['topology'] })
|
|
2347
|
+
onSuccess: (_, variables) => {
|
|
2348
|
+
invalidateCronJobOperationQueries(queryClient, variables.namespace, variables.name)
|
|
2080
2349
|
},
|
|
2081
2350
|
})
|
|
2082
2351
|
}
|
|
@@ -2100,9 +2369,8 @@ export function useSuspendCronJob() {
|
|
|
2100
2369
|
errorMessage: 'Failed to suspend CronJob',
|
|
2101
2370
|
successMessage: 'CronJob suspended',
|
|
2102
2371
|
},
|
|
2103
|
-
onSuccess: () => {
|
|
2104
|
-
queryClient.
|
|
2105
|
-
queryClient.invalidateQueries({ queryKey: ['topology'] })
|
|
2372
|
+
onSuccess: (_, variables) => {
|
|
2373
|
+
invalidateCronJobOperationQueries(queryClient, variables.namespace, variables.name)
|
|
2106
2374
|
},
|
|
2107
2375
|
})
|
|
2108
2376
|
}
|
|
@@ -2126,9 +2394,8 @@ export function useResumeCronJob() {
|
|
|
2126
2394
|
errorMessage: 'Failed to resume CronJob',
|
|
2127
2395
|
successMessage: 'CronJob resumed',
|
|
2128
2396
|
},
|
|
2129
|
-
onSuccess: () => {
|
|
2130
|
-
queryClient.
|
|
2131
|
-
queryClient.invalidateQueries({ queryKey: ['topology'] })
|
|
2397
|
+
onSuccess: (_, variables) => {
|
|
2398
|
+
invalidateCronJobOperationQueries(queryClient, variables.namespace, variables.name)
|
|
2132
2399
|
},
|
|
2133
2400
|
})
|
|
2134
2401
|
}
|
|
@@ -2361,11 +2628,11 @@ export function useHelmReleases(namespaces: string[] = []) {
|
|
|
2361
2628
|
}
|
|
2362
2629
|
|
|
2363
2630
|
// Get details for a specific Helm release
|
|
2364
|
-
export function useHelmRelease(namespace: string, name: string) {
|
|
2631
|
+
export function useHelmRelease(namespace: string, name: string, options?: { enabled?: boolean }) {
|
|
2365
2632
|
return useQuery<HelmReleaseDetail>({
|
|
2366
2633
|
queryKey: ['helm-release', namespace, name],
|
|
2367
2634
|
queryFn: () => fetchJSON(`/helm/releases/${namespace}/${name}`),
|
|
2368
|
-
enabled: Boolean(namespace && name),
|
|
2635
|
+
enabled: Boolean(namespace && name) && (options?.enabled ?? true),
|
|
2369
2636
|
staleTime: 5000,
|
|
2370
2637
|
refetchInterval: 10000, // Poll for live resource status updates (post-upgrade/rollback)
|
|
2371
2638
|
})
|
|
@@ -2652,19 +2919,24 @@ function streamHelmProgress(
|
|
|
2652
2919
|
})
|
|
2653
2920
|
}
|
|
2654
2921
|
|
|
2655
|
-
//
|
|
2922
|
+
// When `values` is provided, the upgrade applies exactly those edited values
|
|
2923
|
+
// instead of carrying the release's prior values over blindly.
|
|
2656
2924
|
export function upgradeWithProgress(
|
|
2657
2925
|
namespace: string,
|
|
2658
2926
|
name: string,
|
|
2659
2927
|
version: string,
|
|
2660
2928
|
repositoryName: string | undefined,
|
|
2661
|
-
onProgress: (event: InstallProgressEvent) => void
|
|
2929
|
+
onProgress: (event: InstallProgressEvent) => void,
|
|
2930
|
+
values?: Record<string, unknown>
|
|
2662
2931
|
): Promise<void> {
|
|
2663
2932
|
const params = new URLSearchParams({ version })
|
|
2664
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' }
|
|
2665
2937
|
return streamHelmProgress(
|
|
2666
2938
|
`${getApiBase()}/helm/releases/${namespace}/${name}/upgrade-stream?${params.toString()}`,
|
|
2667
|
-
|
|
2939
|
+
options,
|
|
2668
2940
|
onProgress,
|
|
2669
2941
|
'Upgrade failed',
|
|
2670
2942
|
).then(() => {})
|
|
@@ -2685,14 +2957,15 @@ export function rollbackWithProgress(
|
|
|
2685
2957
|
).then(() => {})
|
|
2686
2958
|
}
|
|
2687
2959
|
|
|
2688
|
-
//
|
|
2960
|
+
// When `version` is supplied, preview renders against that target chart version
|
|
2961
|
+
// instead of the release's current chart.
|
|
2689
2962
|
export function useHelmPreviewValues() {
|
|
2690
|
-
return useMutation<ValuesPreviewResponse, Error, { namespace: string; name: string; values: Record<string, unknown
|
|
2691
|
-
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 }) => {
|
|
2692
2965
|
const response = await apiFetch(`${getApiBase()}/helm/releases/${namespace}/${name}/values/preview`, {
|
|
2693
2966
|
method: 'POST',
|
|
2694
2967
|
headers: { 'Content-Type': 'application/json' },
|
|
2695
|
-
body: JSON.stringify({ values }),
|
|
2968
|
+
body: JSON.stringify({ values, version, repository }),
|
|
2696
2969
|
})
|
|
2697
2970
|
if (!response.ok) {
|
|
2698
2971
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
@@ -3443,6 +3716,44 @@ export interface WorkloadLogsResponse {
|
|
|
3443
3716
|
timestamp: string
|
|
3444
3717
|
content: string
|
|
3445
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[]
|
|
3446
3757
|
}
|
|
3447
3758
|
|
|
3448
3759
|
// Fetch pods for a workload
|
|
@@ -3455,6 +3766,24 @@ export function useWorkloadPods(kind: string, namespace: string, name: string) {
|
|
|
3455
3766
|
})
|
|
3456
3767
|
}
|
|
3457
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
|
+
|
|
3458
3787
|
// Fetch logs for a workload (non-streaming)
|
|
3459
3788
|
export function useWorkloadLogs(
|
|
3460
3789
|
kind: string,
|