@skyhook-io/radar-app 1.8.7 → 1.8.9
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/package.json +4 -4
- package/src/App.tsx +69 -61
- package/src/RadarApp.tsx +15 -1
- package/src/api/apiResources.ts +1 -1
- package/src/api/client.argoResourceSync.test.ts +69 -0
- package/src/api/client.rightsizing.test.ts +32 -0
- package/src/api/client.ts +1222 -244
- package/src/api/timelineSource.ts +4 -2
- package/src/components/applications/ApplicationsView.tsx +613 -219
- package/src/components/cost/ApplicationCostTab.test.ts +204 -0
- package/src/components/cost/ApplicationCostTab.tsx +571 -0
- package/src/components/cost/CostTrendChart.tsx +103 -72
- package/src/components/cost/CostView.test.ts +12 -0
- package/src/components/cost/CostView.tsx +494 -229
- package/src/components/cost/CostViewTabs.test.tsx +21 -0
- package/src/components/cost/CostViewTabs.tsx +40 -0
- package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
- package/src/components/cost/CurrentAllocationUse.tsx +126 -0
- package/src/components/cost/WorkloadCostTab.test.ts +153 -0
- package/src/components/cost/WorkloadCostTab.tsx +372 -0
- package/src/components/cost/cloud-console.test.ts +39 -0
- package/src/components/cost/cloud-console.ts +81 -0
- package/src/components/cost/errors.ts +8 -0
- package/src/components/cost/format.test.ts +27 -0
- package/src/components/cost/format.ts +46 -0
- package/src/components/cost/kinds.ts +5 -0
- package/src/components/diagnose/AISettings.tsx +7 -12
- package/src/components/diagnose/DiagnoseContext.tsx +1 -0
- package/src/components/diagnose/DiagnoseSurface.tsx +3 -0
- package/src/components/diagnose/InvestigationView.tsx +16 -3
- package/src/components/diagnose/parts.tsx +140 -73
- package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
- package/src/components/gitops/GitOpsView.tsx +81 -14
- package/src/components/gitops/RevisionMetaChip.tsx +63 -0
- package/src/components/helm/HelmCompareRoute.tsx +1 -2
- package/src/components/helm/ManifestDiffViewer.tsx +1 -31
- package/src/components/helm/ValuesDiffPreview.tsx +2 -3
- package/src/components/home/CostCard.tsx +21 -36
- package/src/components/resource/RightsizingStrip.test.ts +109 -0
- package/src/components/resource/RightsizingStrip.tsx +319 -123
- package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
- package/src/components/rightsizing/copy.test.ts +56 -0
- package/src/components/rightsizing/model.test.ts +227 -0
- package/src/components/rightsizing/model.ts +158 -0
- package/src/components/rightsizing/presentation.test.ts +104 -0
- package/src/components/rightsizing/presentation.ts +94 -0
- package/src/components/settings/MyPermissionsDialog.tsx +66 -116
- package/src/components/settings/SettingsDialog.tsx +1268 -318
- package/src/components/timeline/TimelineList.tsx +35 -8
- package/src/components/timeline/TimelineView.tsx +156 -26
- package/src/components/timeline/TimelineView.urlparams.test.ts +43 -2
- package/src/components/workload/WorkloadView.tsx +711 -328
- package/src/context/DiagnoseCustomization.tsx +36 -2
- package/src/index.css +5 -1
- package/src/index.ts +4 -1
- package/src/main.tsx +1 -1
package/src/api/client.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'react'
|
|
2
|
-
import type { AppHistory, AppRow } from '@skyhook-io/k8s-ui'
|
|
2
|
+
import type { AppHistory, AppRow, ArgoSyncOpts } 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'
|
|
@@ -31,6 +31,9 @@ import type {
|
|
|
31
31
|
ArtifactHubChartDetail,
|
|
32
32
|
GitOpsResourceTree,
|
|
33
33
|
GitOpsInsight,
|
|
34
|
+
GitOpsInsightRef,
|
|
35
|
+
GitOpsResourceDiff,
|
|
36
|
+
ArgoRevisionMetadata,
|
|
34
37
|
} from '../types'
|
|
35
38
|
import type { GitOpsOperationResponse } from '../types/gitops'
|
|
36
39
|
import { getApiBase, getAuthHeaders, getCredentialsMode, getBasename, routePath } from './config'
|
|
@@ -42,6 +45,9 @@ const DASHBOARD_REFRESH_INTERVAL_MS = 30_000
|
|
|
42
45
|
const AUDIT_REFRESH_INTERVAL_MS = 60_000
|
|
43
46
|
const ISSUES_REFRESH_INTERVAL_MS = 30_000
|
|
44
47
|
const COST_REFRESH_INTERVAL_MS = 60_000
|
|
48
|
+
const COST_DISCOVERY_RETRY_INTERVAL_MS = 5_000
|
|
49
|
+
export const COST_DISCOVERY_GRACE_MS = 30_000
|
|
50
|
+
const COST_TREND_REFRESH_INTERVAL_MS = 120_000
|
|
45
51
|
const CHANGES_REFRESH_INTERVAL_MS = 60_000
|
|
46
52
|
const APPLICATIONS_REFRESH_INTERVAL_MS = 60_000
|
|
47
53
|
|
|
@@ -54,12 +60,23 @@ export function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise<
|
|
|
54
60
|
for (const [k, v] of Object.entries(getAuthHeaders())) {
|
|
55
61
|
if (!headers.has(k)) headers.set(k, v)
|
|
56
62
|
}
|
|
57
|
-
return fetch(input, {
|
|
63
|
+
return fetch(input, {
|
|
64
|
+
credentials: getCredentialsMode(),
|
|
65
|
+
...init,
|
|
66
|
+
headers,
|
|
67
|
+
}).then(async (response) => {
|
|
58
68
|
const authPrefix = `${getBasename()}/auth`
|
|
59
69
|
if (response.status === 401 && !window.location.pathname.startsWith(authPrefix)) {
|
|
60
70
|
// Save current location so user returns to where they were after re-auth.
|
|
61
71
|
// Editor draft is auto-saved by EditableYamlView via sessionStorage.
|
|
62
|
-
try {
|
|
72
|
+
try {
|
|
73
|
+
sessionStorage.setItem(
|
|
74
|
+
'radar_return_path',
|
|
75
|
+
window.location.pathname + window.location.search,
|
|
76
|
+
)
|
|
77
|
+
} catch {
|
|
78
|
+
/* best-effort */
|
|
79
|
+
}
|
|
63
80
|
|
|
64
81
|
let authMode: string | undefined
|
|
65
82
|
try {
|
|
@@ -78,7 +95,11 @@ export function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise<
|
|
|
78
95
|
const lastReload = sessionStorage.getItem('radar_proxy_reload')
|
|
79
96
|
const now = Date.now()
|
|
80
97
|
if (!lastReload || now - parseInt(lastReload) > 5000) {
|
|
81
|
-
try {
|
|
98
|
+
try {
|
|
99
|
+
sessionStorage.setItem('radar_proxy_reload', String(now))
|
|
100
|
+
} catch {
|
|
101
|
+
/* best-effort */
|
|
102
|
+
}
|
|
82
103
|
window.location.reload()
|
|
83
104
|
}
|
|
84
105
|
}
|
|
@@ -107,11 +128,12 @@ const METRICS_API_GROUP_TOKENS = ['metrics', 'k8s', 'io'] as const
|
|
|
107
128
|
|
|
108
129
|
function mentionsMetricsAPIGroup(message: string): boolean {
|
|
109
130
|
const tokens = message.split(/[^a-z0-9]+/).filter(Boolean)
|
|
110
|
-
return tokens.some(
|
|
111
|
-
token
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
131
|
+
return tokens.some(
|
|
132
|
+
(token, index) =>
|
|
133
|
+
token === METRICS_API_GROUP_TOKENS[0] &&
|
|
134
|
+
tokens[index + 1] === METRICS_API_GROUP_TOKENS[1] &&
|
|
135
|
+
tokens[index + 2] === METRICS_API_GROUP_TOKENS[2],
|
|
136
|
+
)
|
|
115
137
|
}
|
|
116
138
|
|
|
117
139
|
function hasMetricsUnavailablePhrase(message: string): boolean {
|
|
@@ -134,18 +156,18 @@ export function isMetricsUnavailableError(error: unknown): boolean {
|
|
|
134
156
|
return [error.message, error.data?.error].some((message) => {
|
|
135
157
|
if (typeof message !== 'string') return false
|
|
136
158
|
const normalized = message.toLowerCase()
|
|
137
|
-
const hasMetricsSignal =
|
|
159
|
+
const hasMetricsSignal =
|
|
138
160
|
normalized.includes('metrics-server') ||
|
|
139
161
|
mentionsMetricsAPIGroup(normalized) ||
|
|
140
162
|
normalized.includes('pod metrics') ||
|
|
141
163
|
normalized.includes('node metrics')
|
|
142
|
-
)
|
|
143
164
|
return hasMetricsSignal && hasMetricsUnavailablePhrase(normalized)
|
|
144
165
|
})
|
|
145
166
|
}
|
|
146
167
|
|
|
147
|
-
export async function fetchJSON<T>(path: string,
|
|
148
|
-
const
|
|
168
|
+
export async function fetchJSON<T>(path: string, init?: RequestInit | AbortSignal): Promise<T> {
|
|
169
|
+
const requestInit = init instanceof AbortSignal ? { signal: init } : init
|
|
170
|
+
const response = await apiFetch(`${getApiBase()}${path}`, requestInit)
|
|
149
171
|
if (!response.ok) {
|
|
150
172
|
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
151
173
|
throw new ApiError(errorData.error || `HTTP ${response.status}`, response.status, errorData)
|
|
@@ -209,7 +231,13 @@ export interface MetricSummary {
|
|
|
209
231
|
}
|
|
210
232
|
|
|
211
233
|
export interface DashboardResourceCounts {
|
|
212
|
-
pods: {
|
|
234
|
+
pods: {
|
|
235
|
+
total: number
|
|
236
|
+
running: number
|
|
237
|
+
pending: number
|
|
238
|
+
failed: number
|
|
239
|
+
succeeded: number
|
|
240
|
+
}
|
|
213
241
|
deployments: { total: number; available: number; unavailable: number }
|
|
214
242
|
statefulSets: WorkloadCount
|
|
215
243
|
daemonSets: WorkloadCount
|
|
@@ -227,15 +255,6 @@ export interface DashboardResourceCounts {
|
|
|
227
255
|
restricted?: string[] // Resource kinds the user cannot list due to RBAC
|
|
228
256
|
}
|
|
229
257
|
|
|
230
|
-
export interface DashboardEvent {
|
|
231
|
-
type: string
|
|
232
|
-
reason: string
|
|
233
|
-
message: string
|
|
234
|
-
involvedObject: string
|
|
235
|
-
namespace: string
|
|
236
|
-
timestamp: string
|
|
237
|
-
}
|
|
238
|
-
|
|
239
258
|
export interface DashboardChange {
|
|
240
259
|
kind: string
|
|
241
260
|
namespace: string
|
|
@@ -292,7 +311,15 @@ export interface DashboardCRDCount {
|
|
|
292
311
|
}
|
|
293
312
|
|
|
294
313
|
// Re-export shared types from k8s-ui — single source of truth
|
|
295
|
-
import type {
|
|
314
|
+
import type {
|
|
315
|
+
AuditCardData,
|
|
316
|
+
AuditFinding,
|
|
317
|
+
ResourceGroup,
|
|
318
|
+
CheckMeta,
|
|
319
|
+
Check,
|
|
320
|
+
Issue,
|
|
321
|
+
IssueRecentChange,
|
|
322
|
+
} from '@skyhook-io/k8s-ui'
|
|
296
323
|
export type DashboardAudit = AuditCardData
|
|
297
324
|
export type { AuditFinding, ResourceGroup, CheckMeta, Check }
|
|
298
325
|
|
|
@@ -351,7 +378,6 @@ export interface DashboardResponse {
|
|
|
351
378
|
health: DashboardHealth
|
|
352
379
|
problems: DashboardProblem[]
|
|
353
380
|
resourceCounts: DashboardResourceCounts
|
|
354
|
-
recentEvents: DashboardEvent[]
|
|
355
381
|
recentChanges: DashboardChange[]
|
|
356
382
|
topologySummary: DashboardTopologySummary
|
|
357
383
|
trafficSummary: DashboardTrafficSummary | null
|
|
@@ -361,7 +387,11 @@ export interface DashboardResponse {
|
|
|
361
387
|
networkPolicyCoverage: DashboardNetworkPolicyCoverage | null
|
|
362
388
|
audit: DashboardAudit | null
|
|
363
389
|
gitopsControllers: DashboardGitOpsControllers | null
|
|
364
|
-
nodeVersionSkew: {
|
|
390
|
+
nodeVersionSkew: {
|
|
391
|
+
versions: Record<string, string[]>
|
|
392
|
+
minVersion: string
|
|
393
|
+
maxVersion: string
|
|
394
|
+
} | null
|
|
365
395
|
deferredLoading?: boolean // True while deferred informers (secrets, events, etc.) are still syncing
|
|
366
396
|
partialData?: string[] // Critical kinds promoted at first paint that haven't yet finished syncing (live-filtered)
|
|
367
397
|
accessRestricted?: boolean // True when user has no namespace access (RBAC)
|
|
@@ -437,7 +467,13 @@ export function useResourceAudit(kind: string, namespace: string, name: string)
|
|
|
437
467
|
// the "Operational Issues" section in the resource detail. Cluster-scoped
|
|
438
468
|
// resources pass "_" for namespace; namespaced ones also scope the scan via
|
|
439
469
|
// ?namespaces= for a cheap, bounded Compose.
|
|
440
|
-
export function useResourceIssues(
|
|
470
|
+
export function useResourceIssues(
|
|
471
|
+
kind: string,
|
|
472
|
+
group: string | undefined,
|
|
473
|
+
namespace: string,
|
|
474
|
+
name: string,
|
|
475
|
+
enabled = true,
|
|
476
|
+
) {
|
|
441
477
|
const clusterScoped = !namespace
|
|
442
478
|
const pathNs = clusterScoped ? '_' : encodeURIComponent(namespace)
|
|
443
479
|
const params = new URLSearchParams()
|
|
@@ -550,7 +586,8 @@ export interface OpenCostNamespaceCost {
|
|
|
550
586
|
idleCost?: number
|
|
551
587
|
}
|
|
552
588
|
|
|
553
|
-
export type CostUnavailableReason =
|
|
589
|
+
export type CostUnavailableReason =
|
|
590
|
+
'no_prometheus' | 'no_metrics' | 'query_error' | 'access_denied' | 'not_found'
|
|
554
591
|
|
|
555
592
|
export interface OpenCostSummary {
|
|
556
593
|
available: boolean
|
|
@@ -564,11 +601,41 @@ export interface OpenCostSummary {
|
|
|
564
601
|
namespaces?: OpenCostNamespaceCost[]
|
|
565
602
|
}
|
|
566
603
|
|
|
604
|
+
const noPrometheusFirstSeenAt = new Map<string, number>()
|
|
605
|
+
|
|
606
|
+
function costRefetchInterval(
|
|
607
|
+
defaultInterval: number | false = COST_REFRESH_INTERVAL_MS,
|
|
608
|
+
contextName?: string,
|
|
609
|
+
) {
|
|
610
|
+
return (query: {
|
|
611
|
+
queryHash?: string
|
|
612
|
+
queryKey?: unknown
|
|
613
|
+
state: {
|
|
614
|
+
data?: { available?: boolean; reason?: CostUnavailableReason }
|
|
615
|
+
dataUpdatedAt?: number
|
|
616
|
+
}
|
|
617
|
+
}) => {
|
|
618
|
+
const data = query.state.data
|
|
619
|
+
const queryID = `${contextName ?? 'unknown'}:${query.queryHash ?? JSON.stringify(query.queryKey ?? 'opencost')}`
|
|
620
|
+
if (data?.available === false && data.reason === 'no_prometheus') {
|
|
621
|
+
const now = Date.now()
|
|
622
|
+
const firstSeenAt = noPrometheusFirstSeenAt.get(queryID) ?? now
|
|
623
|
+
noPrometheusFirstSeenAt.set(queryID, firstSeenAt)
|
|
624
|
+
return now - firstSeenAt < COST_DISCOVERY_GRACE_MS
|
|
625
|
+
? COST_DISCOVERY_RETRY_INTERVAL_MS
|
|
626
|
+
: defaultInterval
|
|
627
|
+
}
|
|
628
|
+
noPrometheusFirstSeenAt.delete(queryID)
|
|
629
|
+
return defaultInterval
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
567
633
|
export function useOpenCostSummary() {
|
|
634
|
+
const clusterInfo = useClusterInfo()
|
|
568
635
|
return useQuery<OpenCostSummary>({
|
|
569
636
|
queryKey: ['opencost-summary'],
|
|
570
637
|
queryFn: () => fetchJSON('/opencost/summary'),
|
|
571
|
-
refetchInterval: COST_REFRESH_INTERVAL_MS,
|
|
638
|
+
refetchInterval: costRefetchInterval(COST_REFRESH_INTERVAL_MS, clusterInfo.data?.context),
|
|
572
639
|
staleTime: 30000,
|
|
573
640
|
placeholderData: (prev) => prev, // Keep previous data visible during refetch
|
|
574
641
|
})
|
|
@@ -584,6 +651,10 @@ export interface OpenCostWorkloadCost {
|
|
|
584
651
|
replicas: number
|
|
585
652
|
cpuUsageCost?: number
|
|
586
653
|
memoryUsageCost?: number
|
|
654
|
+
cpuUsageAvailable: boolean
|
|
655
|
+
memoryUsageAvailable: boolean
|
|
656
|
+
cpuAllocationUse: number
|
|
657
|
+
memoryAllocationUse: number
|
|
587
658
|
efficiency?: number
|
|
588
659
|
idleCost?: number
|
|
589
660
|
}
|
|
@@ -596,14 +667,44 @@ export interface OpenCostWorkloadResponse {
|
|
|
596
667
|
}
|
|
597
668
|
|
|
598
669
|
export function useOpenCostWorkloads(namespace: string, options?: { enabled?: boolean }) {
|
|
670
|
+
const clusterInfo = useClusterInfo()
|
|
599
671
|
return useQuery<OpenCostWorkloadResponse>({
|
|
600
672
|
queryKey: ['opencost-workloads', namespace],
|
|
601
673
|
queryFn: () => fetchJSON(`/opencost/workloads?namespace=${encodeURIComponent(namespace)}`),
|
|
602
674
|
enabled: (options?.enabled ?? true) && Boolean(namespace),
|
|
675
|
+
refetchInterval: costRefetchInterval(COST_REFRESH_INTERVAL_MS, clusterInfo.data?.context),
|
|
603
676
|
staleTime: 30000,
|
|
604
677
|
})
|
|
605
678
|
}
|
|
606
679
|
|
|
680
|
+
export interface OpenCostWorkloadDetailResponse {
|
|
681
|
+
available: boolean
|
|
682
|
+
reason?: CostUnavailableReason
|
|
683
|
+
namespace: string
|
|
684
|
+
kind: string
|
|
685
|
+
name: string
|
|
686
|
+
current?: OpenCostWorkloadCost
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
export function useOpenCostWorkload(
|
|
690
|
+
kind: string,
|
|
691
|
+
namespace: string,
|
|
692
|
+
name: string,
|
|
693
|
+
options?: { enabled?: boolean },
|
|
694
|
+
) {
|
|
695
|
+
const clusterInfo = useClusterInfo()
|
|
696
|
+
return useQuery<OpenCostWorkloadDetailResponse>({
|
|
697
|
+
queryKey: ['opencost-workload', kind, namespace, name],
|
|
698
|
+
queryFn: () =>
|
|
699
|
+
fetchJSON(
|
|
700
|
+
`/opencost/workload/${encodeURIComponent(kind)}/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`,
|
|
701
|
+
),
|
|
702
|
+
enabled: (options?.enabled ?? true) && Boolean(kind && namespace && name),
|
|
703
|
+
staleTime: 30000,
|
|
704
|
+
refetchInterval: costRefetchInterval(COST_REFRESH_INTERVAL_MS, clusterInfo.data?.context),
|
|
705
|
+
})
|
|
706
|
+
}
|
|
707
|
+
|
|
607
708
|
// Cost trend over time
|
|
608
709
|
export type CostTimeRange = '6h' | '24h' | '7d'
|
|
609
710
|
|
|
@@ -625,18 +726,175 @@ export interface OpenCostTrendResponse {
|
|
|
625
726
|
}
|
|
626
727
|
|
|
627
728
|
export function useOpenCostTrend(range_: CostTimeRange = '24h') {
|
|
729
|
+
const clusterInfo = useClusterInfo()
|
|
628
730
|
return useQuery<OpenCostTrendResponse>({
|
|
629
731
|
queryKey: ['opencost-trend', range_],
|
|
630
732
|
queryFn: () => fetchJSON(`/opencost/trend?range=${range_}`),
|
|
631
733
|
staleTime: 60000,
|
|
632
|
-
refetchInterval:
|
|
734
|
+
refetchInterval: costRefetchInterval(COST_TREND_REFRESH_INTERVAL_MS, clusterInfo.data?.context),
|
|
633
735
|
placeholderData: (prev) => prev,
|
|
634
736
|
})
|
|
635
737
|
}
|
|
636
738
|
|
|
739
|
+
export interface OpenCostWorkloadTrendResponse {
|
|
740
|
+
available: boolean
|
|
741
|
+
reason?: CostUnavailableReason
|
|
742
|
+
namespace: string
|
|
743
|
+
kind: string
|
|
744
|
+
name: string
|
|
745
|
+
range: string
|
|
746
|
+
windowTotalCost?: number
|
|
747
|
+
dataPoints?: OpenCostTrendDataPoint[]
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
export function useOpenCostWorkloadTrend(
|
|
751
|
+
kind: string,
|
|
752
|
+
namespace: string,
|
|
753
|
+
name: string,
|
|
754
|
+
range_: CostTimeRange = '24h',
|
|
755
|
+
options?: { enabled?: boolean },
|
|
756
|
+
) {
|
|
757
|
+
const clusterInfo = useClusterInfo()
|
|
758
|
+
return useQuery<OpenCostWorkloadTrendResponse>({
|
|
759
|
+
queryKey: ['opencost-workload-trend', kind, namespace, name, range_],
|
|
760
|
+
queryFn: () =>
|
|
761
|
+
fetchJSON(
|
|
762
|
+
`/opencost/workload/${encodeURIComponent(kind)}/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/trend?range=${range_}`,
|
|
763
|
+
),
|
|
764
|
+
enabled: (options?.enabled ?? true) && Boolean(kind && namespace && name),
|
|
765
|
+
staleTime: 60000,
|
|
766
|
+
refetchInterval: costRefetchInterval(COST_TREND_REFRESH_INTERVAL_MS, clusterInfo.data?.context),
|
|
767
|
+
})
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
export interface OpenCostApplicationWorkloadRef {
|
|
771
|
+
kind: string
|
|
772
|
+
namespace: string
|
|
773
|
+
name: string
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
export interface OpenCostApplicationWorkloadStatus extends OpenCostApplicationWorkloadRef {
|
|
777
|
+
reason: CostUnavailableReason
|
|
778
|
+
scaledToZero?: boolean
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
export interface OpenCostApplicationCostCoverage {
|
|
782
|
+
total: number
|
|
783
|
+
included: number
|
|
784
|
+
unavailable?: OpenCostApplicationWorkloadStatus[]
|
|
785
|
+
unsupported?: OpenCostApplicationWorkloadRef[]
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
export interface OpenCostApplicationCostTotals {
|
|
789
|
+
hourlyCost: number
|
|
790
|
+
cpuCost: number
|
|
791
|
+
memoryCost: number
|
|
792
|
+
replicas: number
|
|
793
|
+
cpuUsageCost?: number
|
|
794
|
+
memoryUsageCost?: number
|
|
795
|
+
cpuUsageAvailable: boolean
|
|
796
|
+
memoryUsageAvailable: boolean
|
|
797
|
+
cpuAllocationUse: number
|
|
798
|
+
memoryAllocationUse: number
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
export interface OpenCostApplicationWorkloadCost extends OpenCostApplicationWorkloadRef {
|
|
802
|
+
available: boolean
|
|
803
|
+
reason?: CostUnavailableReason
|
|
804
|
+
scaledToZero?: boolean
|
|
805
|
+
current?: OpenCostWorkloadCost
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
export interface OpenCostApplicationCostResponse {
|
|
809
|
+
available: boolean
|
|
810
|
+
reason?: CostUnavailableReason
|
|
811
|
+
partial?: boolean
|
|
812
|
+
totals: OpenCostApplicationCostTotals
|
|
813
|
+
coverage: OpenCostApplicationCostCoverage
|
|
814
|
+
workloads?: OpenCostApplicationWorkloadCost[]
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
export interface OpenCostApplicationCostTrendSeries extends OpenCostApplicationWorkloadRef {
|
|
818
|
+
windowTotalCost?: number
|
|
819
|
+
dataPoints?: OpenCostTrendDataPoint[]
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
export interface OpenCostApplicationCostTrendResponse {
|
|
823
|
+
available: boolean
|
|
824
|
+
reason?: CostUnavailableReason
|
|
825
|
+
range: string
|
|
826
|
+
partial?: boolean
|
|
827
|
+
windowTotalCost?: number
|
|
828
|
+
dataPoints?: OpenCostTrendDataPoint[]
|
|
829
|
+
series?: OpenCostApplicationCostTrendSeries[]
|
|
830
|
+
coverage: OpenCostApplicationCostCoverage
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function stableOpenCostWorkloadRefs(
|
|
834
|
+
workloads: OpenCostApplicationWorkloadRef[],
|
|
835
|
+
): OpenCostApplicationWorkloadRef[] {
|
|
836
|
+
const byKey = new Map<string, OpenCostApplicationWorkloadRef>()
|
|
837
|
+
for (const workload of workloads) {
|
|
838
|
+
if (!workload.kind || !workload.namespace || !workload.name) continue
|
|
839
|
+
const ref = {
|
|
840
|
+
kind: workload.kind,
|
|
841
|
+
namespace: workload.namespace,
|
|
842
|
+
name: workload.name,
|
|
843
|
+
}
|
|
844
|
+
byKey.set(`${ref.namespace}/${ref.kind}/${ref.name}`, ref)
|
|
845
|
+
}
|
|
846
|
+
return [...byKey.values()].sort((a, b) =>
|
|
847
|
+
`${a.namespace}/${a.kind}/${a.name}`.localeCompare(`${b.namespace}/${b.kind}/${b.name}`),
|
|
848
|
+
)
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
export function useOpenCostApplicationCost(
|
|
852
|
+
workloads: OpenCostApplicationWorkloadRef[],
|
|
853
|
+
options?: { enabled?: boolean },
|
|
854
|
+
) {
|
|
855
|
+
const clusterInfo = useClusterInfo()
|
|
856
|
+
const refs = stableOpenCostWorkloadRefs(workloads)
|
|
857
|
+
return useQuery<OpenCostApplicationCostResponse>({
|
|
858
|
+
queryKey: ['opencost-application', refs],
|
|
859
|
+
queryFn: ({ signal }) =>
|
|
860
|
+
fetchJSON('/opencost/application', {
|
|
861
|
+
method: 'POST',
|
|
862
|
+
headers: { 'Content-Type': 'application/json' },
|
|
863
|
+
body: JSON.stringify({ workloads: refs }),
|
|
864
|
+
signal,
|
|
865
|
+
}),
|
|
866
|
+
enabled: (options?.enabled ?? true) && refs.length > 0,
|
|
867
|
+
staleTime: 30000,
|
|
868
|
+
refetchInterval: costRefetchInterval(COST_REFRESH_INTERVAL_MS, clusterInfo.data?.context),
|
|
869
|
+
})
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
export function useOpenCostApplicationCostTrend(
|
|
873
|
+
workloads: OpenCostApplicationWorkloadRef[],
|
|
874
|
+
range_: CostTimeRange = '24h',
|
|
875
|
+
options?: { enabled?: boolean },
|
|
876
|
+
) {
|
|
877
|
+
const clusterInfo = useClusterInfo()
|
|
878
|
+
const refs = stableOpenCostWorkloadRefs(workloads)
|
|
879
|
+
return useQuery<OpenCostApplicationCostTrendResponse>({
|
|
880
|
+
queryKey: ['opencost-application-trend', refs, range_],
|
|
881
|
+
queryFn: ({ signal }) =>
|
|
882
|
+
fetchJSON('/opencost/application/trend', {
|
|
883
|
+
method: 'POST',
|
|
884
|
+
headers: { 'Content-Type': 'application/json' },
|
|
885
|
+
body: JSON.stringify({ workloads: refs, range: range_ }),
|
|
886
|
+
signal,
|
|
887
|
+
}),
|
|
888
|
+
enabled: (options?.enabled ?? true) && refs.length > 0,
|
|
889
|
+
staleTime: 60000,
|
|
890
|
+
refetchInterval: costRefetchInterval(COST_TREND_REFRESH_INTERVAL_MS, clusterInfo.data?.context),
|
|
891
|
+
})
|
|
892
|
+
}
|
|
893
|
+
|
|
637
894
|
// Node cost breakdown
|
|
638
895
|
export interface OpenCostNodeCost {
|
|
639
896
|
name: string
|
|
897
|
+
providerID?: string
|
|
640
898
|
instanceType?: string
|
|
641
899
|
region?: string
|
|
642
900
|
hourlyCost: number
|
|
@@ -651,11 +909,12 @@ export interface OpenCostNodeResponse {
|
|
|
651
909
|
}
|
|
652
910
|
|
|
653
911
|
export function useOpenCostNodes() {
|
|
912
|
+
const clusterInfo = useClusterInfo()
|
|
654
913
|
return useQuery<OpenCostNodeResponse>({
|
|
655
914
|
queryKey: ['opencost-nodes'],
|
|
656
915
|
queryFn: () => fetchJSON('/opencost/nodes'),
|
|
657
916
|
staleTime: 60000,
|
|
658
|
-
refetchInterval:
|
|
917
|
+
refetchInterval: costRefetchInterval(COST_TREND_REFRESH_INTERVAL_MS, clusterInfo.data?.context),
|
|
659
918
|
placeholderData: (prev) => prev,
|
|
660
919
|
})
|
|
661
920
|
}
|
|
@@ -816,7 +1075,15 @@ const SEARCH_MIN_QUERY = 2
|
|
|
816
1075
|
// health/issueCount per hit (rich rows). React Query's AbortSignal cancels
|
|
817
1076
|
// overlapping scans on a new query. keepPreviousData avoids flicker while the
|
|
818
1077
|
// next query resolves.
|
|
819
|
-
export function useSearch(
|
|
1078
|
+
export function useSearch(
|
|
1079
|
+
query: string,
|
|
1080
|
+
opts?: {
|
|
1081
|
+
limit?: number
|
|
1082
|
+
context?: 'summary' | 'none'
|
|
1083
|
+
enabled?: boolean
|
|
1084
|
+
globalNs?: boolean
|
|
1085
|
+
},
|
|
1086
|
+
) {
|
|
820
1087
|
const trimmed = query.trim()
|
|
821
1088
|
const enabled = (opts?.enabled ?? true) && trimmed.length >= SEARCH_MIN_QUERY
|
|
822
1089
|
const limit = opts?.limit ?? 20
|
|
@@ -828,7 +1095,10 @@ export function useSearch(query: string, opts?: { limit?: number; context?: 'sum
|
|
|
828
1095
|
return useQuery<SearchResult>({
|
|
829
1096
|
queryKey: ['search', trimmed, limit, context, globalNs],
|
|
830
1097
|
queryFn: ({ signal }) =>
|
|
831
|
-
fetchJSON<SearchResult>(
|
|
1098
|
+
fetchJSON<SearchResult>(
|
|
1099
|
+
`/search?q=${encodeURIComponent(trimmed)}&limit=${limit}&include=none&context=${context}${globalNs ? '&globalNs=1' : ''}`,
|
|
1100
|
+
signal,
|
|
1101
|
+
),
|
|
832
1102
|
enabled,
|
|
833
1103
|
staleTime: 2000,
|
|
834
1104
|
placeholderData: (prev) => prev, // keepPreviousData
|
|
@@ -863,7 +1133,10 @@ export function useCapabilities() {
|
|
|
863
1133
|
|
|
864
1134
|
// Namespace-scoped capabilities. Users with namespace-scoped RoleBindings may
|
|
865
1135
|
// have these permissions in specific namespaces.
|
|
866
|
-
export function useNamespaceCapabilities(
|
|
1136
|
+
export function useNamespaceCapabilities(
|
|
1137
|
+
namespace: string | undefined,
|
|
1138
|
+
globalCaps: Capabilities | undefined,
|
|
1139
|
+
) {
|
|
867
1140
|
const needsCheck = namespace && globalCaps
|
|
868
1141
|
return useQuery<Capabilities>({
|
|
869
1142
|
queryKey: ['capabilities', namespace],
|
|
@@ -902,7 +1175,11 @@ export function useAuthMe() {
|
|
|
902
1175
|
// CloudRole.AtLeast — the frontend must agree with the backend on what
|
|
903
1176
|
// "member-or-higher" means; otherwise we'd hide a button the
|
|
904
1177
|
// backend would happily honor (or vice versa).
|
|
905
|
-
const CLOUD_ROLE_RANK: Record<string, number> = {
|
|
1178
|
+
const CLOUD_ROLE_RANK: Record<string, number> = {
|
|
1179
|
+
viewer: 1,
|
|
1180
|
+
member: 2,
|
|
1181
|
+
owner: 3,
|
|
1182
|
+
}
|
|
906
1183
|
|
|
907
1184
|
/**
|
|
908
1185
|
* useCloudRole returns the caller's Cloud tier (`owner` / `member` /
|
|
@@ -983,7 +1260,15 @@ export function useNamespaces() {
|
|
|
983
1260
|
}
|
|
984
1261
|
|
|
985
1262
|
// Topology (for manual refresh)
|
|
986
|
-
export function useTopology(
|
|
1263
|
+
export function useTopology(
|
|
1264
|
+
namespaces: string[],
|
|
1265
|
+
viewMode: string = 'resources',
|
|
1266
|
+
options?: {
|
|
1267
|
+
enabled?: boolean
|
|
1268
|
+
includeReplicaSets?: boolean
|
|
1269
|
+
refetchInterval?: number | false
|
|
1270
|
+
},
|
|
1271
|
+
) {
|
|
987
1272
|
const params = new URLSearchParams()
|
|
988
1273
|
if (namespaces.length > 0) params.set('namespaces', namespaces.join(','))
|
|
989
1274
|
if (viewMode) params.set('view', viewMode)
|
|
@@ -1016,7 +1301,11 @@ export function useApplications(namespaces: string[], options?: { enabled?: bool
|
|
|
1016
1301
|
})
|
|
1017
1302
|
}
|
|
1018
1303
|
|
|
1019
|
-
export function useApplicationHistory(
|
|
1304
|
+
export function useApplicationHistory(
|
|
1305
|
+
appKey: string | undefined,
|
|
1306
|
+
namespaces: string[],
|
|
1307
|
+
options?: { enabled?: boolean },
|
|
1308
|
+
) {
|
|
1020
1309
|
const params = new URLSearchParams()
|
|
1021
1310
|
if (appKey) params.set('app', appKey)
|
|
1022
1311
|
if (namespaces.length > 0) params.set('namespaces', namespaces.join(','))
|
|
@@ -1031,7 +1320,14 @@ export function useApplicationHistory(appKey: string | undefined, namespaces: st
|
|
|
1031
1320
|
})
|
|
1032
1321
|
}
|
|
1033
1322
|
|
|
1034
|
-
export function useGitOpsTree(
|
|
1323
|
+
export function useGitOpsTree(
|
|
1324
|
+
kind: string,
|
|
1325
|
+
namespace: string,
|
|
1326
|
+
name: string,
|
|
1327
|
+
group?: string,
|
|
1328
|
+
namespaces: string[] = [],
|
|
1329
|
+
options?: { enabled?: boolean },
|
|
1330
|
+
) {
|
|
1035
1331
|
const ns = namespace || '_'
|
|
1036
1332
|
const params = new URLSearchParams()
|
|
1037
1333
|
if (group) params.set('group', group)
|
|
@@ -1040,7 +1336,8 @@ export function useGitOpsTree(kind: string, namespace: string, name: string, gro
|
|
|
1040
1336
|
|
|
1041
1337
|
return useQuery<GitOpsResourceTree>({
|
|
1042
1338
|
queryKey: ['gitops-tree', kind, namespace, name, group, namespaces],
|
|
1043
|
-
queryFn: () =>
|
|
1339
|
+
queryFn: () =>
|
|
1340
|
+
fetchJSON(`/gitops/tree/${kind}/${ns}/${name}${queryString ? `?${queryString}` : ''}`),
|
|
1044
1341
|
enabled: Boolean(kind && name) && (options?.enabled ?? true),
|
|
1045
1342
|
staleTime: 5000,
|
|
1046
1343
|
})
|
|
@@ -1048,11 +1345,17 @@ export function useGitOpsTree(kind: string, namespace: string, name: string, gro
|
|
|
1048
1345
|
|
|
1049
1346
|
// Poll fast (2s) while a sync/rollback is in flight so the user sees the
|
|
1050
1347
|
// outcome quickly; otherwise rely on staleTime + manual refetch. Argo flips
|
|
1051
|
-
// operationState.phase from
|
|
1348
|
+
// operationState.phase from Running/Terminating to a terminal phase, so this
|
|
1052
1349
|
// auto-quiesces on completion.
|
|
1053
1350
|
const INSIGHTS_RUNNING_POLL_MS = 2000
|
|
1054
1351
|
|
|
1055
|
-
export function useGitOpsInsights(
|
|
1352
|
+
export function useGitOpsInsights(
|
|
1353
|
+
kind: string,
|
|
1354
|
+
namespace: string,
|
|
1355
|
+
name: string,
|
|
1356
|
+
group?: string,
|
|
1357
|
+
namespaces: string[] = [],
|
|
1358
|
+
) {
|
|
1056
1359
|
const ns = namespace || '_'
|
|
1057
1360
|
const params = new URLSearchParams()
|
|
1058
1361
|
if (group) params.set('group', group)
|
|
@@ -1061,19 +1364,71 @@ export function useGitOpsInsights(kind: string, namespace: string, name: string,
|
|
|
1061
1364
|
|
|
1062
1365
|
return useQuery<GitOpsInsight>({
|
|
1063
1366
|
queryKey: ['gitops-insights', kind, namespace, name, group, namespaces],
|
|
1064
|
-
queryFn: () =>
|
|
1367
|
+
queryFn: () =>
|
|
1368
|
+
fetchJSON(`/gitops/insights/${kind}/${ns}/${name}${queryString ? `?${queryString}` : ''}`),
|
|
1065
1369
|
enabled: Boolean(kind && name),
|
|
1066
1370
|
staleTime: 5000,
|
|
1067
1371
|
refetchInterval: (query) => {
|
|
1068
1372
|
const phase = query.state.data?.summary?.operationPhase
|
|
1069
|
-
return phase === 'Running' ? INSIGHTS_RUNNING_POLL_MS : false
|
|
1373
|
+
return phase === 'Running' || phase === 'Terminating' ? INSIGHTS_RUNNING_POLL_MS : false
|
|
1070
1374
|
},
|
|
1071
1375
|
})
|
|
1072
1376
|
}
|
|
1073
1377
|
|
|
1378
|
+
// Full Git-rendered desired-vs-live diff for one Argo CD managed resource.
|
|
1379
|
+
// ns/name identify the Application; the ref identifies the managed resource.
|
|
1380
|
+
// Fetched on demand — the caller mounts this only when the user opens "Full
|
|
1381
|
+
// diff", so it's enabled whenever the ref is resolvable. Errors surface via
|
|
1382
|
+
// fetchJSON's ApiError (server {"error"} string as .message).
|
|
1383
|
+
export function useArgoResourceDiff(appNamespace: string, appName: string, ref: GitOpsInsightRef) {
|
|
1384
|
+
const ns = appNamespace || '_'
|
|
1385
|
+
const params = new URLSearchParams()
|
|
1386
|
+
if (ref.group) params.set('group', ref.group)
|
|
1387
|
+
params.set('kind', ref.kind)
|
|
1388
|
+
if (ref.namespace) params.set('resourceNamespace', ref.namespace)
|
|
1389
|
+
params.set('resourceName', ref.name)
|
|
1390
|
+
|
|
1391
|
+
return useQuery<GitOpsResourceDiff>({
|
|
1392
|
+
queryKey: ['argo-resource-diff', appNamespace, appName, ref.group, ref.kind, ref.namespace, ref.name],
|
|
1393
|
+
queryFn: () => fetchJSON(`/argo/applications/${ns}/${appName}/resource-diff?${params.toString()}`),
|
|
1394
|
+
enabled: Boolean(appName && ref.kind && ref.name),
|
|
1395
|
+
staleTime: 15_000,
|
|
1396
|
+
})
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// Git commit metadata for one deployed revision of an Argo CD Application.
|
|
1400
|
+
// Enabled only when a revision is known and the caller passes `enabled` (gated
|
|
1401
|
+
// on capabilities.revisionMetadataAvailable). Cached long — a resolved SHA's
|
|
1402
|
+
// metadata is effectively immutable.
|
|
1403
|
+
export function useArgoRevisionMetadata(
|
|
1404
|
+
appNamespace: string,
|
|
1405
|
+
appName: string,
|
|
1406
|
+
revision: string | undefined,
|
|
1407
|
+
opts?: { sourceIndex?: number; project?: string; enabled?: boolean },
|
|
1408
|
+
) {
|
|
1409
|
+
const ns = appNamespace || '_'
|
|
1410
|
+
const params = new URLSearchParams()
|
|
1411
|
+
if (revision) params.set('revision', revision)
|
|
1412
|
+
if (opts?.sourceIndex != null) params.set('sourceIndex', String(opts.sourceIndex))
|
|
1413
|
+
if (opts?.project) params.set('project', opts.project)
|
|
1414
|
+
|
|
1415
|
+
return useQuery<ArgoRevisionMetadata>({
|
|
1416
|
+
queryKey: ['argo-revision-metadata', appNamespace, appName, revision, opts?.sourceIndex, opts?.project],
|
|
1417
|
+
queryFn: () => fetchJSON(`/argo/applications/${ns}/${appName}/revision-metadata?${params.toString()}`),
|
|
1418
|
+
enabled: Boolean(appName && revision) && (opts?.enabled ?? true),
|
|
1419
|
+
staleTime: 5 * 60_000,
|
|
1420
|
+
})
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1074
1423
|
// Generic resource fetching - returns resource with relationships
|
|
1075
1424
|
// Uses '_' as placeholder for cluster-scoped resources (empty namespace)
|
|
1076
|
-
export function useResource<T>(
|
|
1425
|
+
export function useResource<T>(
|
|
1426
|
+
kind: string,
|
|
1427
|
+
namespace: string,
|
|
1428
|
+
name: string,
|
|
1429
|
+
group?: string,
|
|
1430
|
+
options?: { enabled?: boolean; refetchInterval?: number | false },
|
|
1431
|
+
) {
|
|
1077
1432
|
// For cluster-scoped resources, use '_' as namespace placeholder
|
|
1078
1433
|
const ns = namespace || '_'
|
|
1079
1434
|
const params = new URLSearchParams()
|
|
@@ -1082,8 +1437,9 @@ export function useResource<T>(kind: string, namespace: string, name: string, gr
|
|
|
1082
1437
|
|
|
1083
1438
|
const query = useQuery<ResourceWithRelationships<T>>({
|
|
1084
1439
|
queryKey: ['resource', kind, namespace, name, group],
|
|
1085
|
-
queryFn: () =>
|
|
1086
|
-
|
|
1440
|
+
queryFn: () =>
|
|
1441
|
+
fetchJSON(`/resources/${kind}/${ns}/${name}${queryString ? `?${queryString}` : ''}`),
|
|
1442
|
+
enabled: (options?.enabled ?? true) && Boolean(kind && name), // namespace can be empty for cluster-scoped resources
|
|
1087
1443
|
refetchInterval: options?.refetchInterval,
|
|
1088
1444
|
})
|
|
1089
1445
|
|
|
@@ -1098,7 +1454,12 @@ export function useResource<T>(kind: string, namespace: string, name: string, gr
|
|
|
1098
1454
|
}
|
|
1099
1455
|
|
|
1100
1456
|
// Hook that returns full response with relationships explicitly
|
|
1101
|
-
export function useResourceWithRelationships<T>(
|
|
1457
|
+
export function useResourceWithRelationships<T>(
|
|
1458
|
+
kind: string,
|
|
1459
|
+
namespace: string,
|
|
1460
|
+
name: string,
|
|
1461
|
+
group?: string,
|
|
1462
|
+
) {
|
|
1102
1463
|
const ns = namespace || '_'
|
|
1103
1464
|
const params = new URLSearchParams()
|
|
1104
1465
|
if (group) params.set('group', group)
|
|
@@ -1106,7 +1467,8 @@ export function useResourceWithRelationships<T>(kind: string, namespace: string,
|
|
|
1106
1467
|
|
|
1107
1468
|
return useQuery<ResourceWithRelationships<T>>({
|
|
1108
1469
|
queryKey: ['resource', kind, namespace, name, group],
|
|
1109
|
-
queryFn: () =>
|
|
1470
|
+
queryFn: () =>
|
|
1471
|
+
fetchJSON(`/resources/${kind}/${ns}/${name}${queryString ? `?${queryString}` : ''}`),
|
|
1110
1472
|
enabled: Boolean(kind && name),
|
|
1111
1473
|
})
|
|
1112
1474
|
}
|
|
@@ -1199,7 +1561,11 @@ async function fetchChangesPage(
|
|
|
1199
1561
|
// (Rows dropped by content filters inside the store query do not; see the
|
|
1200
1562
|
// known limitation on the server's handleChanges.)
|
|
1201
1563
|
const maxSeq = Number(response.headers.get('X-Radar-Timeline-Max-Seq') ?? '0') || 0
|
|
1202
|
-
return {
|
|
1564
|
+
return {
|
|
1565
|
+
events,
|
|
1566
|
+
epoch: response.headers.get('X-Radar-Timeline-Epoch') ?? '',
|
|
1567
|
+
maxSeq,
|
|
1568
|
+
}
|
|
1203
1569
|
}
|
|
1204
1570
|
|
|
1205
1571
|
// Highest store-assigned arrival number in the cached page — the delta cursor.
|
|
@@ -1250,7 +1616,10 @@ export async function runDeltaSyncFetch(deps: {
|
|
|
1250
1616
|
const meta = metaStore.get(metaKey)
|
|
1251
1617
|
const cursor = deltaFetchCursor(meta, cached, now)
|
|
1252
1618
|
if (cursor > 0) {
|
|
1253
|
-
const delta = await fetchChangesPage(
|
|
1619
|
+
const delta = await fetchChangesPage(
|
|
1620
|
+
`${path}${queryString ? '&' : '?'}since_seq=${cursor}`,
|
|
1621
|
+
signal,
|
|
1622
|
+
)
|
|
1254
1623
|
if (delta.epoch && delta.epoch === meta!.epoch) {
|
|
1255
1624
|
meta!.highWaterSeq = Math.max(meta!.highWaterSeq, delta.maxSeq, maxEventSeq(delta.events))
|
|
1256
1625
|
// Returning the cached reference on an empty delta skips re-renders.
|
|
@@ -1260,7 +1629,11 @@ export async function runDeltaSyncFetch(deps: {
|
|
|
1260
1629
|
// cursor is meaningless. Fall through to a full resync.
|
|
1261
1630
|
}
|
|
1262
1631
|
const full = await fetchChangesPage(path, signal)
|
|
1263
|
-
metaStore.set(metaKey, {
|
|
1632
|
+
metaStore.set(metaKey, {
|
|
1633
|
+
epoch: full.epoch,
|
|
1634
|
+
lastFullMs: now,
|
|
1635
|
+
highWaterSeq: Math.max(full.maxSeq, maxEventSeq(full.events)),
|
|
1636
|
+
})
|
|
1264
1637
|
return full.events
|
|
1265
1638
|
}
|
|
1266
1639
|
|
|
@@ -1288,7 +1661,18 @@ function getTimeRangeDate(range: TimeRange): Date | null {
|
|
|
1288
1661
|
}
|
|
1289
1662
|
|
|
1290
1663
|
export function useChanges(options: UseChangesOptions = {}) {
|
|
1291
|
-
const {
|
|
1664
|
+
const {
|
|
1665
|
+
namespaces = [],
|
|
1666
|
+
kinds,
|
|
1667
|
+
timeRange = '1h',
|
|
1668
|
+
filter = 'all',
|
|
1669
|
+
includeK8sEvents = true,
|
|
1670
|
+
includeManaged = false,
|
|
1671
|
+
includeDeleted = true,
|
|
1672
|
+
limit = 200,
|
|
1673
|
+
enabled = true,
|
|
1674
|
+
deltaSync = false,
|
|
1675
|
+
} = options
|
|
1292
1676
|
const queryClient = useQueryClient()
|
|
1293
1677
|
|
|
1294
1678
|
// Only a single-kind selection narrows the server query; a multi-kind
|
|
@@ -1311,7 +1695,17 @@ export function useChanges(options: UseChangesOptions = {}) {
|
|
|
1311
1695
|
|
|
1312
1696
|
const queryString = params.toString()
|
|
1313
1697
|
const path = `/changes${queryString ? `?${queryString}` : ''}`
|
|
1314
|
-
const queryKey = [
|
|
1698
|
+
const queryKey = [
|
|
1699
|
+
'changes',
|
|
1700
|
+
namespaces,
|
|
1701
|
+
serverKind,
|
|
1702
|
+
timeRange,
|
|
1703
|
+
filter,
|
|
1704
|
+
includeK8sEvents,
|
|
1705
|
+
includeManaged,
|
|
1706
|
+
includeDeleted,
|
|
1707
|
+
limit,
|
|
1708
|
+
]
|
|
1315
1709
|
|
|
1316
1710
|
return useQuery<TimelineEvent[]>({
|
|
1317
1711
|
queryKey,
|
|
@@ -1320,7 +1714,16 @@ export function useChanges(options: UseChangesOptions = {}) {
|
|
|
1320
1714
|
|
|
1321
1715
|
const metaKey = JSON.stringify(queryKey)
|
|
1322
1716
|
const cached = queryClient.getQueryData<TimelineEvent[]>(queryKey)
|
|
1323
|
-
return runDeltaSyncFetch({
|
|
1717
|
+
return runDeltaSyncFetch({
|
|
1718
|
+
path,
|
|
1719
|
+
queryString,
|
|
1720
|
+
limit,
|
|
1721
|
+
metaKey,
|
|
1722
|
+
cached,
|
|
1723
|
+
metaStore: changesDeltaMeta,
|
|
1724
|
+
now: Date.now(),
|
|
1725
|
+
signal,
|
|
1726
|
+
})
|
|
1324
1727
|
},
|
|
1325
1728
|
staleTime: 5000, // Consider data stale after 5 seconds to ensure fresh data on navigation
|
|
1326
1729
|
refetchInterval: CHANGES_REFRESH_INTERVAL_MS, // SSE-driven invalidation handles real-time updates; this is the no-SSE fallback
|
|
@@ -1329,7 +1732,12 @@ export function useChanges(options: UseChangesOptions = {}) {
|
|
|
1329
1732
|
}
|
|
1330
1733
|
|
|
1331
1734
|
// Children changes for a parent workload (e.g., ReplicaSets and Pods under a Deployment)
|
|
1332
|
-
export function useResourceChildren(
|
|
1735
|
+
export function useResourceChildren(
|
|
1736
|
+
kind: string,
|
|
1737
|
+
namespace: string,
|
|
1738
|
+
name: string,
|
|
1739
|
+
timeRange: TimeRange = '1h',
|
|
1740
|
+
) {
|
|
1333
1741
|
const sinceDate = getTimeRangeDate(timeRange)
|
|
1334
1742
|
const params = new URLSearchParams()
|
|
1335
1743
|
if (sinceDate) {
|
|
@@ -1358,7 +1766,11 @@ export interface ResourceEventsResult {
|
|
|
1358
1766
|
// K8s events and resource updates are fetched separately so a high-frequency
|
|
1359
1767
|
// informer update stream (e.g. a CrashLoop status field flapping every few
|
|
1360
1768
|
// seconds) can never starve out user-meaningful K8s events under a shared limit.
|
|
1361
|
-
export function useResourceEvents(
|
|
1769
|
+
export function useResourceEvents(
|
|
1770
|
+
kind: string,
|
|
1771
|
+
namespace: string,
|
|
1772
|
+
name: string,
|
|
1773
|
+
): ResourceEventsResult {
|
|
1362
1774
|
// The timeline store keys events by their K8s Kind (singular PascalCase, e.g. "Pod"),
|
|
1363
1775
|
// but callers pass the URL-form kind ("pods").
|
|
1364
1776
|
const singularKind = pluralToKind(kind)
|
|
@@ -1424,8 +1836,8 @@ export function useResourceEvents(kind: string, namespace: string, name: string)
|
|
|
1424
1836
|
export interface ContainerMetrics {
|
|
1425
1837
|
name: string
|
|
1426
1838
|
usage: {
|
|
1427
|
-
cpu: string
|
|
1428
|
-
memory: string
|
|
1839
|
+
cpu: string // e.g., "10m" (millicores)
|
|
1840
|
+
memory: string // e.g., "128Mi"
|
|
1429
1841
|
}
|
|
1430
1842
|
}
|
|
1431
1843
|
|
|
@@ -1500,8 +1912,8 @@ export function useNodeMetrics(nodeName: string, options?: { enabled?: boolean }
|
|
|
1500
1912
|
|
|
1501
1913
|
export interface MetricsDataPoint {
|
|
1502
1914
|
timestamp: string
|
|
1503
|
-
cpu: number
|
|
1504
|
-
memory: number
|
|
1915
|
+
cpu: number // CPU in nanocores
|
|
1916
|
+
memory: number // Memory in bytes
|
|
1505
1917
|
}
|
|
1506
1918
|
|
|
1507
1919
|
export interface ContainerMetricsHistory {
|
|
@@ -1530,7 +1942,9 @@ export interface NodeMetricsHistory {
|
|
|
1530
1942
|
metricsUnavailableReason?: string
|
|
1531
1943
|
}
|
|
1532
1944
|
|
|
1533
|
-
function withoutCollectionError<
|
|
1945
|
+
function withoutCollectionError<
|
|
1946
|
+
T extends { collectionError?: string; rawCollectionError?: string },
|
|
1947
|
+
>(history: T): T {
|
|
1534
1948
|
const next = { ...history }
|
|
1535
1949
|
delete next.collectionError
|
|
1536
1950
|
delete next.rawCollectionError
|
|
@@ -1539,15 +1953,26 @@ function withoutCollectionError<T extends { collectionError?: string; rawCollect
|
|
|
1539
1953
|
|
|
1540
1954
|
export function normalizePodMetricsHistory(history: PodMetricsHistory): PodMetricsHistory {
|
|
1541
1955
|
if (history.metricsUnavailable !== true) return history
|
|
1542
|
-
return {
|
|
1956
|
+
return {
|
|
1957
|
+
...withoutCollectionError(history),
|
|
1958
|
+
metricsUnavailable: true,
|
|
1959
|
+
metricsUnavailableReason: history.rawCollectionError || history.collectionError,
|
|
1960
|
+
}
|
|
1543
1961
|
}
|
|
1544
1962
|
|
|
1545
1963
|
export function normalizeNodeMetricsHistory(history: NodeMetricsHistory): NodeMetricsHistory {
|
|
1546
1964
|
if (history.metricsUnavailable !== true) return history
|
|
1547
|
-
return {
|
|
1965
|
+
return {
|
|
1966
|
+
...withoutCollectionError(history),
|
|
1967
|
+
metricsUnavailable: true,
|
|
1968
|
+
metricsUnavailableReason: history.rawCollectionError || history.collectionError,
|
|
1969
|
+
}
|
|
1548
1970
|
}
|
|
1549
1971
|
|
|
1550
|
-
export function shouldFetchLiveMetrics(
|
|
1972
|
+
export function shouldFetchLiveMetrics(
|
|
1973
|
+
historySettled: boolean,
|
|
1974
|
+
metricsUnavailable: boolean,
|
|
1975
|
+
): boolean {
|
|
1551
1976
|
return historySettled && !metricsUnavailable
|
|
1552
1977
|
}
|
|
1553
1978
|
|
|
@@ -1555,7 +1980,11 @@ export function isLiveMetricsUnavailable(liveMetricsEnabled: boolean, metrics: u
|
|
|
1555
1980
|
return liveMetricsEnabled && metrics === null
|
|
1556
1981
|
}
|
|
1557
1982
|
|
|
1558
|
-
export function getVisibleLiveMetrics<T>(
|
|
1983
|
+
export function getVisibleLiveMetrics<T>(
|
|
1984
|
+
liveMetricsEnabled: boolean,
|
|
1985
|
+
metricsUnavailable: boolean,
|
|
1986
|
+
metrics: T | null | undefined,
|
|
1987
|
+
): T | undefined {
|
|
1559
1988
|
if (!liveMetricsEnabled || metricsUnavailable) return undefined
|
|
1560
1989
|
return metrics ?? undefined
|
|
1561
1990
|
}
|
|
@@ -1564,7 +1993,10 @@ export function getVisibleLiveMetrics<T>(liveMetricsEnabled: boolean, metricsUna
|
|
|
1564
1993
|
export function usePodMetricsHistory(namespace: string, podName: string) {
|
|
1565
1994
|
return useQuery<PodMetricsHistory>({
|
|
1566
1995
|
queryKey: ['pod-metrics-history', namespace, podName],
|
|
1567
|
-
queryFn: async () =>
|
|
1996
|
+
queryFn: async () =>
|
|
1997
|
+
normalizePodMetricsHistory(
|
|
1998
|
+
await fetchJSON<PodMetricsHistory>(`/metrics/pods/${namespace}/${podName}/history`),
|
|
1999
|
+
),
|
|
1568
2000
|
enabled: Boolean(namespace && podName),
|
|
1569
2001
|
staleTime: 25000, // Slightly less than poll interval
|
|
1570
2002
|
refetchInterval: 30000, // Match the backend poll interval
|
|
@@ -1575,7 +2007,10 @@ export function usePodMetricsHistory(namespace: string, podName: string) {
|
|
|
1575
2007
|
export function useNodeMetricsHistory(nodeName: string) {
|
|
1576
2008
|
return useQuery<NodeMetricsHistory>({
|
|
1577
2009
|
queryKey: ['node-metrics-history', nodeName],
|
|
1578
|
-
queryFn: async () =>
|
|
2010
|
+
queryFn: async () =>
|
|
2011
|
+
normalizeNodeMetricsHistory(
|
|
2012
|
+
await fetchJSON<NodeMetricsHistory>(`/metrics/nodes/${nodeName}/history`),
|
|
2013
|
+
),
|
|
1579
2014
|
enabled: Boolean(nodeName),
|
|
1580
2015
|
staleTime: 25000,
|
|
1581
2016
|
refetchInterval: 30000,
|
|
@@ -1586,20 +2021,20 @@ export function useNodeMetricsHistory(nodeName: string) {
|
|
|
1586
2021
|
export interface TopPodMetrics {
|
|
1587
2022
|
namespace: string
|
|
1588
2023
|
name: string
|
|
1589
|
-
cpu: number
|
|
1590
|
-
memory: number
|
|
1591
|
-
cpuRequest: number
|
|
1592
|
-
cpuLimit: number
|
|
2024
|
+
cpu: number // nanocores (usage)
|
|
2025
|
+
memory: number // bytes (usage)
|
|
2026
|
+
cpuRequest: number // nanocores (sum across containers)
|
|
2027
|
+
cpuLimit: number // nanocores (sum across containers)
|
|
1593
2028
|
memoryRequest: number // bytes (sum across containers)
|
|
1594
|
-
memoryLimit: number
|
|
2029
|
+
memoryLimit: number // bytes (sum across containers)
|
|
1595
2030
|
}
|
|
1596
2031
|
|
|
1597
2032
|
export interface TopNodeMetrics {
|
|
1598
2033
|
name: string
|
|
1599
|
-
cpu: number
|
|
1600
|
-
memory: number
|
|
1601
|
-
podCount: number
|
|
1602
|
-
cpuAllocatable: number
|
|
2034
|
+
cpu: number // nanocores (usage)
|
|
2035
|
+
memory: number // bytes (usage)
|
|
2036
|
+
podCount: number // pods scheduled on this node
|
|
2037
|
+
cpuAllocatable: number // nanocores
|
|
1603
2038
|
memoryAllocatable: number // bytes
|
|
1604
2039
|
}
|
|
1605
2040
|
|
|
@@ -1675,11 +2110,13 @@ export interface PrometheusResourceMetrics {
|
|
|
1675
2110
|
range: string
|
|
1676
2111
|
result: PrometheusQueryResult
|
|
1677
2112
|
query?: string // PromQL query (included when result is empty, for diagnostics)
|
|
1678
|
-
hint?: string
|
|
2113
|
+
hint?: string // Contextual hint when results are empty (e.g. cri-docker label issues)
|
|
1679
2114
|
}
|
|
1680
2115
|
|
|
1681
|
-
export type PrometheusMetricCategory =
|
|
1682
|
-
|
|
2116
|
+
export type PrometheusMetricCategory =
|
|
2117
|
+
'cpu' | 'memory' | 'network_rx' | 'network_tx' | 'filesystem' | 'restarts'
|
|
2118
|
+
export type PrometheusTimeRange =
|
|
2119
|
+
'10m' | '30m' | '1h' | '3h' | '6h' | '12h' | '24h' | '48h' | '7d' | '14d'
|
|
1683
2120
|
|
|
1684
2121
|
// PVC usage at a moment in time, derived from kubelet_volume_stats_*.
|
|
1685
2122
|
// HasData=false silently indicates the CSI driver doesn't report or Prom
|
|
@@ -1693,17 +2130,49 @@ export interface PrometheusPVCUsage {
|
|
|
1693
2130
|
hasData: boolean
|
|
1694
2131
|
}
|
|
1695
2132
|
|
|
1696
|
-
export type
|
|
2133
|
+
export type RightsizingFit =
|
|
2134
|
+
'balanced' | 'oversized' | 'under_requested' | 'missing_request' | 'insufficient_history'
|
|
2135
|
+
export type RightsizingConfidence = 'low' | 'medium' | 'high'
|
|
2136
|
+
export type RightsizingOwnerCoverage = 'ksm_history' | 'current_pods'
|
|
1697
2137
|
|
|
1698
2138
|
export interface RightsizingRow {
|
|
1699
2139
|
container: string
|
|
1700
2140
|
resource: 'cpu' | 'memory'
|
|
2141
|
+
fit: RightsizingFit
|
|
2142
|
+
confidence: RightsizingConfidence
|
|
1701
2143
|
currentRequest?: string
|
|
2144
|
+
currentRequestValue?: number
|
|
1702
2145
|
currentLimit?: string
|
|
1703
|
-
|
|
2146
|
+
currentLimitValue?: number
|
|
2147
|
+
observed?: {
|
|
2148
|
+
name: 'P95' | 'P99' | 'Max'
|
|
2149
|
+
value: number
|
|
2150
|
+
formatted: string
|
|
2151
|
+
}
|
|
2152
|
+
peak?: {
|
|
2153
|
+
name: 'P99'
|
|
2154
|
+
value: number
|
|
2155
|
+
formatted: string
|
|
2156
|
+
}
|
|
2157
|
+
calculatedRequest?: string
|
|
2158
|
+
calculatedRequestValue?: number
|
|
1704
2159
|
recommendedRequest?: string
|
|
1705
|
-
|
|
1706
|
-
|
|
2160
|
+
recommendedRequestValue?: number
|
|
2161
|
+
reductionLimited?: boolean
|
|
2162
|
+
bursty?: boolean
|
|
2163
|
+
recommendationReason?: string
|
|
2164
|
+
sampleCount: number
|
|
2165
|
+
expectedSamples: number
|
|
2166
|
+
coverage: number
|
|
2167
|
+
hpaManaged: boolean
|
|
2168
|
+
hpaEvidenceAvailable: boolean
|
|
2169
|
+
throttleAvailable?: boolean
|
|
2170
|
+
throttleRatio?: number
|
|
2171
|
+
currentPodOOM?: boolean
|
|
2172
|
+
windowOomEvidence?: boolean
|
|
2173
|
+
oomEvidenceAvailable: boolean
|
|
2174
|
+
limitConflict?: boolean
|
|
2175
|
+
queryError?: string
|
|
1707
2176
|
}
|
|
1708
2177
|
|
|
1709
2178
|
export interface PrometheusRightsizing {
|
|
@@ -1711,11 +2180,46 @@ export interface PrometheusRightsizing {
|
|
|
1711
2180
|
namespace: string
|
|
1712
2181
|
name: string
|
|
1713
2182
|
window: string
|
|
2183
|
+
source: 'radar'
|
|
2184
|
+
ownerCoverage: RightsizingOwnerCoverage
|
|
2185
|
+
scaledToZero: boolean
|
|
1714
2186
|
sampleAvailable: boolean
|
|
1715
2187
|
rows: RightsizingRow[]
|
|
1716
2188
|
reason?: string
|
|
1717
2189
|
}
|
|
1718
2190
|
|
|
2191
|
+
export type RightsizingScanState = 'complete' | 'partial' | 'unavailable'
|
|
2192
|
+
|
|
2193
|
+
export interface RightsizingScanWorkload {
|
|
2194
|
+
kind: string
|
|
2195
|
+
namespace: string
|
|
2196
|
+
name: string
|
|
2197
|
+
replicas: number
|
|
2198
|
+
scaledToZero: boolean
|
|
2199
|
+
rows: RightsizingRow[]
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
export interface RightsizingScanCoverage {
|
|
2203
|
+
workloadsDiscovered: number
|
|
2204
|
+
workloadsEvaluated: number
|
|
2205
|
+
workloadsWithData: number
|
|
2206
|
+
batches: number
|
|
2207
|
+
completedBatches: number
|
|
2208
|
+
restrictedKinds?: string[]
|
|
2209
|
+
unavailableKinds?: string[]
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
export interface RightsizingScanResponse {
|
|
2213
|
+
state: RightsizingScanState
|
|
2214
|
+
scannedAt: string
|
|
2215
|
+
window: string
|
|
2216
|
+
source: 'radar'
|
|
2217
|
+
coverage: RightsizingScanCoverage
|
|
2218
|
+
workloads: RightsizingScanWorkload[]
|
|
2219
|
+
warnings?: { code: string; message: string }[]
|
|
2220
|
+
reason?: string
|
|
2221
|
+
}
|
|
2222
|
+
|
|
1719
2223
|
// Check Prometheus availability
|
|
1720
2224
|
export function usePrometheusStatus() {
|
|
1721
2225
|
return useQuery<PrometheusStatus>({
|
|
@@ -1726,12 +2230,32 @@ export function usePrometheusStatus() {
|
|
|
1726
2230
|
})
|
|
1727
2231
|
}
|
|
1728
2232
|
|
|
2233
|
+
export interface ArgoStatus {
|
|
2234
|
+
// configured = a URL or token is set; connected = a probe has landed and the
|
|
2235
|
+
// client is live. The two differ right after a restart (configured, reconnecting).
|
|
2236
|
+
configured: boolean
|
|
2237
|
+
connected: boolean
|
|
2238
|
+
address?: string
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
export function useArgoStatus(enabled = true) {
|
|
2242
|
+
return useQuery<ArgoStatus>({
|
|
2243
|
+
queryKey: ['argocd-status'],
|
|
2244
|
+
queryFn: () => fetchJSON('/integrations/argocd/status'),
|
|
2245
|
+
enabled,
|
|
2246
|
+
staleTime: 30000,
|
|
2247
|
+
refetchInterval: 60000,
|
|
2248
|
+
})
|
|
2249
|
+
}
|
|
2250
|
+
|
|
1729
2251
|
// Connect to Prometheus (trigger discovery)
|
|
1730
2252
|
export function usePrometheusConnect() {
|
|
1731
2253
|
const queryClient = useQueryClient()
|
|
1732
2254
|
return useMutation({
|
|
1733
2255
|
mutationFn: async () => {
|
|
1734
|
-
const resp = await apiFetch(`${getApiBase()}/prometheus/connect`, {
|
|
2256
|
+
const resp = await apiFetch(`${getApiBase()}/prometheus/connect`, {
|
|
2257
|
+
method: 'POST',
|
|
2258
|
+
})
|
|
1735
2259
|
if (!resp.ok) {
|
|
1736
2260
|
const body = await resp.json().catch(() => ({ error: 'Unknown error' }))
|
|
1737
2261
|
throw new Error(body.error || `HTTP ${resp.status}`)
|
|
@@ -1792,7 +2316,9 @@ export function useAutoPromConnect(): void {
|
|
|
1792
2316
|
|
|
1793
2317
|
// Persist the "we've connected here before" signal once a connection lands.
|
|
1794
2318
|
if (status?.connected) {
|
|
1795
|
-
try {
|
|
2319
|
+
try {
|
|
2320
|
+
window.localStorage.setItem(promAutoConnectKey(context), '1')
|
|
2321
|
+
} catch {
|
|
1796
2322
|
// localStorage can throw in some restricted browser modes — fail open.
|
|
1797
2323
|
}
|
|
1798
2324
|
return
|
|
@@ -1800,7 +2326,9 @@ export function useAutoPromConnect(): void {
|
|
|
1800
2326
|
|
|
1801
2327
|
if (attemptedRef.current === context) return
|
|
1802
2328
|
let cached: string | null = null
|
|
1803
|
-
try {
|
|
2329
|
+
try {
|
|
2330
|
+
cached = window.localStorage.getItem(promAutoConnectKey(context))
|
|
2331
|
+
} catch {
|
|
1804
2332
|
// keep the null fallback
|
|
1805
2333
|
}
|
|
1806
2334
|
|
|
@@ -1812,16 +2340,20 @@ export function useAutoPromConnect(): void {
|
|
|
1812
2340
|
const timeout = window.setTimeout(() => {
|
|
1813
2341
|
// Direct apiFetch (not via the usePrometheusConnect mutation) so the
|
|
1814
2342
|
// meta-driven toast handler stays silent — the user didn't click anything.
|
|
1815
|
-
apiFetch(`${getApiBase()}/prometheus/connect?optional=true`, {
|
|
1816
|
-
|
|
2343
|
+
apiFetch(`${getApiBase()}/prometheus/connect?optional=true`, {
|
|
2344
|
+
method: 'POST',
|
|
2345
|
+
})
|
|
2346
|
+
.then(async (resp) => {
|
|
1817
2347
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
|
1818
|
-
const nextStatus = await resp.json() as PrometheusStatus
|
|
2348
|
+
const nextStatus = (await resp.json()) as PrometheusStatus
|
|
1819
2349
|
queryClient.setQueryData(['prometheus-status'], nextStatus)
|
|
1820
2350
|
if (!nextStatus.connected) throw new Error(nextStatus.error || 'Prometheus unavailable')
|
|
1821
2351
|
queryClient.invalidateQueries({ queryKey: ['prometheus-status'] })
|
|
1822
2352
|
})
|
|
1823
2353
|
.catch(() => {
|
|
1824
|
-
try {
|
|
2354
|
+
try {
|
|
2355
|
+
window.localStorage.removeItem(promAutoConnectKey(context))
|
|
2356
|
+
} catch {
|
|
1825
2357
|
// ignore — manual CTA will render once status refreshes
|
|
1826
2358
|
}
|
|
1827
2359
|
attemptedRef.current = null
|
|
@@ -1879,8 +2411,7 @@ export function usePrometheusClusterMetrics(
|
|
|
1879
2411
|
) {
|
|
1880
2412
|
return useQuery<PrometheusResourceMetrics>({
|
|
1881
2413
|
queryKey: ['prometheus-cluster-metrics', category, range],
|
|
1882
|
-
queryFn: () =>
|
|
1883
|
-
fetchJSON(`/prometheus/cluster?category=${category}&range=${range}`),
|
|
2414
|
+
queryFn: () => fetchJSON(`/prometheus/cluster?category=${category}&range=${range}`),
|
|
1884
2415
|
enabled,
|
|
1885
2416
|
staleTime: 30000,
|
|
1886
2417
|
refetchInterval: 60000,
|
|
@@ -1899,22 +2430,75 @@ export function usePrometheusPVCUsage(namespace: string, name: string, enabled =
|
|
|
1899
2430
|
}
|
|
1900
2431
|
|
|
1901
2432
|
// Fetch rightsizing recommendations for a workload (Deployment / StatefulSet / DaemonSet).
|
|
1902
|
-
export function usePrometheusRightsizing(
|
|
2433
|
+
export function usePrometheusRightsizing(
|
|
2434
|
+
kind: string,
|
|
2435
|
+
namespace: string,
|
|
2436
|
+
name: string,
|
|
2437
|
+
enabled = true,
|
|
2438
|
+
) {
|
|
1903
2439
|
return useQuery<PrometheusRightsizing>({
|
|
1904
2440
|
queryKey: ['prometheus-rightsizing', kind, namespace, name],
|
|
1905
2441
|
queryFn: () => fetchJSON(`/prometheus/rightsizing/${kind}/${namespace}/${name}`),
|
|
1906
2442
|
enabled: enabled && Boolean(kind && namespace && name),
|
|
1907
|
-
staleTime: 5 * 60 * 1000,
|
|
2443
|
+
staleTime: 5 * 60 * 1000,
|
|
1908
2444
|
refetchInterval: 10 * 60 * 1000,
|
|
1909
2445
|
})
|
|
1910
2446
|
}
|
|
1911
2447
|
|
|
2448
|
+
const RIGHTSIZING_SCAN_CACHE_TIME = 5 * 60 * 1000
|
|
2449
|
+
|
|
2450
|
+
export function getRightsizingScanCacheConfig(
|
|
2451
|
+
namespaces: string[],
|
|
2452
|
+
context = '',
|
|
2453
|
+
): {
|
|
2454
|
+
namespaceKey: string
|
|
2455
|
+
queryKey: readonly ['prometheus-rightsizing-scan', string, string]
|
|
2456
|
+
queryFn: typeof skipToken
|
|
2457
|
+
gcTime: number
|
|
2458
|
+
} {
|
|
2459
|
+
const namespaceKey = [...namespaces].sort().join(',')
|
|
2460
|
+
return {
|
|
2461
|
+
namespaceKey,
|
|
2462
|
+
queryKey: ['prometheus-rightsizing-scan', context, namespaceKey] as const,
|
|
2463
|
+
queryFn: skipToken,
|
|
2464
|
+
gcTime: RIGHTSIZING_SCAN_CACHE_TIME,
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
// A fleet rightsizing scan is intentionally manual. It can query seven days of
|
|
2469
|
+
// Prometheus history for many containers, so navigation alone must never run it.
|
|
2470
|
+
export function useRightsizingScan(namespaces: string[], context = '') {
|
|
2471
|
+
const queryClient = useQueryClient()
|
|
2472
|
+
const { namespaceKey, ...snapshotOptions } = getRightsizingScanCacheConfig(namespaces, context)
|
|
2473
|
+
const scanScope = { namespaceKey, queryKey: snapshotOptions.queryKey }
|
|
2474
|
+
const snapshot = useQuery<RightsizingScanResponse>(snapshotOptions)
|
|
2475
|
+
const mutation = useMutation({
|
|
2476
|
+
mutationFn: async (startedScope: typeof scanScope) => {
|
|
2477
|
+
const params = new URLSearchParams()
|
|
2478
|
+
if (startedScope.namespaceKey) params.set('namespaces', startedScope.namespaceKey)
|
|
2479
|
+
const query = params.toString()
|
|
2480
|
+
return fetchJSON<RightsizingScanResponse>(
|
|
2481
|
+
`/prometheus/rightsizing/scan${query ? `?${query}` : ''}`,
|
|
2482
|
+
{
|
|
2483
|
+
method: 'POST',
|
|
2484
|
+
},
|
|
2485
|
+
)
|
|
2486
|
+
},
|
|
2487
|
+
onSuccess: (result, startedScope) => queryClient.setQueryData(startedScope.queryKey, result),
|
|
2488
|
+
})
|
|
2489
|
+
return {
|
|
2490
|
+
...mutation,
|
|
2491
|
+
data: snapshot.data,
|
|
2492
|
+
mutate: () => mutation.mutate(scanScope),
|
|
2493
|
+
mutateAsync: () => mutation.mutateAsync(scanScope),
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
|
|
1912
2497
|
// Raw PromQL query (range). Used by HPA charts for status_current_replicas etc.
|
|
1913
2498
|
export function usePromQLRange(query: string, range: PrometheusTimeRange = '1h', enabled = true) {
|
|
1914
2499
|
return useQuery<PrometheusQueryResult>({
|
|
1915
2500
|
queryKey: ['promql-range', query, range],
|
|
1916
|
-
queryFn: () =>
|
|
1917
|
-
fetchJSON(`/prometheus/query?query=${encodeURIComponent(query)}&range=${range}`),
|
|
2501
|
+
queryFn: () => fetchJSON(`/prometheus/query?query=${encodeURIComponent(query)}&range=${range}`),
|
|
1918
2502
|
enabled: enabled && Boolean(query),
|
|
1919
2503
|
staleTime: 30000,
|
|
1920
2504
|
refetchInterval: 60000,
|
|
@@ -1947,12 +2531,16 @@ export interface LogStreamEvent {
|
|
|
1947
2531
|
}
|
|
1948
2532
|
|
|
1949
2533
|
// Fetch pod logs (non-streaming)
|
|
1950
|
-
export function usePodLogs(
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
2534
|
+
export function usePodLogs(
|
|
2535
|
+
namespace: string,
|
|
2536
|
+
podName: string,
|
|
2537
|
+
options?: {
|
|
2538
|
+
container?: string
|
|
2539
|
+
tailLines?: number
|
|
2540
|
+
previous?: boolean
|
|
2541
|
+
sinceSeconds?: number
|
|
2542
|
+
},
|
|
2543
|
+
) {
|
|
1956
2544
|
const params = new URLSearchParams()
|
|
1957
2545
|
if (options?.container) params.set('container', options.container)
|
|
1958
2546
|
if (options?.tailLines) params.set('tailLines', String(options.tailLines))
|
|
@@ -1961,8 +2549,17 @@ export function usePodLogs(namespace: string, podName: string, options?: {
|
|
|
1961
2549
|
const queryString = params.toString()
|
|
1962
2550
|
|
|
1963
2551
|
return useQuery<LogsResponse>({
|
|
1964
|
-
queryKey: [
|
|
1965
|
-
|
|
2552
|
+
queryKey: [
|
|
2553
|
+
'pod-logs',
|
|
2554
|
+
namespace,
|
|
2555
|
+
podName,
|
|
2556
|
+
options?.container,
|
|
2557
|
+
options?.tailLines,
|
|
2558
|
+
options?.previous,
|
|
2559
|
+
options?.sinceSeconds,
|
|
2560
|
+
],
|
|
2561
|
+
queryFn: () =>
|
|
2562
|
+
fetchJSON(`/pods/${namespace}/${podName}/logs${queryString ? `?${queryString}` : ''}`),
|
|
1966
2563
|
enabled: Boolean(namespace && podName),
|
|
1967
2564
|
staleTime: 5000, // Allow refetch after 5 seconds
|
|
1968
2565
|
})
|
|
@@ -1977,7 +2574,7 @@ export function createLogStream(
|
|
|
1977
2574
|
tailLines?: number
|
|
1978
2575
|
previous?: boolean
|
|
1979
2576
|
sinceSeconds?: number
|
|
1980
|
-
}
|
|
2577
|
+
},
|
|
1981
2578
|
): EventSource {
|
|
1982
2579
|
const params = new URLSearchParams()
|
|
1983
2580
|
if (options?.container) params.set('container', options.container)
|
|
@@ -1986,9 +2583,12 @@ export function createLogStream(
|
|
|
1986
2583
|
if (options?.sinceSeconds) params.set('sinceSeconds', String(options.sinceSeconds))
|
|
1987
2584
|
const queryString = params.toString()
|
|
1988
2585
|
|
|
1989
|
-
return new EventSource(
|
|
1990
|
-
|
|
1991
|
-
|
|
2586
|
+
return new EventSource(
|
|
2587
|
+
`${getApiBase()}/pods/${namespace}/${podName}/logs/stream${queryString ? `?${queryString}` : ''}`,
|
|
2588
|
+
{
|
|
2589
|
+
withCredentials: getCredentialsMode() === 'include',
|
|
2590
|
+
},
|
|
2591
|
+
)
|
|
1992
2592
|
}
|
|
1993
2593
|
|
|
1994
2594
|
// ============================================================================
|
|
@@ -2021,8 +2621,23 @@ export function useUpdateResource() {
|
|
|
2021
2621
|
const queryClient = useQueryClient()
|
|
2022
2622
|
|
|
2023
2623
|
return useMutation({
|
|
2024
|
-
mutationFn: async ({
|
|
2025
|
-
|
|
2624
|
+
mutationFn: async ({
|
|
2625
|
+
kind,
|
|
2626
|
+
namespace,
|
|
2627
|
+
name,
|
|
2628
|
+
yaml,
|
|
2629
|
+
force = true,
|
|
2630
|
+
}: {
|
|
2631
|
+
kind: string
|
|
2632
|
+
namespace: string
|
|
2633
|
+
name: string
|
|
2634
|
+
yaml: string
|
|
2635
|
+
force?: boolean
|
|
2636
|
+
}) => {
|
|
2637
|
+
const url = new URL(
|
|
2638
|
+
`${getApiBase()}/resources/${kind}/${namespace}/${name}`,
|
|
2639
|
+
window.location.origin,
|
|
2640
|
+
)
|
|
2026
2641
|
if (!force) {
|
|
2027
2642
|
url.searchParams.set('force', 'false')
|
|
2028
2643
|
}
|
|
@@ -2050,16 +2665,22 @@ export function useUpdateResource() {
|
|
|
2050
2665
|
// lagging cache — the change appears not to have taken effect.
|
|
2051
2666
|
if (updated && typeof updated === 'object' && updated.metadata) {
|
|
2052
2667
|
queryClient.setQueriesData(
|
|
2053
|
-
{
|
|
2668
|
+
{
|
|
2669
|
+
queryKey: ['resource', variables.kind, variables.namespace, variables.name],
|
|
2670
|
+
},
|
|
2054
2671
|
(old: any) =>
|
|
2055
2672
|
old && typeof old === 'object' && 'resource' in old
|
|
2056
2673
|
? { ...old, resource: updated }
|
|
2057
|
-
: { resource: updated }
|
|
2674
|
+
: { resource: updated },
|
|
2058
2675
|
)
|
|
2059
2676
|
} else {
|
|
2060
|
-
queryClient.invalidateQueries({
|
|
2677
|
+
queryClient.invalidateQueries({
|
|
2678
|
+
queryKey: ['resource', variables.kind, variables.namespace, variables.name],
|
|
2679
|
+
})
|
|
2061
2680
|
}
|
|
2062
|
-
queryClient.invalidateQueries({
|
|
2681
|
+
queryClient.invalidateQueries({
|
|
2682
|
+
queryKey: ['resources', variables.kind],
|
|
2683
|
+
})
|
|
2063
2684
|
queryClient.invalidateQueries({ queryKey: ['topology'] })
|
|
2064
2685
|
},
|
|
2065
2686
|
})
|
|
@@ -2068,13 +2689,24 @@ export function useUpdateResource() {
|
|
|
2068
2689
|
// Cascade delete preview — shows resources that will be garbage-collected
|
|
2069
2690
|
export interface CascadeDeletePreview {
|
|
2070
2691
|
root: { kind: string; namespace: string; name: string; group?: string }
|
|
2071
|
-
dependents: {
|
|
2692
|
+
dependents: {
|
|
2693
|
+
kind: string
|
|
2694
|
+
namespace: string
|
|
2695
|
+
name: string
|
|
2696
|
+
group?: string
|
|
2697
|
+
}[]
|
|
2072
2698
|
}
|
|
2073
2699
|
|
|
2074
|
-
export function useCascadeDeletePreview(
|
|
2700
|
+
export function useCascadeDeletePreview(
|
|
2701
|
+
kind: string,
|
|
2702
|
+
namespace: string,
|
|
2703
|
+
name: string,
|
|
2704
|
+
enabled: boolean,
|
|
2705
|
+
) {
|
|
2075
2706
|
return useQuery<CascadeDeletePreview>({
|
|
2076
2707
|
queryKey: ['cascade-preview', kind, namespace, name],
|
|
2077
|
-
queryFn: () =>
|
|
2708
|
+
queryFn: () =>
|
|
2709
|
+
fetchJSON<CascadeDeletePreview>(`/resources/${kind}/${namespace}/${name}/cascade-preview`),
|
|
2078
2710
|
enabled,
|
|
2079
2711
|
staleTime: 30_000,
|
|
2080
2712
|
})
|
|
@@ -2085,8 +2717,23 @@ export function useDeleteResource() {
|
|
|
2085
2717
|
const queryClient = useQueryClient()
|
|
2086
2718
|
|
|
2087
2719
|
return useMutation({
|
|
2088
|
-
mutationFn: async ({
|
|
2089
|
-
|
|
2720
|
+
mutationFn: async ({
|
|
2721
|
+
kind,
|
|
2722
|
+
group,
|
|
2723
|
+
namespace,
|
|
2724
|
+
name,
|
|
2725
|
+
force,
|
|
2726
|
+
}: {
|
|
2727
|
+
kind: string
|
|
2728
|
+
group?: string
|
|
2729
|
+
namespace: string
|
|
2730
|
+
name: string
|
|
2731
|
+
force?: boolean
|
|
2732
|
+
}) => {
|
|
2733
|
+
const url = new URL(
|
|
2734
|
+
`${getApiBase()}/resources/${kind}/${namespace}/${name}`,
|
|
2735
|
+
window.location.origin,
|
|
2736
|
+
)
|
|
2090
2737
|
if (group) {
|
|
2091
2738
|
url.searchParams.set('group', group)
|
|
2092
2739
|
}
|
|
@@ -2108,7 +2755,9 @@ export function useDeleteResource() {
|
|
|
2108
2755
|
successMessage: 'Resource deleted',
|
|
2109
2756
|
},
|
|
2110
2757
|
onSuccess: (_, variables) => {
|
|
2111
|
-
queryClient.invalidateQueries({
|
|
2758
|
+
queryClient.invalidateQueries({
|
|
2759
|
+
queryKey: ['resources', variables.kind],
|
|
2760
|
+
})
|
|
2112
2761
|
queryClient.invalidateQueries({ queryKey: ['topology'] })
|
|
2113
2762
|
},
|
|
2114
2763
|
})
|
|
@@ -2118,10 +2767,24 @@ export function useBulkDeleteResources() {
|
|
|
2118
2767
|
const queryClient = useQueryClient()
|
|
2119
2768
|
|
|
2120
2769
|
return useMutation({
|
|
2121
|
-
mutationFn: async ({
|
|
2770
|
+
mutationFn: async ({
|
|
2771
|
+
items,
|
|
2772
|
+
force,
|
|
2773
|
+
}: {
|
|
2774
|
+
items: Array<{
|
|
2775
|
+
kind: string
|
|
2776
|
+
group?: string
|
|
2777
|
+
namespace: string
|
|
2778
|
+
name: string
|
|
2779
|
+
}>
|
|
2780
|
+
force?: boolean
|
|
2781
|
+
}) => {
|
|
2122
2782
|
const results = await Promise.allSettled(
|
|
2123
2783
|
items.map(async ({ kind, group, namespace, name }) => {
|
|
2124
|
-
const url = new URL(
|
|
2784
|
+
const url = new URL(
|
|
2785
|
+
`${getApiBase()}/resources/${kind}/${namespace}/${name}`,
|
|
2786
|
+
window.location.origin,
|
|
2787
|
+
)
|
|
2125
2788
|
if (group) url.searchParams.set('group', group)
|
|
2126
2789
|
if (force) url.searchParams.set('force', 'true')
|
|
2127
2790
|
const response = await apiFetch(url.toString(), { method: 'DELETE' })
|
|
@@ -2130,9 +2793,9 @@ export function useBulkDeleteResources() {
|
|
|
2130
2793
|
throw new Error(error.error || `Failed to delete ${namespace}/${name}`)
|
|
2131
2794
|
}
|
|
2132
2795
|
return { kind, namespace, name }
|
|
2133
|
-
})
|
|
2796
|
+
}),
|
|
2134
2797
|
)
|
|
2135
|
-
const failed = results.filter(r => r.status === 'rejected')
|
|
2798
|
+
const failed = results.filter((r) => r.status === 'rejected')
|
|
2136
2799
|
if (failed.length > 0) {
|
|
2137
2800
|
throw new Error(`Failed to delete ${failed.length} of ${items.length} resources`)
|
|
2138
2801
|
}
|
|
@@ -2165,13 +2828,19 @@ interface BulkWorkloadMutationResult {
|
|
|
2165
2828
|
}
|
|
2166
2829
|
|
|
2167
2830
|
function failedBulkWorkloadMessages(results: PromiseSettledResult<unknown>[]): string[] {
|
|
2168
|
-
return results.flatMap(r =>
|
|
2169
|
-
|
|
2170
|
-
|
|
2831
|
+
return results.flatMap((r) =>
|
|
2832
|
+
r.status === 'rejected'
|
|
2833
|
+
? [r.reason instanceof Error ? r.reason.message : String(r.reason)]
|
|
2834
|
+
: [],
|
|
2171
2835
|
)
|
|
2172
2836
|
}
|
|
2173
2837
|
|
|
2174
|
-
function bulkWorkloadFailureMessage(
|
|
2838
|
+
function bulkWorkloadFailureMessage(
|
|
2839
|
+
action: string,
|
|
2840
|
+
failed: number,
|
|
2841
|
+
total: number,
|
|
2842
|
+
messages: string[],
|
|
2843
|
+
): string {
|
|
2175
2844
|
return `Failed to ${action} ${failed} of ${total} workloads:\n${messages.join('\n')}`
|
|
2176
2845
|
}
|
|
2177
2846
|
|
|
@@ -2179,27 +2848,45 @@ export function useBulkRestartWorkloads() {
|
|
|
2179
2848
|
const queryClient = useQueryClient()
|
|
2180
2849
|
|
|
2181
2850
|
return useMutation({
|
|
2182
|
-
mutationFn: async ({
|
|
2851
|
+
mutationFn: async ({
|
|
2852
|
+
items,
|
|
2853
|
+
}: {
|
|
2854
|
+
items: BulkWorkloadItem[]
|
|
2855
|
+
}): Promise<BulkWorkloadMutationResult> => {
|
|
2183
2856
|
if (items.length === 0) {
|
|
2184
2857
|
return { requested: 0, succeeded: 0, failedMessages: [] }
|
|
2185
2858
|
}
|
|
2186
2859
|
const results = await Promise.allSettled(
|
|
2187
2860
|
items.map(async ({ kind, namespace, name }) => {
|
|
2188
|
-
const response = await apiFetch(
|
|
2189
|
-
|
|
2190
|
-
|
|
2861
|
+
const response = await apiFetch(
|
|
2862
|
+
`${getApiBase()}/workloads/${kind}/${namespace}/${name}/restart`,
|
|
2863
|
+
{
|
|
2864
|
+
method: 'POST',
|
|
2865
|
+
},
|
|
2866
|
+
)
|
|
2191
2867
|
if (!response.ok) {
|
|
2192
2868
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
2193
2869
|
throw new Error(`${namespace}/${name}: ${error.error || `HTTP ${response.status}`}`)
|
|
2194
2870
|
}
|
|
2195
2871
|
return { kind, namespace, name }
|
|
2196
|
-
})
|
|
2872
|
+
}),
|
|
2197
2873
|
)
|
|
2198
2874
|
const failedMessages = failedBulkWorkloadMessages(results)
|
|
2199
2875
|
if (failedMessages.length === items.length) {
|
|
2200
|
-
throw new Error(
|
|
2876
|
+
throw new Error(
|
|
2877
|
+
bulkWorkloadFailureMessage(
|
|
2878
|
+
'restart',
|
|
2879
|
+
failedMessages.length,
|
|
2880
|
+
items.length,
|
|
2881
|
+
failedMessages,
|
|
2882
|
+
),
|
|
2883
|
+
)
|
|
2884
|
+
}
|
|
2885
|
+
return {
|
|
2886
|
+
requested: items.length,
|
|
2887
|
+
succeeded: items.length - failedMessages.length,
|
|
2888
|
+
failedMessages,
|
|
2201
2889
|
}
|
|
2202
|
-
return { requested: items.length, succeeded: items.length - failedMessages.length, failedMessages }
|
|
2203
2890
|
},
|
|
2204
2891
|
meta: {
|
|
2205
2892
|
errorMessage: 'Failed to restart some workloads',
|
|
@@ -2225,29 +2912,44 @@ export function useBulkScaleWorkloads() {
|
|
|
2225
2912
|
const queryClient = useQueryClient()
|
|
2226
2913
|
|
|
2227
2914
|
return useMutation({
|
|
2228
|
-
mutationFn: async ({
|
|
2915
|
+
mutationFn: async ({
|
|
2916
|
+
items,
|
|
2917
|
+
replicas,
|
|
2918
|
+
}: {
|
|
2919
|
+
items: BulkWorkloadItem[]
|
|
2920
|
+
replicas: number
|
|
2921
|
+
}): Promise<BulkWorkloadMutationResult> => {
|
|
2229
2922
|
if (items.length === 0) {
|
|
2230
2923
|
return { requested: 0, succeeded: 0, failedMessages: [] }
|
|
2231
2924
|
}
|
|
2232
2925
|
const results = await Promise.allSettled(
|
|
2233
2926
|
items.map(async ({ kind, namespace, name }) => {
|
|
2234
|
-
const response = await apiFetch(
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2927
|
+
const response = await apiFetch(
|
|
2928
|
+
`${getApiBase()}/workloads/${kind}/${namespace}/${name}/scale`,
|
|
2929
|
+
{
|
|
2930
|
+
method: 'POST',
|
|
2931
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2932
|
+
body: JSON.stringify({ replicas }),
|
|
2933
|
+
},
|
|
2934
|
+
)
|
|
2239
2935
|
if (!response.ok) {
|
|
2240
2936
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
2241
2937
|
throw new Error(`${namespace}/${name}: ${error.error || `HTTP ${response.status}`}`)
|
|
2242
2938
|
}
|
|
2243
2939
|
return { kind, namespace, name }
|
|
2244
|
-
})
|
|
2940
|
+
}),
|
|
2245
2941
|
)
|
|
2246
2942
|
const failedMessages = failedBulkWorkloadMessages(results)
|
|
2247
2943
|
if (failedMessages.length === items.length) {
|
|
2248
|
-
throw new Error(
|
|
2944
|
+
throw new Error(
|
|
2945
|
+
bulkWorkloadFailureMessage('scale', failedMessages.length, items.length, failedMessages),
|
|
2946
|
+
)
|
|
2947
|
+
}
|
|
2948
|
+
return {
|
|
2949
|
+
requested: items.length,
|
|
2950
|
+
succeeded: items.length - failedMessages.length,
|
|
2951
|
+
failedMessages,
|
|
2249
2952
|
}
|
|
2250
|
-
return { requested: items.length, succeeded: items.length - failedMessages.length, failedMessages }
|
|
2251
2953
|
},
|
|
2252
2954
|
meta: {
|
|
2253
2955
|
errorMessage: 'Failed to scale some workloads',
|
|
@@ -2281,7 +2983,17 @@ export function useApplyResource() {
|
|
|
2281
2983
|
const queryClient = useQueryClient()
|
|
2282
2984
|
|
|
2283
2985
|
return useMutation({
|
|
2284
|
-
mutationFn: async ({
|
|
2986
|
+
mutationFn: async ({
|
|
2987
|
+
yaml,
|
|
2988
|
+
mode = 'apply',
|
|
2989
|
+
dryRun = false,
|
|
2990
|
+
force = false,
|
|
2991
|
+
}: {
|
|
2992
|
+
yaml: string
|
|
2993
|
+
mode?: 'apply' | 'create'
|
|
2994
|
+
dryRun?: boolean
|
|
2995
|
+
force?: boolean
|
|
2996
|
+
}) => {
|
|
2285
2997
|
const url = new URL(`${getApiBase()}/resources/apply`, window.location.origin)
|
|
2286
2998
|
url.searchParams.set('mode', mode)
|
|
2287
2999
|
if (dryRun) {
|
|
@@ -2314,11 +3026,19 @@ export function useApplyResource() {
|
|
|
2314
3026
|
// CronJob operations
|
|
2315
3027
|
// ============================================================================
|
|
2316
3028
|
|
|
2317
|
-
function invalidateCronJobOperationQueries(
|
|
3029
|
+
function invalidateCronJobOperationQueries(
|
|
3030
|
+
queryClient: ReturnType<typeof useQueryClient>,
|
|
3031
|
+
namespace: string,
|
|
3032
|
+
name: string,
|
|
3033
|
+
) {
|
|
2318
3034
|
queryClient.invalidateQueries({ queryKey: ['resources', 'cronjobs'] })
|
|
2319
3035
|
queryClient.invalidateQueries({ queryKey: ['resources', 'jobs'] })
|
|
2320
|
-
queryClient.invalidateQueries({
|
|
2321
|
-
|
|
3036
|
+
queryClient.invalidateQueries({
|
|
3037
|
+
queryKey: ['resource', 'cronjobs', namespace, name],
|
|
3038
|
+
})
|
|
3039
|
+
queryClient.invalidateQueries({
|
|
3040
|
+
queryKey: ['workload-runs', 'cronjobs', namespace, name],
|
|
3041
|
+
})
|
|
2322
3042
|
queryClient.invalidateQueries({ queryKey: ['applications'] })
|
|
2323
3043
|
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
|
|
2324
3044
|
queryClient.invalidateQueries({ queryKey: ['resource-counts'] })
|
|
@@ -2409,10 +3129,21 @@ export function useRestartWorkload() {
|
|
|
2409
3129
|
const queryClient = useQueryClient()
|
|
2410
3130
|
|
|
2411
3131
|
return useMutation({
|
|
2412
|
-
mutationFn: async ({
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
3132
|
+
mutationFn: async ({
|
|
3133
|
+
kind,
|
|
3134
|
+
namespace,
|
|
3135
|
+
name,
|
|
3136
|
+
}: {
|
|
3137
|
+
kind: string
|
|
3138
|
+
namespace: string
|
|
3139
|
+
name: string
|
|
3140
|
+
}) => {
|
|
3141
|
+
const response = await apiFetch(
|
|
3142
|
+
`${getApiBase()}/workloads/${kind}/${namespace}/${name}/restart`,
|
|
3143
|
+
{
|
|
3144
|
+
method: 'POST',
|
|
3145
|
+
},
|
|
3146
|
+
)
|
|
2416
3147
|
if (!response.ok) {
|
|
2417
3148
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
2418
3149
|
throw new Error(error.error || `HTTP ${response.status}`)
|
|
@@ -2424,7 +3155,9 @@ export function useRestartWorkload() {
|
|
|
2424
3155
|
successMessage: 'Workload restarting',
|
|
2425
3156
|
},
|
|
2426
3157
|
onSuccess: (_, variables) => {
|
|
2427
|
-
queryClient.invalidateQueries({
|
|
3158
|
+
queryClient.invalidateQueries({
|
|
3159
|
+
queryKey: ['resources', variables.kind],
|
|
3160
|
+
})
|
|
2428
3161
|
queryClient.invalidateQueries({ queryKey: ['topology'] })
|
|
2429
3162
|
},
|
|
2430
3163
|
})
|
|
@@ -2435,12 +3168,25 @@ export function useScaleWorkload() {
|
|
|
2435
3168
|
const queryClient = useQueryClient()
|
|
2436
3169
|
|
|
2437
3170
|
return useMutation({
|
|
2438
|
-
mutationFn: async ({
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
3171
|
+
mutationFn: async ({
|
|
3172
|
+
kind,
|
|
3173
|
+
namespace,
|
|
3174
|
+
name,
|
|
3175
|
+
replicas,
|
|
3176
|
+
}: {
|
|
3177
|
+
kind: string
|
|
3178
|
+
namespace: string
|
|
3179
|
+
name: string
|
|
3180
|
+
replicas: number
|
|
3181
|
+
}) => {
|
|
3182
|
+
const response = await apiFetch(
|
|
3183
|
+
`${getApiBase()}/workloads/${kind}/${namespace}/${name}/scale`,
|
|
3184
|
+
{
|
|
3185
|
+
method: 'POST',
|
|
3186
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3187
|
+
body: JSON.stringify({ replicas }),
|
|
3188
|
+
},
|
|
3189
|
+
)
|
|
2444
3190
|
if (!response.ok) {
|
|
2445
3191
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
2446
3192
|
throw new Error(error.error || `HTTP ${response.status}`)
|
|
@@ -2452,8 +3198,12 @@ export function useScaleWorkload() {
|
|
|
2452
3198
|
successMessage: 'Workload scaled',
|
|
2453
3199
|
},
|
|
2454
3200
|
onSuccess: (_, variables) => {
|
|
2455
|
-
queryClient.invalidateQueries({
|
|
2456
|
-
|
|
3201
|
+
queryClient.invalidateQueries({
|
|
3202
|
+
queryKey: ['resources', variables.kind],
|
|
3203
|
+
})
|
|
3204
|
+
queryClient.invalidateQueries({
|
|
3205
|
+
queryKey: ['resource', variables.kind, variables.namespace, variables.name],
|
|
3206
|
+
})
|
|
2457
3207
|
queryClient.invalidateQueries({ queryKey: ['topology'] })
|
|
2458
3208
|
},
|
|
2459
3209
|
})
|
|
@@ -2473,7 +3223,12 @@ export interface WorkloadRevision {
|
|
|
2473
3223
|
template?: string // Pod template spec as YAML (for revision diff)
|
|
2474
3224
|
}
|
|
2475
3225
|
|
|
2476
|
-
export function useWorkloadRevisions(
|
|
3226
|
+
export function useWorkloadRevisions(
|
|
3227
|
+
kind: string,
|
|
3228
|
+
namespace: string,
|
|
3229
|
+
name: string,
|
|
3230
|
+
enabled = true,
|
|
3231
|
+
) {
|
|
2477
3232
|
return useQuery<WorkloadRevision[]>({
|
|
2478
3233
|
queryKey: ['workload-revisions', kind, namespace, name],
|
|
2479
3234
|
queryFn: () => fetchJSON(`/workloads/${kind}/${namespace}/${name}/revisions`),
|
|
@@ -2484,12 +3239,25 @@ export function useWorkloadRevisions(kind: string, namespace: string, name: stri
|
|
|
2484
3239
|
export function useRollbackWorkload() {
|
|
2485
3240
|
const queryClient = useQueryClient()
|
|
2486
3241
|
return useMutation({
|
|
2487
|
-
mutationFn: async ({
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
3242
|
+
mutationFn: async ({
|
|
3243
|
+
kind,
|
|
3244
|
+
namespace,
|
|
3245
|
+
name,
|
|
3246
|
+
revision,
|
|
3247
|
+
}: {
|
|
3248
|
+
kind: string
|
|
3249
|
+
namespace: string
|
|
3250
|
+
name: string
|
|
3251
|
+
revision: number
|
|
3252
|
+
}) => {
|
|
3253
|
+
const response = await apiFetch(
|
|
3254
|
+
`${getApiBase()}/workloads/${kind}/${namespace}/${name}/rollback`,
|
|
3255
|
+
{
|
|
3256
|
+
method: 'POST',
|
|
3257
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3258
|
+
body: JSON.stringify({ revision }),
|
|
3259
|
+
},
|
|
3260
|
+
)
|
|
2493
3261
|
if (!response.ok) {
|
|
2494
3262
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
2495
3263
|
throw new Error(error.error || `HTTP ${response.status}`)
|
|
@@ -2501,9 +3269,15 @@ export function useRollbackWorkload() {
|
|
|
2501
3269
|
successMessage: 'Rollback initiated',
|
|
2502
3270
|
},
|
|
2503
3271
|
onSuccess: (_, variables) => {
|
|
2504
|
-
queryClient.invalidateQueries({
|
|
2505
|
-
|
|
2506
|
-
|
|
3272
|
+
queryClient.invalidateQueries({
|
|
3273
|
+
queryKey: ['resources', variables.kind],
|
|
3274
|
+
})
|
|
3275
|
+
queryClient.invalidateQueries({
|
|
3276
|
+
queryKey: ['resource', variables.kind, variables.namespace, variables.name],
|
|
3277
|
+
})
|
|
3278
|
+
queryClient.invalidateQueries({
|
|
3279
|
+
queryKey: ['workload-revisions', variables.kind, variables.namespace, variables.name],
|
|
3280
|
+
})
|
|
2507
3281
|
queryClient.invalidateQueries({ queryKey: ['topology'] })
|
|
2508
3282
|
},
|
|
2509
3283
|
})
|
|
@@ -2533,7 +3307,9 @@ export function useCordonNode() {
|
|
|
2533
3307
|
},
|
|
2534
3308
|
onSuccess: (_, variables) => {
|
|
2535
3309
|
queryClient.invalidateQueries({ queryKey: ['resources', 'nodes'] })
|
|
2536
|
-
queryClient.invalidateQueries({
|
|
3310
|
+
queryClient.invalidateQueries({
|
|
3311
|
+
queryKey: ['resource', 'nodes', '', variables.name],
|
|
3312
|
+
})
|
|
2537
3313
|
queryClient.invalidateQueries({ queryKey: ['topology'] })
|
|
2538
3314
|
},
|
|
2539
3315
|
})
|
|
@@ -2559,7 +3335,9 @@ export function useUncordonNode() {
|
|
|
2559
3335
|
},
|
|
2560
3336
|
onSuccess: (_, variables) => {
|
|
2561
3337
|
queryClient.invalidateQueries({ queryKey: ['resources', 'nodes'] })
|
|
2562
|
-
queryClient.invalidateQueries({
|
|
3338
|
+
queryClient.invalidateQueries({
|
|
3339
|
+
queryKey: ['resource', 'nodes', '', variables.name],
|
|
3340
|
+
})
|
|
2563
3341
|
queryClient.invalidateQueries({ queryKey: ['topology'] })
|
|
2564
3342
|
},
|
|
2565
3343
|
})
|
|
@@ -2592,7 +3370,9 @@ export function useDrainNode() {
|
|
|
2592
3370
|
},
|
|
2593
3371
|
onSuccess: (data: { evictedPods?: string[]; errors?: string[] }, variables) => {
|
|
2594
3372
|
queryClient.invalidateQueries({ queryKey: ['resources', 'nodes'] })
|
|
2595
|
-
queryClient.invalidateQueries({
|
|
3373
|
+
queryClient.invalidateQueries({
|
|
3374
|
+
queryKey: ['resource', 'nodes', '', variables.name],
|
|
3375
|
+
})
|
|
2596
3376
|
queryClient.invalidateQueries({ queryKey: ['topology'] })
|
|
2597
3377
|
|
|
2598
3378
|
const evicted = data?.evictedPods?.length ?? 0
|
|
@@ -2642,12 +3422,19 @@ export function useHelmRelease(namespace: string, name: string, options?: { enab
|
|
|
2642
3422
|
// `enabled` lets callers skip the query when the user's Cloud role
|
|
2643
3423
|
// would 403 the read — saves a round-trip and avoids a transient
|
|
2644
3424
|
// "error" state that the role-gated empty panel doesn't need.
|
|
2645
|
-
export function useHelmManifest(
|
|
3425
|
+
export function useHelmManifest(
|
|
3426
|
+
namespace: string,
|
|
3427
|
+
name: string,
|
|
3428
|
+
revision?: number,
|
|
3429
|
+
enabled = true,
|
|
3430
|
+
) {
|
|
2646
3431
|
const params = revision ? `?revision=${revision}` : ''
|
|
2647
3432
|
return useQuery<string>({
|
|
2648
3433
|
queryKey: ['helm-manifest', namespace, name, revision],
|
|
2649
3434
|
queryFn: async () => {
|
|
2650
|
-
const response = await apiFetch(
|
|
3435
|
+
const response = await apiFetch(
|
|
3436
|
+
`${getApiBase()}/helm/releases/${namespace}/${name}/manifest${params}`,
|
|
3437
|
+
)
|
|
2651
3438
|
if (!response.ok) {
|
|
2652
3439
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
2653
3440
|
throw new Error(error.error || `HTTP ${response.status}`)
|
|
@@ -2660,7 +3447,13 @@ export function useHelmManifest(namespace: string, name: string, revision?: numb
|
|
|
2660
3447
|
}
|
|
2661
3448
|
|
|
2662
3449
|
// Get values for a Helm release. `enabled` see useHelmManifest.
|
|
2663
|
-
export function useHelmValues(
|
|
3450
|
+
export function useHelmValues(
|
|
3451
|
+
namespace: string,
|
|
3452
|
+
name: string,
|
|
3453
|
+
allValues?: boolean,
|
|
3454
|
+
enabled = true,
|
|
3455
|
+
revision?: number,
|
|
3456
|
+
) {
|
|
2664
3457
|
const params = new URLSearchParams()
|
|
2665
3458
|
if (allValues) params.set('all', 'true')
|
|
2666
3459
|
if (revision && revision > 0) params.set('revision', String(revision))
|
|
@@ -2684,8 +3477,12 @@ export function useHelmManifestDiff(
|
|
|
2684
3477
|
return useQuery<ManifestDiff>({
|
|
2685
3478
|
queryKey: ['helm-diff', namespace, name, revision1, revision2],
|
|
2686
3479
|
queryFn: () =>
|
|
2687
|
-
fetchJSON(
|
|
2688
|
-
|
|
3480
|
+
fetchJSON(
|
|
3481
|
+
`/helm/releases/${namespace}/${name}/diff?revision1=${revision1}&revision2=${revision2}`,
|
|
3482
|
+
),
|
|
3483
|
+
enabled: Boolean(
|
|
3484
|
+
namespace && name && revision1 > 0 && revision2 > 0 && revision1 !== revision2 && enabled,
|
|
3485
|
+
),
|
|
2689
3486
|
staleTime: 60000,
|
|
2690
3487
|
})
|
|
2691
3488
|
}
|
|
@@ -2708,7 +3505,9 @@ export function useHelmValuesDiff(
|
|
|
2708
3505
|
if (allValues) params.set('all', 'true')
|
|
2709
3506
|
return fetchJSON(`/helm/releases/${namespace}/${name}/values/diff?${params.toString()}`)
|
|
2710
3507
|
},
|
|
2711
|
-
enabled: Boolean(
|
|
3508
|
+
enabled: Boolean(
|
|
3509
|
+
namespace && name && revision1 > 0 && revision2 > 0 && revision1 !== revision2 && enabled,
|
|
3510
|
+
),
|
|
2712
3511
|
staleTime: 60000,
|
|
2713
3512
|
})
|
|
2714
3513
|
}
|
|
@@ -2723,8 +3522,12 @@ export function useHelmNotesDiff(
|
|
|
2723
3522
|
return useQuery<NotesDiff>({
|
|
2724
3523
|
queryKey: ['helm-notes-diff', namespace, name, revision1, revision2],
|
|
2725
3524
|
queryFn: () =>
|
|
2726
|
-
fetchJSON(
|
|
2727
|
-
|
|
3525
|
+
fetchJSON(
|
|
3526
|
+
`/helm/releases/${namespace}/${name}/notes/diff?revision1=${revision1}&revision2=${revision2}`,
|
|
3527
|
+
),
|
|
3528
|
+
enabled: Boolean(
|
|
3529
|
+
namespace && name && revision1 > 0 && revision2 > 0 && revision1 !== revision2 && enabled,
|
|
3530
|
+
),
|
|
2728
3531
|
staleTime: 60000,
|
|
2729
3532
|
})
|
|
2730
3533
|
}
|
|
@@ -2739,8 +3542,12 @@ export function useHelmHooksDiff(
|
|
|
2739
3542
|
return useQuery<HooksDiff>({
|
|
2740
3543
|
queryKey: ['helm-hooks-diff', namespace, name, revision1, revision2],
|
|
2741
3544
|
queryFn: () =>
|
|
2742
|
-
fetchJSON(
|
|
2743
|
-
|
|
3545
|
+
fetchJSON(
|
|
3546
|
+
`/helm/releases/${namespace}/${name}/hooks/diff?revision1=${revision1}&revision2=${revision2}`,
|
|
3547
|
+
),
|
|
3548
|
+
enabled: Boolean(
|
|
3549
|
+
namespace && name && revision1 > 0 && revision2 > 0 && revision1 !== revision2 && enabled,
|
|
3550
|
+
),
|
|
2744
3551
|
staleTime: 60000,
|
|
2745
3552
|
})
|
|
2746
3553
|
}
|
|
@@ -2755,8 +3562,12 @@ export function useHelmResourceDiff(
|
|
|
2755
3562
|
return useQuery<ResourceDiff>({
|
|
2756
3563
|
queryKey: ['helm-resource-diff', namespace, name, revision1, revision2],
|
|
2757
3564
|
queryFn: () =>
|
|
2758
|
-
fetchJSON(
|
|
2759
|
-
|
|
3565
|
+
fetchJSON(
|
|
3566
|
+
`/helm/releases/${namespace}/${name}/resources/diff?revision1=${revision1}&revision2=${revision2}`,
|
|
3567
|
+
),
|
|
3568
|
+
enabled: Boolean(
|
|
3569
|
+
namespace && name && revision1 > 0 && revision2 > 0 && revision1 !== revision2 && enabled,
|
|
3570
|
+
),
|
|
2760
3571
|
staleTime: 60000,
|
|
2761
3572
|
})
|
|
2762
3573
|
}
|
|
@@ -2806,10 +3617,21 @@ export function useHelmRollback() {
|
|
|
2806
3617
|
const queryClient = useQueryClient()
|
|
2807
3618
|
|
|
2808
3619
|
return useMutation({
|
|
2809
|
-
mutationFn: async ({
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
3620
|
+
mutationFn: async ({
|
|
3621
|
+
namespace,
|
|
3622
|
+
name,
|
|
3623
|
+
revision,
|
|
3624
|
+
}: {
|
|
3625
|
+
namespace: string
|
|
3626
|
+
name: string
|
|
3627
|
+
revision: number
|
|
3628
|
+
}) => {
|
|
3629
|
+
const response = await apiFetch(
|
|
3630
|
+
`${getApiBase()}/helm/releases/${namespace}/${name}/rollback?revision=${revision}`,
|
|
3631
|
+
{
|
|
3632
|
+
method: 'POST',
|
|
3633
|
+
},
|
|
3634
|
+
)
|
|
2813
3635
|
if (!response.ok) {
|
|
2814
3636
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
2815
3637
|
throw new Error(error.error || `HTTP ${response.status}`)
|
|
@@ -2822,7 +3644,9 @@ export function useHelmRollback() {
|
|
|
2822
3644
|
},
|
|
2823
3645
|
onSuccess: (_, variables) => {
|
|
2824
3646
|
queryClient.invalidateQueries({ queryKey: ['helm-releases'] })
|
|
2825
|
-
queryClient.invalidateQueries({
|
|
3647
|
+
queryClient.invalidateQueries({
|
|
3648
|
+
queryKey: ['helm-release', variables.namespace, variables.name],
|
|
3649
|
+
})
|
|
2826
3650
|
},
|
|
2827
3651
|
})
|
|
2828
3652
|
}
|
|
@@ -2906,7 +3730,9 @@ function streamHelmProgress(
|
|
|
2906
3730
|
return
|
|
2907
3731
|
}
|
|
2908
3732
|
} catch (err) {
|
|
2909
|
-
reject(
|
|
3733
|
+
reject(
|
|
3734
|
+
err instanceof Error ? err : new Error(`${failureLabel}: invalid progress event`),
|
|
3735
|
+
)
|
|
2910
3736
|
return
|
|
2911
3737
|
}
|
|
2912
3738
|
}
|
|
@@ -2927,12 +3753,16 @@ export function upgradeWithProgress(
|
|
|
2927
3753
|
version: string,
|
|
2928
3754
|
repositoryName: string | undefined,
|
|
2929
3755
|
onProgress: (event: InstallProgressEvent) => void,
|
|
2930
|
-
values?: Record<string, unknown
|
|
3756
|
+
values?: Record<string, unknown>,
|
|
2931
3757
|
): Promise<void> {
|
|
2932
3758
|
const params = new URLSearchParams({ version })
|
|
2933
3759
|
if (repositoryName) params.set('repository', repositoryName)
|
|
2934
3760
|
const options: RequestInit = values
|
|
2935
|
-
? {
|
|
3761
|
+
? {
|
|
3762
|
+
method: 'POST',
|
|
3763
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3764
|
+
body: JSON.stringify({ values }),
|
|
3765
|
+
}
|
|
2936
3766
|
: { method: 'POST' }
|
|
2937
3767
|
return streamHelmProgress(
|
|
2938
3768
|
`${getApiBase()}/helm/releases/${namespace}/${name}/upgrade-stream?${params.toString()}`,
|
|
@@ -2947,7 +3777,7 @@ export function rollbackWithProgress(
|
|
|
2947
3777
|
namespace: string,
|
|
2948
3778
|
name: string,
|
|
2949
3779
|
revision: number,
|
|
2950
|
-
onProgress: (event: InstallProgressEvent) => void
|
|
3780
|
+
onProgress: (event: InstallProgressEvent) => void,
|
|
2951
3781
|
): Promise<void> {
|
|
2952
3782
|
return streamHelmProgress(
|
|
2953
3783
|
`${getApiBase()}/helm/releases/${namespace}/${name}/rollback-stream?revision=${revision}`,
|
|
@@ -2960,13 +3790,26 @@ export function rollbackWithProgress(
|
|
|
2960
3790
|
// When `version` is supplied, preview renders against that target chart version
|
|
2961
3791
|
// instead of the release's current chart.
|
|
2962
3792
|
export function useHelmPreviewValues() {
|
|
2963
|
-
return useMutation<
|
|
3793
|
+
return useMutation<
|
|
3794
|
+
ValuesPreviewResponse,
|
|
3795
|
+
Error,
|
|
3796
|
+
{
|
|
3797
|
+
namespace: string
|
|
3798
|
+
name: string
|
|
3799
|
+
values: Record<string, unknown>
|
|
3800
|
+
version?: string
|
|
3801
|
+
repository?: string
|
|
3802
|
+
}
|
|
3803
|
+
>({
|
|
2964
3804
|
mutationFn: async ({ namespace, name, values, version, repository }) => {
|
|
2965
|
-
const response = await apiFetch(
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
3805
|
+
const response = await apiFetch(
|
|
3806
|
+
`${getApiBase()}/helm/releases/${namespace}/${name}/values/preview`,
|
|
3807
|
+
{
|
|
3808
|
+
method: 'POST',
|
|
3809
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3810
|
+
body: JSON.stringify({ values, version, repository }),
|
|
3811
|
+
},
|
|
3812
|
+
)
|
|
2970
3813
|
if (!response.ok) {
|
|
2971
3814
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
2972
3815
|
throw new Error(error.error || `HTTP ${response.status}`)
|
|
@@ -2981,7 +3824,15 @@ export function useHelmApplyValues() {
|
|
|
2981
3824
|
const queryClient = useQueryClient()
|
|
2982
3825
|
|
|
2983
3826
|
return useMutation({
|
|
2984
|
-
mutationFn: async ({
|
|
3827
|
+
mutationFn: async ({
|
|
3828
|
+
namespace,
|
|
3829
|
+
name,
|
|
3830
|
+
values,
|
|
3831
|
+
}: {
|
|
3832
|
+
namespace: string
|
|
3833
|
+
name: string
|
|
3834
|
+
values: Record<string, unknown>
|
|
3835
|
+
}) => {
|
|
2985
3836
|
const response = await apiFetch(`${getApiBase()}/helm/releases/${namespace}/${name}/values`, {
|
|
2986
3837
|
method: 'PUT',
|
|
2987
3838
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -2999,8 +3850,12 @@ export function useHelmApplyValues() {
|
|
|
2999
3850
|
},
|
|
3000
3851
|
onSuccess: (_, variables) => {
|
|
3001
3852
|
queryClient.invalidateQueries({ queryKey: ['helm-releases'] })
|
|
3002
|
-
queryClient.invalidateQueries({
|
|
3003
|
-
|
|
3853
|
+
queryClient.invalidateQueries({
|
|
3854
|
+
queryKey: ['helm-release', variables.namespace, variables.name],
|
|
3855
|
+
})
|
|
3856
|
+
queryClient.invalidateQueries({
|
|
3857
|
+
queryKey: ['helm-values', variables.namespace, variables.name],
|
|
3858
|
+
})
|
|
3004
3859
|
},
|
|
3005
3860
|
})
|
|
3006
3861
|
}
|
|
@@ -3097,7 +3952,10 @@ export function useAddOCISource() {
|
|
|
3097
3952
|
const queryClient = useQueryClient()
|
|
3098
3953
|
return useMutation({
|
|
3099
3954
|
mutationFn: (source: string) => mutateOCISource('POST', source),
|
|
3100
|
-
meta: {
|
|
3955
|
+
meta: {
|
|
3956
|
+
errorMessage: 'Failed to add chart source',
|
|
3957
|
+
successMessage: 'Chart source added',
|
|
3958
|
+
},
|
|
3101
3959
|
onSuccess: () => invalidateHelmAfterSourceChange(queryClient),
|
|
3102
3960
|
})
|
|
3103
3961
|
}
|
|
@@ -3106,7 +3964,10 @@ export function useRemoveOCISource() {
|
|
|
3106
3964
|
const queryClient = useQueryClient()
|
|
3107
3965
|
return useMutation({
|
|
3108
3966
|
mutationFn: (source: string) => mutateOCISource('DELETE', source),
|
|
3109
|
-
meta: {
|
|
3967
|
+
meta: {
|
|
3968
|
+
errorMessage: 'Failed to remove chart source',
|
|
3969
|
+
successMessage: 'Chart source removed',
|
|
3970
|
+
},
|
|
3110
3971
|
onSuccess: () => invalidateHelmAfterSourceChange(queryClient),
|
|
3111
3972
|
})
|
|
3112
3973
|
}
|
|
@@ -3178,11 +4039,15 @@ export interface InstallProgressEvent {
|
|
|
3178
4039
|
// Install a chart with progress streaming via SSE
|
|
3179
4040
|
export function installChartWithProgress(
|
|
3180
4041
|
req: InstallChartRequest,
|
|
3181
|
-
onProgress: (event: InstallProgressEvent) => void
|
|
4042
|
+
onProgress: (event: InstallProgressEvent) => void,
|
|
3182
4043
|
): Promise<HelmRelease> {
|
|
3183
4044
|
return streamHelmProgress(
|
|
3184
4045
|
`${getApiBase()}/helm/releases/install-stream`,
|
|
3185
|
-
{
|
|
4046
|
+
{
|
|
4047
|
+
method: 'POST',
|
|
4048
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4049
|
+
body: JSON.stringify(req),
|
|
4050
|
+
},
|
|
3186
4051
|
onProgress,
|
|
3187
4052
|
'Install failed',
|
|
3188
4053
|
).then((event) => event.release as HelmRelease)
|
|
@@ -3198,8 +4063,14 @@ export type ArtifactHubSortOption = 'relevance' | 'stars' | 'last_updated'
|
|
|
3198
4063
|
// Search charts on ArtifactHub
|
|
3199
4064
|
export function useArtifactHubSearch(
|
|
3200
4065
|
query: string,
|
|
3201
|
-
options?: {
|
|
3202
|
-
|
|
4066
|
+
options?: {
|
|
4067
|
+
offset?: number
|
|
4068
|
+
limit?: number
|
|
4069
|
+
official?: boolean
|
|
4070
|
+
verified?: boolean
|
|
4071
|
+
sort?: ArtifactHubSortOption
|
|
4072
|
+
},
|
|
4073
|
+
enabled = true,
|
|
3203
4074
|
) {
|
|
3204
4075
|
const params = new URLSearchParams()
|
|
3205
4076
|
if (query) params.set('query', query)
|
|
@@ -3210,7 +4081,15 @@ export function useArtifactHubSearch(
|
|
|
3210
4081
|
if (options?.sort && options.sort !== 'relevance') params.set('sort', options.sort)
|
|
3211
4082
|
|
|
3212
4083
|
return useQuery<ArtifactHubSearchResult>({
|
|
3213
|
-
queryKey: [
|
|
4084
|
+
queryKey: [
|
|
4085
|
+
'artifacthub-search',
|
|
4086
|
+
query,
|
|
4087
|
+
options?.offset,
|
|
4088
|
+
options?.limit,
|
|
4089
|
+
options?.official,
|
|
4090
|
+
options?.verified,
|
|
4091
|
+
options?.sort,
|
|
4092
|
+
],
|
|
3214
4093
|
queryFn: () => fetchJSON(`/helm/artifacthub/search?${params.toString()}`),
|
|
3215
4094
|
enabled: enabled && query.length > 0,
|
|
3216
4095
|
staleTime: 60000, // 1 minute
|
|
@@ -3218,7 +4097,12 @@ export function useArtifactHubSearch(
|
|
|
3218
4097
|
}
|
|
3219
4098
|
|
|
3220
4099
|
// Get chart detail from ArtifactHub
|
|
3221
|
-
export function useArtifactHubChart(
|
|
4100
|
+
export function useArtifactHubChart(
|
|
4101
|
+
repoName: string,
|
|
4102
|
+
chartName: string,
|
|
4103
|
+
version?: string,
|
|
4104
|
+
enabled = true,
|
|
4105
|
+
) {
|
|
3222
4106
|
const path = version
|
|
3223
4107
|
? `/helm/artifacthub/charts/${repoName}/${chartName}/${version}`
|
|
3224
4108
|
: `/helm/artifacthub/charts/${repoName}/${chartName}`
|
|
@@ -3239,7 +4123,8 @@ interface GitOpsMutationConfig<TVariables> {
|
|
|
3239
4123
|
getPath: (variables: TVariables) => string
|
|
3240
4124
|
getBody?: (variables: TVariables) => unknown
|
|
3241
4125
|
errorMessage: string
|
|
3242
|
-
successMessage
|
|
4126
|
+
successMessage?: string
|
|
4127
|
+
getSuccessMessage?: (data: GitOpsOperationResponse) => string
|
|
3243
4128
|
getInvalidateKeys: (variables: TVariables) => (string | undefined)[][]
|
|
3244
4129
|
}
|
|
3245
4130
|
|
|
@@ -3267,9 +4152,10 @@ function createGitOpsMutation<TVariables>(config: GitOpsMutationConfig<TVariable
|
|
|
3267
4152
|
errorMessage: config.errorMessage,
|
|
3268
4153
|
successMessage: config.successMessage,
|
|
3269
4154
|
},
|
|
3270
|
-
onSuccess: (
|
|
3271
|
-
config.
|
|
3272
|
-
|
|
4155
|
+
onSuccess: (data, variables) => {
|
|
4156
|
+
if (config.getSuccessMessage) showApiSuccess(config.getSuccessMessage(data))
|
|
4157
|
+
config.getInvalidateKeys(variables).forEach((key) =>
|
|
4158
|
+
queryClient.invalidateQueries({ queryKey: key }),
|
|
3273
4159
|
)
|
|
3274
4160
|
},
|
|
3275
4161
|
})
|
|
@@ -3284,8 +4170,13 @@ type ArgoAppVars = { namespace: string; name: string }
|
|
|
3284
4170
|
// ArgoSyncVars extends ArgoAppVars with the sync request body fields. Only
|
|
3285
4171
|
// useArgoSync sends these — splitting the type prevents callers from passing
|
|
3286
4172
|
// resources/revision/prune to mutations that would silently drop them.
|
|
3287
|
-
type ArgoSyncVars = ArgoAppVars & {
|
|
3288
|
-
resources?: Array<{
|
|
4173
|
+
export type ArgoSyncVars = ArgoAppVars & {
|
|
4174
|
+
resources?: Array<{
|
|
4175
|
+
group?: string
|
|
4176
|
+
kind: string
|
|
4177
|
+
namespace?: string
|
|
4178
|
+
name: string
|
|
4179
|
+
}>
|
|
3289
4180
|
revision?: string
|
|
3290
4181
|
prune?: boolean
|
|
3291
4182
|
dryRun?: boolean
|
|
@@ -3297,6 +4188,31 @@ type ArgoSyncVars = ArgoAppVars & {
|
|
|
3297
4188
|
syncOptions?: string[]
|
|
3298
4189
|
}
|
|
3299
4190
|
|
|
4191
|
+
export interface ArgoResourceValidationResult {
|
|
4192
|
+
outcome: 'succeeded' | 'failed' | 'inconclusive'
|
|
4193
|
+
message: string
|
|
4194
|
+
resource?: {
|
|
4195
|
+
group?: string
|
|
4196
|
+
kind: string
|
|
4197
|
+
namespace?: string
|
|
4198
|
+
name: string
|
|
4199
|
+
status?: string
|
|
4200
|
+
message?: string
|
|
4201
|
+
}
|
|
4202
|
+
}
|
|
4203
|
+
|
|
4204
|
+
export function buildArgoResourceSyncVars(namespace: string, name: string, resource: GitOpsInsightRef, opts: ArgoSyncOpts): ArgoSyncVars {
|
|
4205
|
+
return {
|
|
4206
|
+
namespace,
|
|
4207
|
+
name,
|
|
4208
|
+
...opts,
|
|
4209
|
+
resources: [{ group: resource.group, kind: resource.kind, namespace: resource.namespace, name: resource.name }],
|
|
4210
|
+
revision: undefined,
|
|
4211
|
+
prune: false,
|
|
4212
|
+
applyOnly: false,
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
4215
|
+
|
|
3300
4216
|
// ArgoRollbackVars targets a specific Argo history entry by ID. Prune and
|
|
3301
4217
|
// DryRun mirror the sync flags so the rollback dialog can offer the same
|
|
3302
4218
|
// safety net.
|
|
@@ -3378,6 +4294,31 @@ export const useArgoSync = createGitOpsMutation<ArgoSyncVars>({
|
|
|
3378
4294
|
getInvalidateKeys: argoInvalidateKeys,
|
|
3379
4295
|
})
|
|
3380
4296
|
|
|
4297
|
+
export function useArgoResourceValidation() {
|
|
4298
|
+
const queryClient = useQueryClient()
|
|
4299
|
+
return useMutation<ArgoResourceValidationResult, Error, ArgoSyncVars>({
|
|
4300
|
+
mutationFn: async (variables) => {
|
|
4301
|
+
const response = await apiFetch(`${getApiBase()}/argo/applications/${variables.namespace}/${variables.name}/validate-resource`, {
|
|
4302
|
+
method: 'POST',
|
|
4303
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4304
|
+
body: JSON.stringify({
|
|
4305
|
+
resources: variables.resources,
|
|
4306
|
+
force: variables.force,
|
|
4307
|
+
syncOptions: variables.syncOptions,
|
|
4308
|
+
}),
|
|
4309
|
+
})
|
|
4310
|
+
if (!response.ok) {
|
|
4311
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
4312
|
+
throw new Error(error.error || `HTTP ${response.status}`)
|
|
4313
|
+
}
|
|
4314
|
+
return response.json() as Promise<ArgoResourceValidationResult>
|
|
4315
|
+
},
|
|
4316
|
+
onSettled: (_, __, variables) => {
|
|
4317
|
+
argoInvalidateKeys(variables).forEach(key => queryClient.invalidateQueries({ queryKey: key }))
|
|
4318
|
+
},
|
|
4319
|
+
})
|
|
4320
|
+
}
|
|
4321
|
+
|
|
3381
4322
|
export const useArgoRollback = createGitOpsMutation<ArgoRollbackVars>({
|
|
3382
4323
|
getPath: (v) => `/argo/applications/${v.namespace}/${v.name}/rollback`,
|
|
3383
4324
|
getBody: (v) => ({ id: v.id, prune: v.prune, dryRun: v.dryRun }),
|
|
@@ -3389,7 +4330,7 @@ export const useArgoRollback = createGitOpsMutation<ArgoRollbackVars>({
|
|
|
3389
4330
|
export const useArgoTerminate = createGitOpsMutation<ArgoAppVars>({
|
|
3390
4331
|
getPath: (v) => `/argo/applications/${v.namespace}/${v.name}/terminate`,
|
|
3391
4332
|
errorMessage: 'Failed to terminate sync',
|
|
3392
|
-
|
|
4333
|
+
getSuccessMessage: (data) => data.message,
|
|
3393
4334
|
getInvalidateKeys: argoInvalidateKeys,
|
|
3394
4335
|
})
|
|
3395
4336
|
|
|
@@ -3412,11 +4353,22 @@ export function useArgoRefresh() {
|
|
|
3412
4353
|
const queryClient = useQueryClient()
|
|
3413
4354
|
|
|
3414
4355
|
return useMutation({
|
|
3415
|
-
mutationFn: async ({
|
|
4356
|
+
mutationFn: async ({
|
|
4357
|
+
namespace,
|
|
4358
|
+
name,
|
|
4359
|
+
hard = false,
|
|
4360
|
+
}: {
|
|
4361
|
+
namespace: string
|
|
4362
|
+
name: string
|
|
4363
|
+
hard?: boolean
|
|
4364
|
+
}) => {
|
|
3416
4365
|
const params = hard ? '?type=hard' : ''
|
|
3417
|
-
const response = await apiFetch(
|
|
3418
|
-
|
|
3419
|
-
|
|
4366
|
+
const response = await apiFetch(
|
|
4367
|
+
`${getApiBase()}/argo/applications/${namespace}/${name}/refresh${params}`,
|
|
4368
|
+
{
|
|
4369
|
+
method: 'POST',
|
|
4370
|
+
},
|
|
4371
|
+
)
|
|
3420
4372
|
if (!response.ok) {
|
|
3421
4373
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
3422
4374
|
throw new Error(error.error || `HTTP ${response.status}`)
|
|
@@ -3433,7 +4385,7 @@ export function useArgoRefresh() {
|
|
|
3433
4385
|
// Refresh — without these two extra keys the user clicks Refresh and
|
|
3434
4386
|
// sees stale insight/tree data until the next staleTime tick.
|
|
3435
4387
|
argoInvalidateKeys(variables).forEach((key) =>
|
|
3436
|
-
queryClient.invalidateQueries({ queryKey: key })
|
|
4388
|
+
queryClient.invalidateQueries({ queryKey: key }),
|
|
3437
4389
|
)
|
|
3438
4390
|
},
|
|
3439
4391
|
})
|
|
@@ -3491,7 +4443,9 @@ export function useSwitchContext() {
|
|
|
3491
4443
|
} catch (error) {
|
|
3492
4444
|
clearTimeout(timeoutId)
|
|
3493
4445
|
if (error instanceof Error && error.name === 'AbortError') {
|
|
3494
|
-
throw new Error('Context switch timed out. The cluster may be unreachable.', {
|
|
4446
|
+
throw new Error('Context switch timed out. The cluster may be unreachable.', {
|
|
4447
|
+
cause: error,
|
|
4448
|
+
})
|
|
3495
4449
|
}
|
|
3496
4450
|
throw error
|
|
3497
4451
|
}
|
|
@@ -3609,9 +4563,12 @@ export function useSetActiveNamespace() {
|
|
|
3609
4563
|
error: error instanceof Error ? error.message : String(error),
|
|
3610
4564
|
})
|
|
3611
4565
|
if (error instanceof Error && error.name === 'AbortError') {
|
|
3612
|
-
throw new Error(
|
|
3613
|
-
|
|
3614
|
-
|
|
4566
|
+
throw new Error(
|
|
4567
|
+
isRescope
|
|
4568
|
+
? 'Namespace rescope timed out. The cluster may still be loading.'
|
|
4569
|
+
: 'Namespace switch timed out. The cluster may be unreachable.',
|
|
4570
|
+
{ cause: error },
|
|
4571
|
+
)
|
|
3615
4572
|
}
|
|
3616
4573
|
throw error
|
|
3617
4574
|
}
|
|
@@ -3623,7 +4580,9 @@ export function useSetActiveNamespace() {
|
|
|
3623
4580
|
accessibleCount: scope.accessibleNamespaces.length,
|
|
3624
4581
|
})
|
|
3625
4582
|
if (scope.cacheScoped) {
|
|
3626
|
-
queryClient.removeQueries({
|
|
4583
|
+
queryClient.removeQueries({
|
|
4584
|
+
predicate: (query) => query.queryKey[0] !== 'namespace-scope',
|
|
4585
|
+
})
|
|
3627
4586
|
}
|
|
3628
4587
|
queryClient.setQueryData<NamespaceScope>(['namespace-scope'], scope)
|
|
3629
4588
|
if (scope.cacheScoped) {
|
|
@@ -3654,7 +4613,7 @@ export function useImageMetadata(
|
|
|
3654
4613
|
namespace: string,
|
|
3655
4614
|
podName: string,
|
|
3656
4615
|
pullSecrets: string[],
|
|
3657
|
-
enabled = true
|
|
4616
|
+
enabled = true,
|
|
3658
4617
|
) {
|
|
3659
4618
|
const params = new URLSearchParams()
|
|
3660
4619
|
params.set('image', image)
|
|
@@ -3677,7 +4636,7 @@ export function useImageFilesystem(
|
|
|
3677
4636
|
namespace: string,
|
|
3678
4637
|
podName: string,
|
|
3679
4638
|
pullSecrets: string[],
|
|
3680
|
-
enabled = true
|
|
4639
|
+
enabled = true,
|
|
3681
4640
|
) {
|
|
3682
4641
|
const params = new URLSearchParams()
|
|
3683
4642
|
params.set('image', image)
|
|
@@ -3690,9 +4649,7 @@ export function useImageFilesystem(
|
|
|
3690
4649
|
return useQuery<ImageFilesystem>({
|
|
3691
4650
|
queryKey: ['image-filesystem', image, namespace, podName, pullSecrets.join(',')],
|
|
3692
4651
|
// Use skipToken to completely prevent the query from running when disabled
|
|
3693
|
-
queryFn: shouldFetch
|
|
3694
|
-
? () => fetchJSON(`/images/inspect?${params.toString()}`)
|
|
3695
|
-
: skipToken,
|
|
4652
|
+
queryFn: shouldFetch ? () => fetchJSON(`/images/inspect?${params.toString()}`) : skipToken,
|
|
3696
4653
|
staleTime: 300000, // 5 minutes - image content doesn't change
|
|
3697
4654
|
retry: false, // Don't retry on auth errors
|
|
3698
4655
|
})
|
|
@@ -3766,7 +4723,13 @@ export function useWorkloadPods(kind: string, namespace: string, name: string) {
|
|
|
3766
4723
|
})
|
|
3767
4724
|
}
|
|
3768
4725
|
|
|
3769
|
-
export function useWorkloadRuns(
|
|
4726
|
+
export function useWorkloadRuns(
|
|
4727
|
+
kind: string,
|
|
4728
|
+
namespace: string,
|
|
4729
|
+
name: string,
|
|
4730
|
+
enabled = true,
|
|
4731
|
+
options?: { refetchActive?: boolean; clusterScoped?: boolean },
|
|
4732
|
+
) {
|
|
3770
4733
|
const clusterScoped = options?.clusterScoped ?? false
|
|
3771
4734
|
const ns = clusterScoped ? '_' : namespace
|
|
3772
4735
|
const params = new URLSearchParams()
|
|
@@ -3775,11 +4738,12 @@ export function useWorkloadRuns(kind: string, namespace: string, name: string, e
|
|
|
3775
4738
|
|
|
3776
4739
|
return useQuery<WorkloadRunsResponse>({
|
|
3777
4740
|
queryKey: ['workload-runs', kind, namespace, name, clusterScoped],
|
|
3778
|
-
queryFn: () =>
|
|
4741
|
+
queryFn: () =>
|
|
4742
|
+
fetchJSON(`/workloads/${kind}/${ns}/${name}/runs${queryString ? `?${queryString}` : ''}`),
|
|
3779
4743
|
enabled: enabled && Boolean(kind && name && (namespace || clusterScoped)),
|
|
3780
4744
|
staleTime: 10000,
|
|
3781
4745
|
refetchInterval: options?.refetchActive
|
|
3782
|
-
? (query) => query.state.data?.runs?.some((run) => run.active) ? 5000 : 30000
|
|
4746
|
+
? (query) => (query.state.data?.runs?.some((run) => run.active) ? 5000 : 30000)
|
|
3783
4747
|
: false,
|
|
3784
4748
|
})
|
|
3785
4749
|
}
|
|
@@ -3793,7 +4757,7 @@ export function useWorkloadLogs(
|
|
|
3793
4757
|
container?: string
|
|
3794
4758
|
tailLines?: number
|
|
3795
4759
|
sinceSeconds?: number
|
|
3796
|
-
}
|
|
4760
|
+
},
|
|
3797
4761
|
) {
|
|
3798
4762
|
const params = new URLSearchParams()
|
|
3799
4763
|
if (options?.container) params.set('container', options.container)
|
|
@@ -3802,8 +4766,19 @@ export function useWorkloadLogs(
|
|
|
3802
4766
|
const queryString = params.toString()
|
|
3803
4767
|
|
|
3804
4768
|
return useQuery<WorkloadLogsResponse>({
|
|
3805
|
-
queryKey: [
|
|
3806
|
-
|
|
4769
|
+
queryKey: [
|
|
4770
|
+
'workload-logs',
|
|
4771
|
+
kind,
|
|
4772
|
+
namespace,
|
|
4773
|
+
name,
|
|
4774
|
+
options?.container,
|
|
4775
|
+
options?.tailLines,
|
|
4776
|
+
options?.sinceSeconds,
|
|
4777
|
+
],
|
|
4778
|
+
queryFn: () =>
|
|
4779
|
+
fetchJSON(
|
|
4780
|
+
`/workloads/${kind}/${namespace}/${name}/logs${queryString ? `?${queryString}` : ''}`,
|
|
4781
|
+
),
|
|
3807
4782
|
enabled: Boolean(kind && namespace && name),
|
|
3808
4783
|
staleTime: 5000,
|
|
3809
4784
|
})
|
|
@@ -3818,7 +4793,7 @@ export function createWorkloadLogStream(
|
|
|
3818
4793
|
container?: string
|
|
3819
4794
|
tailLines?: number
|
|
3820
4795
|
sinceSeconds?: number
|
|
3821
|
-
}
|
|
4796
|
+
},
|
|
3822
4797
|
): EventSource {
|
|
3823
4798
|
const params = new URLSearchParams()
|
|
3824
4799
|
if (options?.container) params.set('container', options.container)
|
|
@@ -3826,9 +4801,12 @@ export function createWorkloadLogStream(
|
|
|
3826
4801
|
if (options?.sinceSeconds) params.set('sinceSeconds', String(options.sinceSeconds))
|
|
3827
4802
|
const queryString = params.toString()
|
|
3828
4803
|
|
|
3829
|
-
return new EventSource(
|
|
3830
|
-
|
|
3831
|
-
|
|
4804
|
+
return new EventSource(
|
|
4805
|
+
`${getApiBase()}/workloads/${kind}/${namespace}/${name}/logs/stream${queryString ? `?${queryString}` : ''}`,
|
|
4806
|
+
{
|
|
4807
|
+
withCredentials: getCredentialsMode() === 'include',
|
|
4808
|
+
},
|
|
4809
|
+
)
|
|
3832
4810
|
}
|
|
3833
4811
|
|
|
3834
4812
|
// ============================================================================
|