@skyhook-io/radar-app 1.8.7 → 1.8.8
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 +58 -50
- package/src/api/client.argoResourceSync.test.ts +69 -0
- package/src/api/client.rightsizing.test.ts +32 -0
- package/src/api/client.ts +1222 -234
- 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/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/index.css +5 -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
|
|
@@ -292,7 +320,15 @@ export interface DashboardCRDCount {
|
|
|
292
320
|
}
|
|
293
321
|
|
|
294
322
|
// Re-export shared types from k8s-ui — single source of truth
|
|
295
|
-
import type {
|
|
323
|
+
import type {
|
|
324
|
+
AuditCardData,
|
|
325
|
+
AuditFinding,
|
|
326
|
+
ResourceGroup,
|
|
327
|
+
CheckMeta,
|
|
328
|
+
Check,
|
|
329
|
+
Issue,
|
|
330
|
+
IssueRecentChange,
|
|
331
|
+
} from '@skyhook-io/k8s-ui'
|
|
296
332
|
export type DashboardAudit = AuditCardData
|
|
297
333
|
export type { AuditFinding, ResourceGroup, CheckMeta, Check }
|
|
298
334
|
|
|
@@ -361,7 +397,11 @@ export interface DashboardResponse {
|
|
|
361
397
|
networkPolicyCoverage: DashboardNetworkPolicyCoverage | null
|
|
362
398
|
audit: DashboardAudit | null
|
|
363
399
|
gitopsControllers: DashboardGitOpsControllers | null
|
|
364
|
-
nodeVersionSkew: {
|
|
400
|
+
nodeVersionSkew: {
|
|
401
|
+
versions: Record<string, string[]>
|
|
402
|
+
minVersion: string
|
|
403
|
+
maxVersion: string
|
|
404
|
+
} | null
|
|
365
405
|
deferredLoading?: boolean // True while deferred informers (secrets, events, etc.) are still syncing
|
|
366
406
|
partialData?: string[] // Critical kinds promoted at first paint that haven't yet finished syncing (live-filtered)
|
|
367
407
|
accessRestricted?: boolean // True when user has no namespace access (RBAC)
|
|
@@ -437,7 +477,13 @@ export function useResourceAudit(kind: string, namespace: string, name: string)
|
|
|
437
477
|
// the "Operational Issues" section in the resource detail. Cluster-scoped
|
|
438
478
|
// resources pass "_" for namespace; namespaced ones also scope the scan via
|
|
439
479
|
// ?namespaces= for a cheap, bounded Compose.
|
|
440
|
-
export function useResourceIssues(
|
|
480
|
+
export function useResourceIssues(
|
|
481
|
+
kind: string,
|
|
482
|
+
group: string | undefined,
|
|
483
|
+
namespace: string,
|
|
484
|
+
name: string,
|
|
485
|
+
enabled = true,
|
|
486
|
+
) {
|
|
441
487
|
const clusterScoped = !namespace
|
|
442
488
|
const pathNs = clusterScoped ? '_' : encodeURIComponent(namespace)
|
|
443
489
|
const params = new URLSearchParams()
|
|
@@ -550,7 +596,8 @@ export interface OpenCostNamespaceCost {
|
|
|
550
596
|
idleCost?: number
|
|
551
597
|
}
|
|
552
598
|
|
|
553
|
-
export type CostUnavailableReason =
|
|
599
|
+
export type CostUnavailableReason =
|
|
600
|
+
'no_prometheus' | 'no_metrics' | 'query_error' | 'access_denied' | 'not_found'
|
|
554
601
|
|
|
555
602
|
export interface OpenCostSummary {
|
|
556
603
|
available: boolean
|
|
@@ -564,11 +611,41 @@ export interface OpenCostSummary {
|
|
|
564
611
|
namespaces?: OpenCostNamespaceCost[]
|
|
565
612
|
}
|
|
566
613
|
|
|
614
|
+
const noPrometheusFirstSeenAt = new Map<string, number>()
|
|
615
|
+
|
|
616
|
+
function costRefetchInterval(
|
|
617
|
+
defaultInterval: number | false = COST_REFRESH_INTERVAL_MS,
|
|
618
|
+
contextName?: string,
|
|
619
|
+
) {
|
|
620
|
+
return (query: {
|
|
621
|
+
queryHash?: string
|
|
622
|
+
queryKey?: unknown
|
|
623
|
+
state: {
|
|
624
|
+
data?: { available?: boolean; reason?: CostUnavailableReason }
|
|
625
|
+
dataUpdatedAt?: number
|
|
626
|
+
}
|
|
627
|
+
}) => {
|
|
628
|
+
const data = query.state.data
|
|
629
|
+
const queryID = `${contextName ?? 'unknown'}:${query.queryHash ?? JSON.stringify(query.queryKey ?? 'opencost')}`
|
|
630
|
+
if (data?.available === false && data.reason === 'no_prometheus') {
|
|
631
|
+
const now = Date.now()
|
|
632
|
+
const firstSeenAt = noPrometheusFirstSeenAt.get(queryID) ?? now
|
|
633
|
+
noPrometheusFirstSeenAt.set(queryID, firstSeenAt)
|
|
634
|
+
return now - firstSeenAt < COST_DISCOVERY_GRACE_MS
|
|
635
|
+
? COST_DISCOVERY_RETRY_INTERVAL_MS
|
|
636
|
+
: defaultInterval
|
|
637
|
+
}
|
|
638
|
+
noPrometheusFirstSeenAt.delete(queryID)
|
|
639
|
+
return defaultInterval
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
567
643
|
export function useOpenCostSummary() {
|
|
644
|
+
const clusterInfo = useClusterInfo()
|
|
568
645
|
return useQuery<OpenCostSummary>({
|
|
569
646
|
queryKey: ['opencost-summary'],
|
|
570
647
|
queryFn: () => fetchJSON('/opencost/summary'),
|
|
571
|
-
refetchInterval: COST_REFRESH_INTERVAL_MS,
|
|
648
|
+
refetchInterval: costRefetchInterval(COST_REFRESH_INTERVAL_MS, clusterInfo.data?.context),
|
|
572
649
|
staleTime: 30000,
|
|
573
650
|
placeholderData: (prev) => prev, // Keep previous data visible during refetch
|
|
574
651
|
})
|
|
@@ -584,6 +661,10 @@ export interface OpenCostWorkloadCost {
|
|
|
584
661
|
replicas: number
|
|
585
662
|
cpuUsageCost?: number
|
|
586
663
|
memoryUsageCost?: number
|
|
664
|
+
cpuUsageAvailable: boolean
|
|
665
|
+
memoryUsageAvailable: boolean
|
|
666
|
+
cpuAllocationUse: number
|
|
667
|
+
memoryAllocationUse: number
|
|
587
668
|
efficiency?: number
|
|
588
669
|
idleCost?: number
|
|
589
670
|
}
|
|
@@ -596,11 +677,41 @@ export interface OpenCostWorkloadResponse {
|
|
|
596
677
|
}
|
|
597
678
|
|
|
598
679
|
export function useOpenCostWorkloads(namespace: string, options?: { enabled?: boolean }) {
|
|
680
|
+
const clusterInfo = useClusterInfo()
|
|
599
681
|
return useQuery<OpenCostWorkloadResponse>({
|
|
600
682
|
queryKey: ['opencost-workloads', namespace],
|
|
601
683
|
queryFn: () => fetchJSON(`/opencost/workloads?namespace=${encodeURIComponent(namespace)}`),
|
|
602
684
|
enabled: (options?.enabled ?? true) && Boolean(namespace),
|
|
685
|
+
refetchInterval: costRefetchInterval(COST_REFRESH_INTERVAL_MS, clusterInfo.data?.context),
|
|
686
|
+
staleTime: 30000,
|
|
687
|
+
})
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
export interface OpenCostWorkloadDetailResponse {
|
|
691
|
+
available: boolean
|
|
692
|
+
reason?: CostUnavailableReason
|
|
693
|
+
namespace: string
|
|
694
|
+
kind: string
|
|
695
|
+
name: string
|
|
696
|
+
current?: OpenCostWorkloadCost
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
export function useOpenCostWorkload(
|
|
700
|
+
kind: string,
|
|
701
|
+
namespace: string,
|
|
702
|
+
name: string,
|
|
703
|
+
options?: { enabled?: boolean },
|
|
704
|
+
) {
|
|
705
|
+
const clusterInfo = useClusterInfo()
|
|
706
|
+
return useQuery<OpenCostWorkloadDetailResponse>({
|
|
707
|
+
queryKey: ['opencost-workload', kind, namespace, name],
|
|
708
|
+
queryFn: () =>
|
|
709
|
+
fetchJSON(
|
|
710
|
+
`/opencost/workload/${encodeURIComponent(kind)}/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`,
|
|
711
|
+
),
|
|
712
|
+
enabled: (options?.enabled ?? true) && Boolean(kind && namespace && name),
|
|
603
713
|
staleTime: 30000,
|
|
714
|
+
refetchInterval: costRefetchInterval(COST_REFRESH_INTERVAL_MS, clusterInfo.data?.context),
|
|
604
715
|
})
|
|
605
716
|
}
|
|
606
717
|
|
|
@@ -625,18 +736,175 @@ export interface OpenCostTrendResponse {
|
|
|
625
736
|
}
|
|
626
737
|
|
|
627
738
|
export function useOpenCostTrend(range_: CostTimeRange = '24h') {
|
|
739
|
+
const clusterInfo = useClusterInfo()
|
|
628
740
|
return useQuery<OpenCostTrendResponse>({
|
|
629
741
|
queryKey: ['opencost-trend', range_],
|
|
630
742
|
queryFn: () => fetchJSON(`/opencost/trend?range=${range_}`),
|
|
631
743
|
staleTime: 60000,
|
|
632
|
-
refetchInterval:
|
|
744
|
+
refetchInterval: costRefetchInterval(COST_TREND_REFRESH_INTERVAL_MS, clusterInfo.data?.context),
|
|
633
745
|
placeholderData: (prev) => prev,
|
|
634
746
|
})
|
|
635
747
|
}
|
|
636
748
|
|
|
749
|
+
export interface OpenCostWorkloadTrendResponse {
|
|
750
|
+
available: boolean
|
|
751
|
+
reason?: CostUnavailableReason
|
|
752
|
+
namespace: string
|
|
753
|
+
kind: string
|
|
754
|
+
name: string
|
|
755
|
+
range: string
|
|
756
|
+
windowTotalCost?: number
|
|
757
|
+
dataPoints?: OpenCostTrendDataPoint[]
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
export function useOpenCostWorkloadTrend(
|
|
761
|
+
kind: string,
|
|
762
|
+
namespace: string,
|
|
763
|
+
name: string,
|
|
764
|
+
range_: CostTimeRange = '24h',
|
|
765
|
+
options?: { enabled?: boolean },
|
|
766
|
+
) {
|
|
767
|
+
const clusterInfo = useClusterInfo()
|
|
768
|
+
return useQuery<OpenCostWorkloadTrendResponse>({
|
|
769
|
+
queryKey: ['opencost-workload-trend', kind, namespace, name, range_],
|
|
770
|
+
queryFn: () =>
|
|
771
|
+
fetchJSON(
|
|
772
|
+
`/opencost/workload/${encodeURIComponent(kind)}/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/trend?range=${range_}`,
|
|
773
|
+
),
|
|
774
|
+
enabled: (options?.enabled ?? true) && Boolean(kind && namespace && name),
|
|
775
|
+
staleTime: 60000,
|
|
776
|
+
refetchInterval: costRefetchInterval(COST_TREND_REFRESH_INTERVAL_MS, clusterInfo.data?.context),
|
|
777
|
+
})
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
export interface OpenCostApplicationWorkloadRef {
|
|
781
|
+
kind: string
|
|
782
|
+
namespace: string
|
|
783
|
+
name: string
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
export interface OpenCostApplicationWorkloadStatus extends OpenCostApplicationWorkloadRef {
|
|
787
|
+
reason: CostUnavailableReason
|
|
788
|
+
scaledToZero?: boolean
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
export interface OpenCostApplicationCostCoverage {
|
|
792
|
+
total: number
|
|
793
|
+
included: number
|
|
794
|
+
unavailable?: OpenCostApplicationWorkloadStatus[]
|
|
795
|
+
unsupported?: OpenCostApplicationWorkloadRef[]
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
export interface OpenCostApplicationCostTotals {
|
|
799
|
+
hourlyCost: number
|
|
800
|
+
cpuCost: number
|
|
801
|
+
memoryCost: number
|
|
802
|
+
replicas: number
|
|
803
|
+
cpuUsageCost?: number
|
|
804
|
+
memoryUsageCost?: number
|
|
805
|
+
cpuUsageAvailable: boolean
|
|
806
|
+
memoryUsageAvailable: boolean
|
|
807
|
+
cpuAllocationUse: number
|
|
808
|
+
memoryAllocationUse: number
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
export interface OpenCostApplicationWorkloadCost extends OpenCostApplicationWorkloadRef {
|
|
812
|
+
available: boolean
|
|
813
|
+
reason?: CostUnavailableReason
|
|
814
|
+
scaledToZero?: boolean
|
|
815
|
+
current?: OpenCostWorkloadCost
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
export interface OpenCostApplicationCostResponse {
|
|
819
|
+
available: boolean
|
|
820
|
+
reason?: CostUnavailableReason
|
|
821
|
+
partial?: boolean
|
|
822
|
+
totals: OpenCostApplicationCostTotals
|
|
823
|
+
coverage: OpenCostApplicationCostCoverage
|
|
824
|
+
workloads?: OpenCostApplicationWorkloadCost[]
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
export interface OpenCostApplicationCostTrendSeries extends OpenCostApplicationWorkloadRef {
|
|
828
|
+
windowTotalCost?: number
|
|
829
|
+
dataPoints?: OpenCostTrendDataPoint[]
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
export interface OpenCostApplicationCostTrendResponse {
|
|
833
|
+
available: boolean
|
|
834
|
+
reason?: CostUnavailableReason
|
|
835
|
+
range: string
|
|
836
|
+
partial?: boolean
|
|
837
|
+
windowTotalCost?: number
|
|
838
|
+
dataPoints?: OpenCostTrendDataPoint[]
|
|
839
|
+
series?: OpenCostApplicationCostTrendSeries[]
|
|
840
|
+
coverage: OpenCostApplicationCostCoverage
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function stableOpenCostWorkloadRefs(
|
|
844
|
+
workloads: OpenCostApplicationWorkloadRef[],
|
|
845
|
+
): OpenCostApplicationWorkloadRef[] {
|
|
846
|
+
const byKey = new Map<string, OpenCostApplicationWorkloadRef>()
|
|
847
|
+
for (const workload of workloads) {
|
|
848
|
+
if (!workload.kind || !workload.namespace || !workload.name) continue
|
|
849
|
+
const ref = {
|
|
850
|
+
kind: workload.kind,
|
|
851
|
+
namespace: workload.namespace,
|
|
852
|
+
name: workload.name,
|
|
853
|
+
}
|
|
854
|
+
byKey.set(`${ref.namespace}/${ref.kind}/${ref.name}`, ref)
|
|
855
|
+
}
|
|
856
|
+
return [...byKey.values()].sort((a, b) =>
|
|
857
|
+
`${a.namespace}/${a.kind}/${a.name}`.localeCompare(`${b.namespace}/${b.kind}/${b.name}`),
|
|
858
|
+
)
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
export function useOpenCostApplicationCost(
|
|
862
|
+
workloads: OpenCostApplicationWorkloadRef[],
|
|
863
|
+
options?: { enabled?: boolean },
|
|
864
|
+
) {
|
|
865
|
+
const clusterInfo = useClusterInfo()
|
|
866
|
+
const refs = stableOpenCostWorkloadRefs(workloads)
|
|
867
|
+
return useQuery<OpenCostApplicationCostResponse>({
|
|
868
|
+
queryKey: ['opencost-application', refs],
|
|
869
|
+
queryFn: ({ signal }) =>
|
|
870
|
+
fetchJSON('/opencost/application', {
|
|
871
|
+
method: 'POST',
|
|
872
|
+
headers: { 'Content-Type': 'application/json' },
|
|
873
|
+
body: JSON.stringify({ workloads: refs }),
|
|
874
|
+
signal,
|
|
875
|
+
}),
|
|
876
|
+
enabled: (options?.enabled ?? true) && refs.length > 0,
|
|
877
|
+
staleTime: 30000,
|
|
878
|
+
refetchInterval: costRefetchInterval(COST_REFRESH_INTERVAL_MS, clusterInfo.data?.context),
|
|
879
|
+
})
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
export function useOpenCostApplicationCostTrend(
|
|
883
|
+
workloads: OpenCostApplicationWorkloadRef[],
|
|
884
|
+
range_: CostTimeRange = '24h',
|
|
885
|
+
options?: { enabled?: boolean },
|
|
886
|
+
) {
|
|
887
|
+
const clusterInfo = useClusterInfo()
|
|
888
|
+
const refs = stableOpenCostWorkloadRefs(workloads)
|
|
889
|
+
return useQuery<OpenCostApplicationCostTrendResponse>({
|
|
890
|
+
queryKey: ['opencost-application-trend', refs, range_],
|
|
891
|
+
queryFn: ({ signal }) =>
|
|
892
|
+
fetchJSON('/opencost/application/trend', {
|
|
893
|
+
method: 'POST',
|
|
894
|
+
headers: { 'Content-Type': 'application/json' },
|
|
895
|
+
body: JSON.stringify({ workloads: refs, range: range_ }),
|
|
896
|
+
signal,
|
|
897
|
+
}),
|
|
898
|
+
enabled: (options?.enabled ?? true) && refs.length > 0,
|
|
899
|
+
staleTime: 60000,
|
|
900
|
+
refetchInterval: costRefetchInterval(COST_TREND_REFRESH_INTERVAL_MS, clusterInfo.data?.context),
|
|
901
|
+
})
|
|
902
|
+
}
|
|
903
|
+
|
|
637
904
|
// Node cost breakdown
|
|
638
905
|
export interface OpenCostNodeCost {
|
|
639
906
|
name: string
|
|
907
|
+
providerID?: string
|
|
640
908
|
instanceType?: string
|
|
641
909
|
region?: string
|
|
642
910
|
hourlyCost: number
|
|
@@ -651,11 +919,12 @@ export interface OpenCostNodeResponse {
|
|
|
651
919
|
}
|
|
652
920
|
|
|
653
921
|
export function useOpenCostNodes() {
|
|
922
|
+
const clusterInfo = useClusterInfo()
|
|
654
923
|
return useQuery<OpenCostNodeResponse>({
|
|
655
924
|
queryKey: ['opencost-nodes'],
|
|
656
925
|
queryFn: () => fetchJSON('/opencost/nodes'),
|
|
657
926
|
staleTime: 60000,
|
|
658
|
-
refetchInterval:
|
|
927
|
+
refetchInterval: costRefetchInterval(COST_TREND_REFRESH_INTERVAL_MS, clusterInfo.data?.context),
|
|
659
928
|
placeholderData: (prev) => prev,
|
|
660
929
|
})
|
|
661
930
|
}
|
|
@@ -816,7 +1085,15 @@ const SEARCH_MIN_QUERY = 2
|
|
|
816
1085
|
// health/issueCount per hit (rich rows). React Query's AbortSignal cancels
|
|
817
1086
|
// overlapping scans on a new query. keepPreviousData avoids flicker while the
|
|
818
1087
|
// next query resolves.
|
|
819
|
-
export function useSearch(
|
|
1088
|
+
export function useSearch(
|
|
1089
|
+
query: string,
|
|
1090
|
+
opts?: {
|
|
1091
|
+
limit?: number
|
|
1092
|
+
context?: 'summary' | 'none'
|
|
1093
|
+
enabled?: boolean
|
|
1094
|
+
globalNs?: boolean
|
|
1095
|
+
},
|
|
1096
|
+
) {
|
|
820
1097
|
const trimmed = query.trim()
|
|
821
1098
|
const enabled = (opts?.enabled ?? true) && trimmed.length >= SEARCH_MIN_QUERY
|
|
822
1099
|
const limit = opts?.limit ?? 20
|
|
@@ -828,7 +1105,10 @@ export function useSearch(query: string, opts?: { limit?: number; context?: 'sum
|
|
|
828
1105
|
return useQuery<SearchResult>({
|
|
829
1106
|
queryKey: ['search', trimmed, limit, context, globalNs],
|
|
830
1107
|
queryFn: ({ signal }) =>
|
|
831
|
-
fetchJSON<SearchResult>(
|
|
1108
|
+
fetchJSON<SearchResult>(
|
|
1109
|
+
`/search?q=${encodeURIComponent(trimmed)}&limit=${limit}&include=none&context=${context}${globalNs ? '&globalNs=1' : ''}`,
|
|
1110
|
+
signal,
|
|
1111
|
+
),
|
|
832
1112
|
enabled,
|
|
833
1113
|
staleTime: 2000,
|
|
834
1114
|
placeholderData: (prev) => prev, // keepPreviousData
|
|
@@ -863,7 +1143,10 @@ export function useCapabilities() {
|
|
|
863
1143
|
|
|
864
1144
|
// Namespace-scoped capabilities. Users with namespace-scoped RoleBindings may
|
|
865
1145
|
// have these permissions in specific namespaces.
|
|
866
|
-
export function useNamespaceCapabilities(
|
|
1146
|
+
export function useNamespaceCapabilities(
|
|
1147
|
+
namespace: string | undefined,
|
|
1148
|
+
globalCaps: Capabilities | undefined,
|
|
1149
|
+
) {
|
|
867
1150
|
const needsCheck = namespace && globalCaps
|
|
868
1151
|
return useQuery<Capabilities>({
|
|
869
1152
|
queryKey: ['capabilities', namespace],
|
|
@@ -902,7 +1185,11 @@ export function useAuthMe() {
|
|
|
902
1185
|
// CloudRole.AtLeast — the frontend must agree with the backend on what
|
|
903
1186
|
// "member-or-higher" means; otherwise we'd hide a button the
|
|
904
1187
|
// backend would happily honor (or vice versa).
|
|
905
|
-
const CLOUD_ROLE_RANK: Record<string, number> = {
|
|
1188
|
+
const CLOUD_ROLE_RANK: Record<string, number> = {
|
|
1189
|
+
viewer: 1,
|
|
1190
|
+
member: 2,
|
|
1191
|
+
owner: 3,
|
|
1192
|
+
}
|
|
906
1193
|
|
|
907
1194
|
/**
|
|
908
1195
|
* useCloudRole returns the caller's Cloud tier (`owner` / `member` /
|
|
@@ -983,7 +1270,15 @@ export function useNamespaces() {
|
|
|
983
1270
|
}
|
|
984
1271
|
|
|
985
1272
|
// Topology (for manual refresh)
|
|
986
|
-
export function useTopology(
|
|
1273
|
+
export function useTopology(
|
|
1274
|
+
namespaces: string[],
|
|
1275
|
+
viewMode: string = 'resources',
|
|
1276
|
+
options?: {
|
|
1277
|
+
enabled?: boolean
|
|
1278
|
+
includeReplicaSets?: boolean
|
|
1279
|
+
refetchInterval?: number | false
|
|
1280
|
+
},
|
|
1281
|
+
) {
|
|
987
1282
|
const params = new URLSearchParams()
|
|
988
1283
|
if (namespaces.length > 0) params.set('namespaces', namespaces.join(','))
|
|
989
1284
|
if (viewMode) params.set('view', viewMode)
|
|
@@ -1016,7 +1311,11 @@ export function useApplications(namespaces: string[], options?: { enabled?: bool
|
|
|
1016
1311
|
})
|
|
1017
1312
|
}
|
|
1018
1313
|
|
|
1019
|
-
export function useApplicationHistory(
|
|
1314
|
+
export function useApplicationHistory(
|
|
1315
|
+
appKey: string | undefined,
|
|
1316
|
+
namespaces: string[],
|
|
1317
|
+
options?: { enabled?: boolean },
|
|
1318
|
+
) {
|
|
1020
1319
|
const params = new URLSearchParams()
|
|
1021
1320
|
if (appKey) params.set('app', appKey)
|
|
1022
1321
|
if (namespaces.length > 0) params.set('namespaces', namespaces.join(','))
|
|
@@ -1031,7 +1330,14 @@ export function useApplicationHistory(appKey: string | undefined, namespaces: st
|
|
|
1031
1330
|
})
|
|
1032
1331
|
}
|
|
1033
1332
|
|
|
1034
|
-
export function useGitOpsTree(
|
|
1333
|
+
export function useGitOpsTree(
|
|
1334
|
+
kind: string,
|
|
1335
|
+
namespace: string,
|
|
1336
|
+
name: string,
|
|
1337
|
+
group?: string,
|
|
1338
|
+
namespaces: string[] = [],
|
|
1339
|
+
options?: { enabled?: boolean },
|
|
1340
|
+
) {
|
|
1035
1341
|
const ns = namespace || '_'
|
|
1036
1342
|
const params = new URLSearchParams()
|
|
1037
1343
|
if (group) params.set('group', group)
|
|
@@ -1040,7 +1346,8 @@ export function useGitOpsTree(kind: string, namespace: string, name: string, gro
|
|
|
1040
1346
|
|
|
1041
1347
|
return useQuery<GitOpsResourceTree>({
|
|
1042
1348
|
queryKey: ['gitops-tree', kind, namespace, name, group, namespaces],
|
|
1043
|
-
queryFn: () =>
|
|
1349
|
+
queryFn: () =>
|
|
1350
|
+
fetchJSON(`/gitops/tree/${kind}/${ns}/${name}${queryString ? `?${queryString}` : ''}`),
|
|
1044
1351
|
enabled: Boolean(kind && name) && (options?.enabled ?? true),
|
|
1045
1352
|
staleTime: 5000,
|
|
1046
1353
|
})
|
|
@@ -1048,11 +1355,17 @@ export function useGitOpsTree(kind: string, namespace: string, name: string, gro
|
|
|
1048
1355
|
|
|
1049
1356
|
// Poll fast (2s) while a sync/rollback is in flight so the user sees the
|
|
1050
1357
|
// outcome quickly; otherwise rely on staleTime + manual refetch. Argo flips
|
|
1051
|
-
// operationState.phase from
|
|
1358
|
+
// operationState.phase from Running/Terminating to a terminal phase, so this
|
|
1052
1359
|
// auto-quiesces on completion.
|
|
1053
1360
|
const INSIGHTS_RUNNING_POLL_MS = 2000
|
|
1054
1361
|
|
|
1055
|
-
export function useGitOpsInsights(
|
|
1362
|
+
export function useGitOpsInsights(
|
|
1363
|
+
kind: string,
|
|
1364
|
+
namespace: string,
|
|
1365
|
+
name: string,
|
|
1366
|
+
group?: string,
|
|
1367
|
+
namespaces: string[] = [],
|
|
1368
|
+
) {
|
|
1056
1369
|
const ns = namespace || '_'
|
|
1057
1370
|
const params = new URLSearchParams()
|
|
1058
1371
|
if (group) params.set('group', group)
|
|
@@ -1061,19 +1374,71 @@ export function useGitOpsInsights(kind: string, namespace: string, name: string,
|
|
|
1061
1374
|
|
|
1062
1375
|
return useQuery<GitOpsInsight>({
|
|
1063
1376
|
queryKey: ['gitops-insights', kind, namespace, name, group, namespaces],
|
|
1064
|
-
queryFn: () =>
|
|
1377
|
+
queryFn: () =>
|
|
1378
|
+
fetchJSON(`/gitops/insights/${kind}/${ns}/${name}${queryString ? `?${queryString}` : ''}`),
|
|
1065
1379
|
enabled: Boolean(kind && name),
|
|
1066
1380
|
staleTime: 5000,
|
|
1067
1381
|
refetchInterval: (query) => {
|
|
1068
1382
|
const phase = query.state.data?.summary?.operationPhase
|
|
1069
|
-
return phase === 'Running' ? INSIGHTS_RUNNING_POLL_MS : false
|
|
1383
|
+
return phase === 'Running' || phase === 'Terminating' ? INSIGHTS_RUNNING_POLL_MS : false
|
|
1070
1384
|
},
|
|
1071
1385
|
})
|
|
1072
1386
|
}
|
|
1073
1387
|
|
|
1388
|
+
// Full Git-rendered desired-vs-live diff for one Argo CD managed resource.
|
|
1389
|
+
// ns/name identify the Application; the ref identifies the managed resource.
|
|
1390
|
+
// Fetched on demand — the caller mounts this only when the user opens "Full
|
|
1391
|
+
// diff", so it's enabled whenever the ref is resolvable. Errors surface via
|
|
1392
|
+
// fetchJSON's ApiError (server {"error"} string as .message).
|
|
1393
|
+
export function useArgoResourceDiff(appNamespace: string, appName: string, ref: GitOpsInsightRef) {
|
|
1394
|
+
const ns = appNamespace || '_'
|
|
1395
|
+
const params = new URLSearchParams()
|
|
1396
|
+
if (ref.group) params.set('group', ref.group)
|
|
1397
|
+
params.set('kind', ref.kind)
|
|
1398
|
+
if (ref.namespace) params.set('resourceNamespace', ref.namespace)
|
|
1399
|
+
params.set('resourceName', ref.name)
|
|
1400
|
+
|
|
1401
|
+
return useQuery<GitOpsResourceDiff>({
|
|
1402
|
+
queryKey: ['argo-resource-diff', appNamespace, appName, ref.group, ref.kind, ref.namespace, ref.name],
|
|
1403
|
+
queryFn: () => fetchJSON(`/argo/applications/${ns}/${appName}/resource-diff?${params.toString()}`),
|
|
1404
|
+
enabled: Boolean(appName && ref.kind && ref.name),
|
|
1405
|
+
staleTime: 15_000,
|
|
1406
|
+
})
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// Git commit metadata for one deployed revision of an Argo CD Application.
|
|
1410
|
+
// Enabled only when a revision is known and the caller passes `enabled` (gated
|
|
1411
|
+
// on capabilities.revisionMetadataAvailable). Cached long — a resolved SHA's
|
|
1412
|
+
// metadata is effectively immutable.
|
|
1413
|
+
export function useArgoRevisionMetadata(
|
|
1414
|
+
appNamespace: string,
|
|
1415
|
+
appName: string,
|
|
1416
|
+
revision: string | undefined,
|
|
1417
|
+
opts?: { sourceIndex?: number; project?: string; enabled?: boolean },
|
|
1418
|
+
) {
|
|
1419
|
+
const ns = appNamespace || '_'
|
|
1420
|
+
const params = new URLSearchParams()
|
|
1421
|
+
if (revision) params.set('revision', revision)
|
|
1422
|
+
if (opts?.sourceIndex != null) params.set('sourceIndex', String(opts.sourceIndex))
|
|
1423
|
+
if (opts?.project) params.set('project', opts.project)
|
|
1424
|
+
|
|
1425
|
+
return useQuery<ArgoRevisionMetadata>({
|
|
1426
|
+
queryKey: ['argo-revision-metadata', appNamespace, appName, revision, opts?.sourceIndex, opts?.project],
|
|
1427
|
+
queryFn: () => fetchJSON(`/argo/applications/${ns}/${appName}/revision-metadata?${params.toString()}`),
|
|
1428
|
+
enabled: Boolean(appName && revision) && (opts?.enabled ?? true),
|
|
1429
|
+
staleTime: 5 * 60_000,
|
|
1430
|
+
})
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1074
1433
|
// Generic resource fetching - returns resource with relationships
|
|
1075
1434
|
// Uses '_' as placeholder for cluster-scoped resources (empty namespace)
|
|
1076
|
-
export function useResource<T>(
|
|
1435
|
+
export function useResource<T>(
|
|
1436
|
+
kind: string,
|
|
1437
|
+
namespace: string,
|
|
1438
|
+
name: string,
|
|
1439
|
+
group?: string,
|
|
1440
|
+
options?: { enabled?: boolean; refetchInterval?: number | false },
|
|
1441
|
+
) {
|
|
1077
1442
|
// For cluster-scoped resources, use '_' as namespace placeholder
|
|
1078
1443
|
const ns = namespace || '_'
|
|
1079
1444
|
const params = new URLSearchParams()
|
|
@@ -1082,8 +1447,9 @@ export function useResource<T>(kind: string, namespace: string, name: string, gr
|
|
|
1082
1447
|
|
|
1083
1448
|
const query = useQuery<ResourceWithRelationships<T>>({
|
|
1084
1449
|
queryKey: ['resource', kind, namespace, name, group],
|
|
1085
|
-
queryFn: () =>
|
|
1086
|
-
|
|
1450
|
+
queryFn: () =>
|
|
1451
|
+
fetchJSON(`/resources/${kind}/${ns}/${name}${queryString ? `?${queryString}` : ''}`),
|
|
1452
|
+
enabled: (options?.enabled ?? true) && Boolean(kind && name), // namespace can be empty for cluster-scoped resources
|
|
1087
1453
|
refetchInterval: options?.refetchInterval,
|
|
1088
1454
|
})
|
|
1089
1455
|
|
|
@@ -1098,7 +1464,12 @@ export function useResource<T>(kind: string, namespace: string, name: string, gr
|
|
|
1098
1464
|
}
|
|
1099
1465
|
|
|
1100
1466
|
// Hook that returns full response with relationships explicitly
|
|
1101
|
-
export function useResourceWithRelationships<T>(
|
|
1467
|
+
export function useResourceWithRelationships<T>(
|
|
1468
|
+
kind: string,
|
|
1469
|
+
namespace: string,
|
|
1470
|
+
name: string,
|
|
1471
|
+
group?: string,
|
|
1472
|
+
) {
|
|
1102
1473
|
const ns = namespace || '_'
|
|
1103
1474
|
const params = new URLSearchParams()
|
|
1104
1475
|
if (group) params.set('group', group)
|
|
@@ -1106,7 +1477,8 @@ export function useResourceWithRelationships<T>(kind: string, namespace: string,
|
|
|
1106
1477
|
|
|
1107
1478
|
return useQuery<ResourceWithRelationships<T>>({
|
|
1108
1479
|
queryKey: ['resource', kind, namespace, name, group],
|
|
1109
|
-
queryFn: () =>
|
|
1480
|
+
queryFn: () =>
|
|
1481
|
+
fetchJSON(`/resources/${kind}/${ns}/${name}${queryString ? `?${queryString}` : ''}`),
|
|
1110
1482
|
enabled: Boolean(kind && name),
|
|
1111
1483
|
})
|
|
1112
1484
|
}
|
|
@@ -1199,7 +1571,11 @@ async function fetchChangesPage(
|
|
|
1199
1571
|
// (Rows dropped by content filters inside the store query do not; see the
|
|
1200
1572
|
// known limitation on the server's handleChanges.)
|
|
1201
1573
|
const maxSeq = Number(response.headers.get('X-Radar-Timeline-Max-Seq') ?? '0') || 0
|
|
1202
|
-
return {
|
|
1574
|
+
return {
|
|
1575
|
+
events,
|
|
1576
|
+
epoch: response.headers.get('X-Radar-Timeline-Epoch') ?? '',
|
|
1577
|
+
maxSeq,
|
|
1578
|
+
}
|
|
1203
1579
|
}
|
|
1204
1580
|
|
|
1205
1581
|
// Highest store-assigned arrival number in the cached page — the delta cursor.
|
|
@@ -1250,7 +1626,10 @@ export async function runDeltaSyncFetch(deps: {
|
|
|
1250
1626
|
const meta = metaStore.get(metaKey)
|
|
1251
1627
|
const cursor = deltaFetchCursor(meta, cached, now)
|
|
1252
1628
|
if (cursor > 0) {
|
|
1253
|
-
const delta = await fetchChangesPage(
|
|
1629
|
+
const delta = await fetchChangesPage(
|
|
1630
|
+
`${path}${queryString ? '&' : '?'}since_seq=${cursor}`,
|
|
1631
|
+
signal,
|
|
1632
|
+
)
|
|
1254
1633
|
if (delta.epoch && delta.epoch === meta!.epoch) {
|
|
1255
1634
|
meta!.highWaterSeq = Math.max(meta!.highWaterSeq, delta.maxSeq, maxEventSeq(delta.events))
|
|
1256
1635
|
// Returning the cached reference on an empty delta skips re-renders.
|
|
@@ -1260,7 +1639,11 @@ export async function runDeltaSyncFetch(deps: {
|
|
|
1260
1639
|
// cursor is meaningless. Fall through to a full resync.
|
|
1261
1640
|
}
|
|
1262
1641
|
const full = await fetchChangesPage(path, signal)
|
|
1263
|
-
metaStore.set(metaKey, {
|
|
1642
|
+
metaStore.set(metaKey, {
|
|
1643
|
+
epoch: full.epoch,
|
|
1644
|
+
lastFullMs: now,
|
|
1645
|
+
highWaterSeq: Math.max(full.maxSeq, maxEventSeq(full.events)),
|
|
1646
|
+
})
|
|
1264
1647
|
return full.events
|
|
1265
1648
|
}
|
|
1266
1649
|
|
|
@@ -1288,7 +1671,18 @@ function getTimeRangeDate(range: TimeRange): Date | null {
|
|
|
1288
1671
|
}
|
|
1289
1672
|
|
|
1290
1673
|
export function useChanges(options: UseChangesOptions = {}) {
|
|
1291
|
-
const {
|
|
1674
|
+
const {
|
|
1675
|
+
namespaces = [],
|
|
1676
|
+
kinds,
|
|
1677
|
+
timeRange = '1h',
|
|
1678
|
+
filter = 'all',
|
|
1679
|
+
includeK8sEvents = true,
|
|
1680
|
+
includeManaged = false,
|
|
1681
|
+
includeDeleted = true,
|
|
1682
|
+
limit = 200,
|
|
1683
|
+
enabled = true,
|
|
1684
|
+
deltaSync = false,
|
|
1685
|
+
} = options
|
|
1292
1686
|
const queryClient = useQueryClient()
|
|
1293
1687
|
|
|
1294
1688
|
// Only a single-kind selection narrows the server query; a multi-kind
|
|
@@ -1311,7 +1705,17 @@ export function useChanges(options: UseChangesOptions = {}) {
|
|
|
1311
1705
|
|
|
1312
1706
|
const queryString = params.toString()
|
|
1313
1707
|
const path = `/changes${queryString ? `?${queryString}` : ''}`
|
|
1314
|
-
const queryKey = [
|
|
1708
|
+
const queryKey = [
|
|
1709
|
+
'changes',
|
|
1710
|
+
namespaces,
|
|
1711
|
+
serverKind,
|
|
1712
|
+
timeRange,
|
|
1713
|
+
filter,
|
|
1714
|
+
includeK8sEvents,
|
|
1715
|
+
includeManaged,
|
|
1716
|
+
includeDeleted,
|
|
1717
|
+
limit,
|
|
1718
|
+
]
|
|
1315
1719
|
|
|
1316
1720
|
return useQuery<TimelineEvent[]>({
|
|
1317
1721
|
queryKey,
|
|
@@ -1320,7 +1724,16 @@ export function useChanges(options: UseChangesOptions = {}) {
|
|
|
1320
1724
|
|
|
1321
1725
|
const metaKey = JSON.stringify(queryKey)
|
|
1322
1726
|
const cached = queryClient.getQueryData<TimelineEvent[]>(queryKey)
|
|
1323
|
-
return runDeltaSyncFetch({
|
|
1727
|
+
return runDeltaSyncFetch({
|
|
1728
|
+
path,
|
|
1729
|
+
queryString,
|
|
1730
|
+
limit,
|
|
1731
|
+
metaKey,
|
|
1732
|
+
cached,
|
|
1733
|
+
metaStore: changesDeltaMeta,
|
|
1734
|
+
now: Date.now(),
|
|
1735
|
+
signal,
|
|
1736
|
+
})
|
|
1324
1737
|
},
|
|
1325
1738
|
staleTime: 5000, // Consider data stale after 5 seconds to ensure fresh data on navigation
|
|
1326
1739
|
refetchInterval: CHANGES_REFRESH_INTERVAL_MS, // SSE-driven invalidation handles real-time updates; this is the no-SSE fallback
|
|
@@ -1329,7 +1742,12 @@ export function useChanges(options: UseChangesOptions = {}) {
|
|
|
1329
1742
|
}
|
|
1330
1743
|
|
|
1331
1744
|
// Children changes for a parent workload (e.g., ReplicaSets and Pods under a Deployment)
|
|
1332
|
-
export function useResourceChildren(
|
|
1745
|
+
export function useResourceChildren(
|
|
1746
|
+
kind: string,
|
|
1747
|
+
namespace: string,
|
|
1748
|
+
name: string,
|
|
1749
|
+
timeRange: TimeRange = '1h',
|
|
1750
|
+
) {
|
|
1333
1751
|
const sinceDate = getTimeRangeDate(timeRange)
|
|
1334
1752
|
const params = new URLSearchParams()
|
|
1335
1753
|
if (sinceDate) {
|
|
@@ -1358,7 +1776,11 @@ export interface ResourceEventsResult {
|
|
|
1358
1776
|
// K8s events and resource updates are fetched separately so a high-frequency
|
|
1359
1777
|
// informer update stream (e.g. a CrashLoop status field flapping every few
|
|
1360
1778
|
// seconds) can never starve out user-meaningful K8s events under a shared limit.
|
|
1361
|
-
export function useResourceEvents(
|
|
1779
|
+
export function useResourceEvents(
|
|
1780
|
+
kind: string,
|
|
1781
|
+
namespace: string,
|
|
1782
|
+
name: string,
|
|
1783
|
+
): ResourceEventsResult {
|
|
1362
1784
|
// The timeline store keys events by their K8s Kind (singular PascalCase, e.g. "Pod"),
|
|
1363
1785
|
// but callers pass the URL-form kind ("pods").
|
|
1364
1786
|
const singularKind = pluralToKind(kind)
|
|
@@ -1424,8 +1846,8 @@ export function useResourceEvents(kind: string, namespace: string, name: string)
|
|
|
1424
1846
|
export interface ContainerMetrics {
|
|
1425
1847
|
name: string
|
|
1426
1848
|
usage: {
|
|
1427
|
-
cpu: string
|
|
1428
|
-
memory: string
|
|
1849
|
+
cpu: string // e.g., "10m" (millicores)
|
|
1850
|
+
memory: string // e.g., "128Mi"
|
|
1429
1851
|
}
|
|
1430
1852
|
}
|
|
1431
1853
|
|
|
@@ -1500,8 +1922,8 @@ export function useNodeMetrics(nodeName: string, options?: { enabled?: boolean }
|
|
|
1500
1922
|
|
|
1501
1923
|
export interface MetricsDataPoint {
|
|
1502
1924
|
timestamp: string
|
|
1503
|
-
cpu: number
|
|
1504
|
-
memory: number
|
|
1925
|
+
cpu: number // CPU in nanocores
|
|
1926
|
+
memory: number // Memory in bytes
|
|
1505
1927
|
}
|
|
1506
1928
|
|
|
1507
1929
|
export interface ContainerMetricsHistory {
|
|
@@ -1530,7 +1952,9 @@ export interface NodeMetricsHistory {
|
|
|
1530
1952
|
metricsUnavailableReason?: string
|
|
1531
1953
|
}
|
|
1532
1954
|
|
|
1533
|
-
function withoutCollectionError<
|
|
1955
|
+
function withoutCollectionError<
|
|
1956
|
+
T extends { collectionError?: string; rawCollectionError?: string },
|
|
1957
|
+
>(history: T): T {
|
|
1534
1958
|
const next = { ...history }
|
|
1535
1959
|
delete next.collectionError
|
|
1536
1960
|
delete next.rawCollectionError
|
|
@@ -1539,15 +1963,26 @@ function withoutCollectionError<T extends { collectionError?: string; rawCollect
|
|
|
1539
1963
|
|
|
1540
1964
|
export function normalizePodMetricsHistory(history: PodMetricsHistory): PodMetricsHistory {
|
|
1541
1965
|
if (history.metricsUnavailable !== true) return history
|
|
1542
|
-
return {
|
|
1966
|
+
return {
|
|
1967
|
+
...withoutCollectionError(history),
|
|
1968
|
+
metricsUnavailable: true,
|
|
1969
|
+
metricsUnavailableReason: history.rawCollectionError || history.collectionError,
|
|
1970
|
+
}
|
|
1543
1971
|
}
|
|
1544
1972
|
|
|
1545
1973
|
export function normalizeNodeMetricsHistory(history: NodeMetricsHistory): NodeMetricsHistory {
|
|
1546
1974
|
if (history.metricsUnavailable !== true) return history
|
|
1547
|
-
return {
|
|
1975
|
+
return {
|
|
1976
|
+
...withoutCollectionError(history),
|
|
1977
|
+
metricsUnavailable: true,
|
|
1978
|
+
metricsUnavailableReason: history.rawCollectionError || history.collectionError,
|
|
1979
|
+
}
|
|
1548
1980
|
}
|
|
1549
1981
|
|
|
1550
|
-
export function shouldFetchLiveMetrics(
|
|
1982
|
+
export function shouldFetchLiveMetrics(
|
|
1983
|
+
historySettled: boolean,
|
|
1984
|
+
metricsUnavailable: boolean,
|
|
1985
|
+
): boolean {
|
|
1551
1986
|
return historySettled && !metricsUnavailable
|
|
1552
1987
|
}
|
|
1553
1988
|
|
|
@@ -1555,7 +1990,11 @@ export function isLiveMetricsUnavailable(liveMetricsEnabled: boolean, metrics: u
|
|
|
1555
1990
|
return liveMetricsEnabled && metrics === null
|
|
1556
1991
|
}
|
|
1557
1992
|
|
|
1558
|
-
export function getVisibleLiveMetrics<T>(
|
|
1993
|
+
export function getVisibleLiveMetrics<T>(
|
|
1994
|
+
liveMetricsEnabled: boolean,
|
|
1995
|
+
metricsUnavailable: boolean,
|
|
1996
|
+
metrics: T | null | undefined,
|
|
1997
|
+
): T | undefined {
|
|
1559
1998
|
if (!liveMetricsEnabled || metricsUnavailable) return undefined
|
|
1560
1999
|
return metrics ?? undefined
|
|
1561
2000
|
}
|
|
@@ -1564,7 +2003,10 @@ export function getVisibleLiveMetrics<T>(liveMetricsEnabled: boolean, metricsUna
|
|
|
1564
2003
|
export function usePodMetricsHistory(namespace: string, podName: string) {
|
|
1565
2004
|
return useQuery<PodMetricsHistory>({
|
|
1566
2005
|
queryKey: ['pod-metrics-history', namespace, podName],
|
|
1567
|
-
queryFn: async () =>
|
|
2006
|
+
queryFn: async () =>
|
|
2007
|
+
normalizePodMetricsHistory(
|
|
2008
|
+
await fetchJSON<PodMetricsHistory>(`/metrics/pods/${namespace}/${podName}/history`),
|
|
2009
|
+
),
|
|
1568
2010
|
enabled: Boolean(namespace && podName),
|
|
1569
2011
|
staleTime: 25000, // Slightly less than poll interval
|
|
1570
2012
|
refetchInterval: 30000, // Match the backend poll interval
|
|
@@ -1575,7 +2017,10 @@ export function usePodMetricsHistory(namespace: string, podName: string) {
|
|
|
1575
2017
|
export function useNodeMetricsHistory(nodeName: string) {
|
|
1576
2018
|
return useQuery<NodeMetricsHistory>({
|
|
1577
2019
|
queryKey: ['node-metrics-history', nodeName],
|
|
1578
|
-
queryFn: async () =>
|
|
2020
|
+
queryFn: async () =>
|
|
2021
|
+
normalizeNodeMetricsHistory(
|
|
2022
|
+
await fetchJSON<NodeMetricsHistory>(`/metrics/nodes/${nodeName}/history`),
|
|
2023
|
+
),
|
|
1579
2024
|
enabled: Boolean(nodeName),
|
|
1580
2025
|
staleTime: 25000,
|
|
1581
2026
|
refetchInterval: 30000,
|
|
@@ -1586,20 +2031,20 @@ export function useNodeMetricsHistory(nodeName: string) {
|
|
|
1586
2031
|
export interface TopPodMetrics {
|
|
1587
2032
|
namespace: string
|
|
1588
2033
|
name: string
|
|
1589
|
-
cpu: number
|
|
1590
|
-
memory: number
|
|
1591
|
-
cpuRequest: number
|
|
1592
|
-
cpuLimit: number
|
|
2034
|
+
cpu: number // nanocores (usage)
|
|
2035
|
+
memory: number // bytes (usage)
|
|
2036
|
+
cpuRequest: number // nanocores (sum across containers)
|
|
2037
|
+
cpuLimit: number // nanocores (sum across containers)
|
|
1593
2038
|
memoryRequest: number // bytes (sum across containers)
|
|
1594
|
-
memoryLimit: number
|
|
2039
|
+
memoryLimit: number // bytes (sum across containers)
|
|
1595
2040
|
}
|
|
1596
2041
|
|
|
1597
2042
|
export interface TopNodeMetrics {
|
|
1598
2043
|
name: string
|
|
1599
|
-
cpu: number
|
|
1600
|
-
memory: number
|
|
1601
|
-
podCount: number
|
|
1602
|
-
cpuAllocatable: number
|
|
2044
|
+
cpu: number // nanocores (usage)
|
|
2045
|
+
memory: number // bytes (usage)
|
|
2046
|
+
podCount: number // pods scheduled on this node
|
|
2047
|
+
cpuAllocatable: number // nanocores
|
|
1603
2048
|
memoryAllocatable: number // bytes
|
|
1604
2049
|
}
|
|
1605
2050
|
|
|
@@ -1675,11 +2120,13 @@ export interface PrometheusResourceMetrics {
|
|
|
1675
2120
|
range: string
|
|
1676
2121
|
result: PrometheusQueryResult
|
|
1677
2122
|
query?: string // PromQL query (included when result is empty, for diagnostics)
|
|
1678
|
-
hint?: string
|
|
2123
|
+
hint?: string // Contextual hint when results are empty (e.g. cri-docker label issues)
|
|
1679
2124
|
}
|
|
1680
2125
|
|
|
1681
|
-
export type PrometheusMetricCategory =
|
|
1682
|
-
|
|
2126
|
+
export type PrometheusMetricCategory =
|
|
2127
|
+
'cpu' | 'memory' | 'network_rx' | 'network_tx' | 'filesystem' | 'restarts'
|
|
2128
|
+
export type PrometheusTimeRange =
|
|
2129
|
+
'10m' | '30m' | '1h' | '3h' | '6h' | '12h' | '24h' | '48h' | '7d' | '14d'
|
|
1683
2130
|
|
|
1684
2131
|
// PVC usage at a moment in time, derived from kubelet_volume_stats_*.
|
|
1685
2132
|
// HasData=false silently indicates the CSI driver doesn't report or Prom
|
|
@@ -1693,17 +2140,49 @@ export interface PrometheusPVCUsage {
|
|
|
1693
2140
|
hasData: boolean
|
|
1694
2141
|
}
|
|
1695
2142
|
|
|
1696
|
-
export type
|
|
2143
|
+
export type RightsizingFit =
|
|
2144
|
+
'balanced' | 'oversized' | 'under_requested' | 'missing_request' | 'insufficient_history'
|
|
2145
|
+
export type RightsizingConfidence = 'low' | 'medium' | 'high'
|
|
2146
|
+
export type RightsizingOwnerCoverage = 'ksm_history' | 'current_pods'
|
|
1697
2147
|
|
|
1698
2148
|
export interface RightsizingRow {
|
|
1699
2149
|
container: string
|
|
1700
2150
|
resource: 'cpu' | 'memory'
|
|
2151
|
+
fit: RightsizingFit
|
|
2152
|
+
confidence: RightsizingConfidence
|
|
1701
2153
|
currentRequest?: string
|
|
2154
|
+
currentRequestValue?: number
|
|
1702
2155
|
currentLimit?: string
|
|
1703
|
-
|
|
2156
|
+
currentLimitValue?: number
|
|
2157
|
+
observed?: {
|
|
2158
|
+
name: 'P95' | 'P99' | 'Max'
|
|
2159
|
+
value: number
|
|
2160
|
+
formatted: string
|
|
2161
|
+
}
|
|
2162
|
+
peak?: {
|
|
2163
|
+
name: 'P99'
|
|
2164
|
+
value: number
|
|
2165
|
+
formatted: string
|
|
2166
|
+
}
|
|
2167
|
+
calculatedRequest?: string
|
|
2168
|
+
calculatedRequestValue?: number
|
|
1704
2169
|
recommendedRequest?: string
|
|
1705
|
-
|
|
1706
|
-
|
|
2170
|
+
recommendedRequestValue?: number
|
|
2171
|
+
reductionLimited?: boolean
|
|
2172
|
+
bursty?: boolean
|
|
2173
|
+
recommendationReason?: string
|
|
2174
|
+
sampleCount: number
|
|
2175
|
+
expectedSamples: number
|
|
2176
|
+
coverage: number
|
|
2177
|
+
hpaManaged: boolean
|
|
2178
|
+
hpaEvidenceAvailable: boolean
|
|
2179
|
+
throttleAvailable?: boolean
|
|
2180
|
+
throttleRatio?: number
|
|
2181
|
+
currentPodOOM?: boolean
|
|
2182
|
+
windowOomEvidence?: boolean
|
|
2183
|
+
oomEvidenceAvailable: boolean
|
|
2184
|
+
limitConflict?: boolean
|
|
2185
|
+
queryError?: string
|
|
1707
2186
|
}
|
|
1708
2187
|
|
|
1709
2188
|
export interface PrometheusRightsizing {
|
|
@@ -1711,11 +2190,46 @@ export interface PrometheusRightsizing {
|
|
|
1711
2190
|
namespace: string
|
|
1712
2191
|
name: string
|
|
1713
2192
|
window: string
|
|
2193
|
+
source: 'radar'
|
|
2194
|
+
ownerCoverage: RightsizingOwnerCoverage
|
|
2195
|
+
scaledToZero: boolean
|
|
1714
2196
|
sampleAvailable: boolean
|
|
1715
2197
|
rows: RightsizingRow[]
|
|
1716
2198
|
reason?: string
|
|
1717
2199
|
}
|
|
1718
2200
|
|
|
2201
|
+
export type RightsizingScanState = 'complete' | 'partial' | 'unavailable'
|
|
2202
|
+
|
|
2203
|
+
export interface RightsizingScanWorkload {
|
|
2204
|
+
kind: string
|
|
2205
|
+
namespace: string
|
|
2206
|
+
name: string
|
|
2207
|
+
replicas: number
|
|
2208
|
+
scaledToZero: boolean
|
|
2209
|
+
rows: RightsizingRow[]
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
export interface RightsizingScanCoverage {
|
|
2213
|
+
workloadsDiscovered: number
|
|
2214
|
+
workloadsEvaluated: number
|
|
2215
|
+
workloadsWithData: number
|
|
2216
|
+
batches: number
|
|
2217
|
+
completedBatches: number
|
|
2218
|
+
restrictedKinds?: string[]
|
|
2219
|
+
unavailableKinds?: string[]
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
export interface RightsizingScanResponse {
|
|
2223
|
+
state: RightsizingScanState
|
|
2224
|
+
scannedAt: string
|
|
2225
|
+
window: string
|
|
2226
|
+
source: 'radar'
|
|
2227
|
+
coverage: RightsizingScanCoverage
|
|
2228
|
+
workloads: RightsizingScanWorkload[]
|
|
2229
|
+
warnings?: { code: string; message: string }[]
|
|
2230
|
+
reason?: string
|
|
2231
|
+
}
|
|
2232
|
+
|
|
1719
2233
|
// Check Prometheus availability
|
|
1720
2234
|
export function usePrometheusStatus() {
|
|
1721
2235
|
return useQuery<PrometheusStatus>({
|
|
@@ -1726,12 +2240,32 @@ export function usePrometheusStatus() {
|
|
|
1726
2240
|
})
|
|
1727
2241
|
}
|
|
1728
2242
|
|
|
2243
|
+
export interface ArgoStatus {
|
|
2244
|
+
// configured = a URL or token is set; connected = a probe has landed and the
|
|
2245
|
+
// client is live. The two differ right after a restart (configured, reconnecting).
|
|
2246
|
+
configured: boolean
|
|
2247
|
+
connected: boolean
|
|
2248
|
+
address?: string
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
export function useArgoStatus(enabled = true) {
|
|
2252
|
+
return useQuery<ArgoStatus>({
|
|
2253
|
+
queryKey: ['argocd-status'],
|
|
2254
|
+
queryFn: () => fetchJSON('/integrations/argocd/status'),
|
|
2255
|
+
enabled,
|
|
2256
|
+
staleTime: 30000,
|
|
2257
|
+
refetchInterval: 60000,
|
|
2258
|
+
})
|
|
2259
|
+
}
|
|
2260
|
+
|
|
1729
2261
|
// Connect to Prometheus (trigger discovery)
|
|
1730
2262
|
export function usePrometheusConnect() {
|
|
1731
2263
|
const queryClient = useQueryClient()
|
|
1732
2264
|
return useMutation({
|
|
1733
2265
|
mutationFn: async () => {
|
|
1734
|
-
const resp = await apiFetch(`${getApiBase()}/prometheus/connect`, {
|
|
2266
|
+
const resp = await apiFetch(`${getApiBase()}/prometheus/connect`, {
|
|
2267
|
+
method: 'POST',
|
|
2268
|
+
})
|
|
1735
2269
|
if (!resp.ok) {
|
|
1736
2270
|
const body = await resp.json().catch(() => ({ error: 'Unknown error' }))
|
|
1737
2271
|
throw new Error(body.error || `HTTP ${resp.status}`)
|
|
@@ -1792,7 +2326,9 @@ export function useAutoPromConnect(): void {
|
|
|
1792
2326
|
|
|
1793
2327
|
// Persist the "we've connected here before" signal once a connection lands.
|
|
1794
2328
|
if (status?.connected) {
|
|
1795
|
-
try {
|
|
2329
|
+
try {
|
|
2330
|
+
window.localStorage.setItem(promAutoConnectKey(context), '1')
|
|
2331
|
+
} catch {
|
|
1796
2332
|
// localStorage can throw in some restricted browser modes — fail open.
|
|
1797
2333
|
}
|
|
1798
2334
|
return
|
|
@@ -1800,7 +2336,9 @@ export function useAutoPromConnect(): void {
|
|
|
1800
2336
|
|
|
1801
2337
|
if (attemptedRef.current === context) return
|
|
1802
2338
|
let cached: string | null = null
|
|
1803
|
-
try {
|
|
2339
|
+
try {
|
|
2340
|
+
cached = window.localStorage.getItem(promAutoConnectKey(context))
|
|
2341
|
+
} catch {
|
|
1804
2342
|
// keep the null fallback
|
|
1805
2343
|
}
|
|
1806
2344
|
|
|
@@ -1812,16 +2350,20 @@ export function useAutoPromConnect(): void {
|
|
|
1812
2350
|
const timeout = window.setTimeout(() => {
|
|
1813
2351
|
// Direct apiFetch (not via the usePrometheusConnect mutation) so the
|
|
1814
2352
|
// meta-driven toast handler stays silent — the user didn't click anything.
|
|
1815
|
-
apiFetch(`${getApiBase()}/prometheus/connect?optional=true`, {
|
|
1816
|
-
|
|
2353
|
+
apiFetch(`${getApiBase()}/prometheus/connect?optional=true`, {
|
|
2354
|
+
method: 'POST',
|
|
2355
|
+
})
|
|
2356
|
+
.then(async (resp) => {
|
|
1817
2357
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
|
1818
|
-
const nextStatus = await resp.json() as PrometheusStatus
|
|
2358
|
+
const nextStatus = (await resp.json()) as PrometheusStatus
|
|
1819
2359
|
queryClient.setQueryData(['prometheus-status'], nextStatus)
|
|
1820
2360
|
if (!nextStatus.connected) throw new Error(nextStatus.error || 'Prometheus unavailable')
|
|
1821
2361
|
queryClient.invalidateQueries({ queryKey: ['prometheus-status'] })
|
|
1822
2362
|
})
|
|
1823
2363
|
.catch(() => {
|
|
1824
|
-
try {
|
|
2364
|
+
try {
|
|
2365
|
+
window.localStorage.removeItem(promAutoConnectKey(context))
|
|
2366
|
+
} catch {
|
|
1825
2367
|
// ignore — manual CTA will render once status refreshes
|
|
1826
2368
|
}
|
|
1827
2369
|
attemptedRef.current = null
|
|
@@ -1879,8 +2421,7 @@ export function usePrometheusClusterMetrics(
|
|
|
1879
2421
|
) {
|
|
1880
2422
|
return useQuery<PrometheusResourceMetrics>({
|
|
1881
2423
|
queryKey: ['prometheus-cluster-metrics', category, range],
|
|
1882
|
-
queryFn: () =>
|
|
1883
|
-
fetchJSON(`/prometheus/cluster?category=${category}&range=${range}`),
|
|
2424
|
+
queryFn: () => fetchJSON(`/prometheus/cluster?category=${category}&range=${range}`),
|
|
1884
2425
|
enabled,
|
|
1885
2426
|
staleTime: 30000,
|
|
1886
2427
|
refetchInterval: 60000,
|
|
@@ -1899,22 +2440,75 @@ export function usePrometheusPVCUsage(namespace: string, name: string, enabled =
|
|
|
1899
2440
|
}
|
|
1900
2441
|
|
|
1901
2442
|
// Fetch rightsizing recommendations for a workload (Deployment / StatefulSet / DaemonSet).
|
|
1902
|
-
export function usePrometheusRightsizing(
|
|
2443
|
+
export function usePrometheusRightsizing(
|
|
2444
|
+
kind: string,
|
|
2445
|
+
namespace: string,
|
|
2446
|
+
name: string,
|
|
2447
|
+
enabled = true,
|
|
2448
|
+
) {
|
|
1903
2449
|
return useQuery<PrometheusRightsizing>({
|
|
1904
2450
|
queryKey: ['prometheus-rightsizing', kind, namespace, name],
|
|
1905
2451
|
queryFn: () => fetchJSON(`/prometheus/rightsizing/${kind}/${namespace}/${name}`),
|
|
1906
2452
|
enabled: enabled && Boolean(kind && namespace && name),
|
|
1907
|
-
staleTime: 5 * 60 * 1000,
|
|
2453
|
+
staleTime: 5 * 60 * 1000,
|
|
1908
2454
|
refetchInterval: 10 * 60 * 1000,
|
|
1909
2455
|
})
|
|
1910
2456
|
}
|
|
1911
2457
|
|
|
2458
|
+
const RIGHTSIZING_SCAN_CACHE_TIME = 5 * 60 * 1000
|
|
2459
|
+
|
|
2460
|
+
export function getRightsizingScanCacheConfig(
|
|
2461
|
+
namespaces: string[],
|
|
2462
|
+
context = '',
|
|
2463
|
+
): {
|
|
2464
|
+
namespaceKey: string
|
|
2465
|
+
queryKey: readonly ['prometheus-rightsizing-scan', string, string]
|
|
2466
|
+
queryFn: typeof skipToken
|
|
2467
|
+
gcTime: number
|
|
2468
|
+
} {
|
|
2469
|
+
const namespaceKey = [...namespaces].sort().join(',')
|
|
2470
|
+
return {
|
|
2471
|
+
namespaceKey,
|
|
2472
|
+
queryKey: ['prometheus-rightsizing-scan', context, namespaceKey] as const,
|
|
2473
|
+
queryFn: skipToken,
|
|
2474
|
+
gcTime: RIGHTSIZING_SCAN_CACHE_TIME,
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
// A fleet rightsizing scan is intentionally manual. It can query seven days of
|
|
2479
|
+
// Prometheus history for many containers, so navigation alone must never run it.
|
|
2480
|
+
export function useRightsizingScan(namespaces: string[], context = '') {
|
|
2481
|
+
const queryClient = useQueryClient()
|
|
2482
|
+
const { namespaceKey, ...snapshotOptions } = getRightsizingScanCacheConfig(namespaces, context)
|
|
2483
|
+
const scanScope = { namespaceKey, queryKey: snapshotOptions.queryKey }
|
|
2484
|
+
const snapshot = useQuery<RightsizingScanResponse>(snapshotOptions)
|
|
2485
|
+
const mutation = useMutation({
|
|
2486
|
+
mutationFn: async (startedScope: typeof scanScope) => {
|
|
2487
|
+
const params = new URLSearchParams()
|
|
2488
|
+
if (startedScope.namespaceKey) params.set('namespaces', startedScope.namespaceKey)
|
|
2489
|
+
const query = params.toString()
|
|
2490
|
+
return fetchJSON<RightsizingScanResponse>(
|
|
2491
|
+
`/prometheus/rightsizing/scan${query ? `?${query}` : ''}`,
|
|
2492
|
+
{
|
|
2493
|
+
method: 'POST',
|
|
2494
|
+
},
|
|
2495
|
+
)
|
|
2496
|
+
},
|
|
2497
|
+
onSuccess: (result, startedScope) => queryClient.setQueryData(startedScope.queryKey, result),
|
|
2498
|
+
})
|
|
2499
|
+
return {
|
|
2500
|
+
...mutation,
|
|
2501
|
+
data: snapshot.data,
|
|
2502
|
+
mutate: () => mutation.mutate(scanScope),
|
|
2503
|
+
mutateAsync: () => mutation.mutateAsync(scanScope),
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
|
|
1912
2507
|
// Raw PromQL query (range). Used by HPA charts for status_current_replicas etc.
|
|
1913
2508
|
export function usePromQLRange(query: string, range: PrometheusTimeRange = '1h', enabled = true) {
|
|
1914
2509
|
return useQuery<PrometheusQueryResult>({
|
|
1915
2510
|
queryKey: ['promql-range', query, range],
|
|
1916
|
-
queryFn: () =>
|
|
1917
|
-
fetchJSON(`/prometheus/query?query=${encodeURIComponent(query)}&range=${range}`),
|
|
2511
|
+
queryFn: () => fetchJSON(`/prometheus/query?query=${encodeURIComponent(query)}&range=${range}`),
|
|
1918
2512
|
enabled: enabled && Boolean(query),
|
|
1919
2513
|
staleTime: 30000,
|
|
1920
2514
|
refetchInterval: 60000,
|
|
@@ -1947,12 +2541,16 @@ export interface LogStreamEvent {
|
|
|
1947
2541
|
}
|
|
1948
2542
|
|
|
1949
2543
|
// Fetch pod logs (non-streaming)
|
|
1950
|
-
export function usePodLogs(
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
2544
|
+
export function usePodLogs(
|
|
2545
|
+
namespace: string,
|
|
2546
|
+
podName: string,
|
|
2547
|
+
options?: {
|
|
2548
|
+
container?: string
|
|
2549
|
+
tailLines?: number
|
|
2550
|
+
previous?: boolean
|
|
2551
|
+
sinceSeconds?: number
|
|
2552
|
+
},
|
|
2553
|
+
) {
|
|
1956
2554
|
const params = new URLSearchParams()
|
|
1957
2555
|
if (options?.container) params.set('container', options.container)
|
|
1958
2556
|
if (options?.tailLines) params.set('tailLines', String(options.tailLines))
|
|
@@ -1961,8 +2559,17 @@ export function usePodLogs(namespace: string, podName: string, options?: {
|
|
|
1961
2559
|
const queryString = params.toString()
|
|
1962
2560
|
|
|
1963
2561
|
return useQuery<LogsResponse>({
|
|
1964
|
-
queryKey: [
|
|
1965
|
-
|
|
2562
|
+
queryKey: [
|
|
2563
|
+
'pod-logs',
|
|
2564
|
+
namespace,
|
|
2565
|
+
podName,
|
|
2566
|
+
options?.container,
|
|
2567
|
+
options?.tailLines,
|
|
2568
|
+
options?.previous,
|
|
2569
|
+
options?.sinceSeconds,
|
|
2570
|
+
],
|
|
2571
|
+
queryFn: () =>
|
|
2572
|
+
fetchJSON(`/pods/${namespace}/${podName}/logs${queryString ? `?${queryString}` : ''}`),
|
|
1966
2573
|
enabled: Boolean(namespace && podName),
|
|
1967
2574
|
staleTime: 5000, // Allow refetch after 5 seconds
|
|
1968
2575
|
})
|
|
@@ -1977,7 +2584,7 @@ export function createLogStream(
|
|
|
1977
2584
|
tailLines?: number
|
|
1978
2585
|
previous?: boolean
|
|
1979
2586
|
sinceSeconds?: number
|
|
1980
|
-
}
|
|
2587
|
+
},
|
|
1981
2588
|
): EventSource {
|
|
1982
2589
|
const params = new URLSearchParams()
|
|
1983
2590
|
if (options?.container) params.set('container', options.container)
|
|
@@ -1986,9 +2593,12 @@ export function createLogStream(
|
|
|
1986
2593
|
if (options?.sinceSeconds) params.set('sinceSeconds', String(options.sinceSeconds))
|
|
1987
2594
|
const queryString = params.toString()
|
|
1988
2595
|
|
|
1989
|
-
return new EventSource(
|
|
1990
|
-
|
|
1991
|
-
|
|
2596
|
+
return new EventSource(
|
|
2597
|
+
`${getApiBase()}/pods/${namespace}/${podName}/logs/stream${queryString ? `?${queryString}` : ''}`,
|
|
2598
|
+
{
|
|
2599
|
+
withCredentials: getCredentialsMode() === 'include',
|
|
2600
|
+
},
|
|
2601
|
+
)
|
|
1992
2602
|
}
|
|
1993
2603
|
|
|
1994
2604
|
// ============================================================================
|
|
@@ -2021,8 +2631,23 @@ export function useUpdateResource() {
|
|
|
2021
2631
|
const queryClient = useQueryClient()
|
|
2022
2632
|
|
|
2023
2633
|
return useMutation({
|
|
2024
|
-
mutationFn: async ({
|
|
2025
|
-
|
|
2634
|
+
mutationFn: async ({
|
|
2635
|
+
kind,
|
|
2636
|
+
namespace,
|
|
2637
|
+
name,
|
|
2638
|
+
yaml,
|
|
2639
|
+
force = true,
|
|
2640
|
+
}: {
|
|
2641
|
+
kind: string
|
|
2642
|
+
namespace: string
|
|
2643
|
+
name: string
|
|
2644
|
+
yaml: string
|
|
2645
|
+
force?: boolean
|
|
2646
|
+
}) => {
|
|
2647
|
+
const url = new URL(
|
|
2648
|
+
`${getApiBase()}/resources/${kind}/${namespace}/${name}`,
|
|
2649
|
+
window.location.origin,
|
|
2650
|
+
)
|
|
2026
2651
|
if (!force) {
|
|
2027
2652
|
url.searchParams.set('force', 'false')
|
|
2028
2653
|
}
|
|
@@ -2050,16 +2675,22 @@ export function useUpdateResource() {
|
|
|
2050
2675
|
// lagging cache — the change appears not to have taken effect.
|
|
2051
2676
|
if (updated && typeof updated === 'object' && updated.metadata) {
|
|
2052
2677
|
queryClient.setQueriesData(
|
|
2053
|
-
{
|
|
2678
|
+
{
|
|
2679
|
+
queryKey: ['resource', variables.kind, variables.namespace, variables.name],
|
|
2680
|
+
},
|
|
2054
2681
|
(old: any) =>
|
|
2055
2682
|
old && typeof old === 'object' && 'resource' in old
|
|
2056
2683
|
? { ...old, resource: updated }
|
|
2057
|
-
: { resource: updated }
|
|
2684
|
+
: { resource: updated },
|
|
2058
2685
|
)
|
|
2059
2686
|
} else {
|
|
2060
|
-
queryClient.invalidateQueries({
|
|
2687
|
+
queryClient.invalidateQueries({
|
|
2688
|
+
queryKey: ['resource', variables.kind, variables.namespace, variables.name],
|
|
2689
|
+
})
|
|
2061
2690
|
}
|
|
2062
|
-
queryClient.invalidateQueries({
|
|
2691
|
+
queryClient.invalidateQueries({
|
|
2692
|
+
queryKey: ['resources', variables.kind],
|
|
2693
|
+
})
|
|
2063
2694
|
queryClient.invalidateQueries({ queryKey: ['topology'] })
|
|
2064
2695
|
},
|
|
2065
2696
|
})
|
|
@@ -2068,13 +2699,24 @@ export function useUpdateResource() {
|
|
|
2068
2699
|
// Cascade delete preview — shows resources that will be garbage-collected
|
|
2069
2700
|
export interface CascadeDeletePreview {
|
|
2070
2701
|
root: { kind: string; namespace: string; name: string; group?: string }
|
|
2071
|
-
dependents: {
|
|
2702
|
+
dependents: {
|
|
2703
|
+
kind: string
|
|
2704
|
+
namespace: string
|
|
2705
|
+
name: string
|
|
2706
|
+
group?: string
|
|
2707
|
+
}[]
|
|
2072
2708
|
}
|
|
2073
2709
|
|
|
2074
|
-
export function useCascadeDeletePreview(
|
|
2710
|
+
export function useCascadeDeletePreview(
|
|
2711
|
+
kind: string,
|
|
2712
|
+
namespace: string,
|
|
2713
|
+
name: string,
|
|
2714
|
+
enabled: boolean,
|
|
2715
|
+
) {
|
|
2075
2716
|
return useQuery<CascadeDeletePreview>({
|
|
2076
2717
|
queryKey: ['cascade-preview', kind, namespace, name],
|
|
2077
|
-
queryFn: () =>
|
|
2718
|
+
queryFn: () =>
|
|
2719
|
+
fetchJSON<CascadeDeletePreview>(`/resources/${kind}/${namespace}/${name}/cascade-preview`),
|
|
2078
2720
|
enabled,
|
|
2079
2721
|
staleTime: 30_000,
|
|
2080
2722
|
})
|
|
@@ -2085,8 +2727,23 @@ export function useDeleteResource() {
|
|
|
2085
2727
|
const queryClient = useQueryClient()
|
|
2086
2728
|
|
|
2087
2729
|
return useMutation({
|
|
2088
|
-
mutationFn: async ({
|
|
2089
|
-
|
|
2730
|
+
mutationFn: async ({
|
|
2731
|
+
kind,
|
|
2732
|
+
group,
|
|
2733
|
+
namespace,
|
|
2734
|
+
name,
|
|
2735
|
+
force,
|
|
2736
|
+
}: {
|
|
2737
|
+
kind: string
|
|
2738
|
+
group?: string
|
|
2739
|
+
namespace: string
|
|
2740
|
+
name: string
|
|
2741
|
+
force?: boolean
|
|
2742
|
+
}) => {
|
|
2743
|
+
const url = new URL(
|
|
2744
|
+
`${getApiBase()}/resources/${kind}/${namespace}/${name}`,
|
|
2745
|
+
window.location.origin,
|
|
2746
|
+
)
|
|
2090
2747
|
if (group) {
|
|
2091
2748
|
url.searchParams.set('group', group)
|
|
2092
2749
|
}
|
|
@@ -2108,7 +2765,9 @@ export function useDeleteResource() {
|
|
|
2108
2765
|
successMessage: 'Resource deleted',
|
|
2109
2766
|
},
|
|
2110
2767
|
onSuccess: (_, variables) => {
|
|
2111
|
-
queryClient.invalidateQueries({
|
|
2768
|
+
queryClient.invalidateQueries({
|
|
2769
|
+
queryKey: ['resources', variables.kind],
|
|
2770
|
+
})
|
|
2112
2771
|
queryClient.invalidateQueries({ queryKey: ['topology'] })
|
|
2113
2772
|
},
|
|
2114
2773
|
})
|
|
@@ -2118,10 +2777,24 @@ export function useBulkDeleteResources() {
|
|
|
2118
2777
|
const queryClient = useQueryClient()
|
|
2119
2778
|
|
|
2120
2779
|
return useMutation({
|
|
2121
|
-
mutationFn: async ({
|
|
2780
|
+
mutationFn: async ({
|
|
2781
|
+
items,
|
|
2782
|
+
force,
|
|
2783
|
+
}: {
|
|
2784
|
+
items: Array<{
|
|
2785
|
+
kind: string
|
|
2786
|
+
group?: string
|
|
2787
|
+
namespace: string
|
|
2788
|
+
name: string
|
|
2789
|
+
}>
|
|
2790
|
+
force?: boolean
|
|
2791
|
+
}) => {
|
|
2122
2792
|
const results = await Promise.allSettled(
|
|
2123
2793
|
items.map(async ({ kind, group, namespace, name }) => {
|
|
2124
|
-
const url = new URL(
|
|
2794
|
+
const url = new URL(
|
|
2795
|
+
`${getApiBase()}/resources/${kind}/${namespace}/${name}`,
|
|
2796
|
+
window.location.origin,
|
|
2797
|
+
)
|
|
2125
2798
|
if (group) url.searchParams.set('group', group)
|
|
2126
2799
|
if (force) url.searchParams.set('force', 'true')
|
|
2127
2800
|
const response = await apiFetch(url.toString(), { method: 'DELETE' })
|
|
@@ -2130,9 +2803,9 @@ export function useBulkDeleteResources() {
|
|
|
2130
2803
|
throw new Error(error.error || `Failed to delete ${namespace}/${name}`)
|
|
2131
2804
|
}
|
|
2132
2805
|
return { kind, namespace, name }
|
|
2133
|
-
})
|
|
2806
|
+
}),
|
|
2134
2807
|
)
|
|
2135
|
-
const failed = results.filter(r => r.status === 'rejected')
|
|
2808
|
+
const failed = results.filter((r) => r.status === 'rejected')
|
|
2136
2809
|
if (failed.length > 0) {
|
|
2137
2810
|
throw new Error(`Failed to delete ${failed.length} of ${items.length} resources`)
|
|
2138
2811
|
}
|
|
@@ -2165,13 +2838,19 @@ interface BulkWorkloadMutationResult {
|
|
|
2165
2838
|
}
|
|
2166
2839
|
|
|
2167
2840
|
function failedBulkWorkloadMessages(results: PromiseSettledResult<unknown>[]): string[] {
|
|
2168
|
-
return results.flatMap(r =>
|
|
2169
|
-
|
|
2170
|
-
|
|
2841
|
+
return results.flatMap((r) =>
|
|
2842
|
+
r.status === 'rejected'
|
|
2843
|
+
? [r.reason instanceof Error ? r.reason.message : String(r.reason)]
|
|
2844
|
+
: [],
|
|
2171
2845
|
)
|
|
2172
2846
|
}
|
|
2173
2847
|
|
|
2174
|
-
function bulkWorkloadFailureMessage(
|
|
2848
|
+
function bulkWorkloadFailureMessage(
|
|
2849
|
+
action: string,
|
|
2850
|
+
failed: number,
|
|
2851
|
+
total: number,
|
|
2852
|
+
messages: string[],
|
|
2853
|
+
): string {
|
|
2175
2854
|
return `Failed to ${action} ${failed} of ${total} workloads:\n${messages.join('\n')}`
|
|
2176
2855
|
}
|
|
2177
2856
|
|
|
@@ -2179,27 +2858,45 @@ export function useBulkRestartWorkloads() {
|
|
|
2179
2858
|
const queryClient = useQueryClient()
|
|
2180
2859
|
|
|
2181
2860
|
return useMutation({
|
|
2182
|
-
mutationFn: async ({
|
|
2861
|
+
mutationFn: async ({
|
|
2862
|
+
items,
|
|
2863
|
+
}: {
|
|
2864
|
+
items: BulkWorkloadItem[]
|
|
2865
|
+
}): Promise<BulkWorkloadMutationResult> => {
|
|
2183
2866
|
if (items.length === 0) {
|
|
2184
2867
|
return { requested: 0, succeeded: 0, failedMessages: [] }
|
|
2185
2868
|
}
|
|
2186
2869
|
const results = await Promise.allSettled(
|
|
2187
2870
|
items.map(async ({ kind, namespace, name }) => {
|
|
2188
|
-
const response = await apiFetch(
|
|
2189
|
-
|
|
2190
|
-
|
|
2871
|
+
const response = await apiFetch(
|
|
2872
|
+
`${getApiBase()}/workloads/${kind}/${namespace}/${name}/restart`,
|
|
2873
|
+
{
|
|
2874
|
+
method: 'POST',
|
|
2875
|
+
},
|
|
2876
|
+
)
|
|
2191
2877
|
if (!response.ok) {
|
|
2192
2878
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
2193
2879
|
throw new Error(`${namespace}/${name}: ${error.error || `HTTP ${response.status}`}`)
|
|
2194
2880
|
}
|
|
2195
2881
|
return { kind, namespace, name }
|
|
2196
|
-
})
|
|
2882
|
+
}),
|
|
2197
2883
|
)
|
|
2198
2884
|
const failedMessages = failedBulkWorkloadMessages(results)
|
|
2199
2885
|
if (failedMessages.length === items.length) {
|
|
2200
|
-
throw new Error(
|
|
2886
|
+
throw new Error(
|
|
2887
|
+
bulkWorkloadFailureMessage(
|
|
2888
|
+
'restart',
|
|
2889
|
+
failedMessages.length,
|
|
2890
|
+
items.length,
|
|
2891
|
+
failedMessages,
|
|
2892
|
+
),
|
|
2893
|
+
)
|
|
2894
|
+
}
|
|
2895
|
+
return {
|
|
2896
|
+
requested: items.length,
|
|
2897
|
+
succeeded: items.length - failedMessages.length,
|
|
2898
|
+
failedMessages,
|
|
2201
2899
|
}
|
|
2202
|
-
return { requested: items.length, succeeded: items.length - failedMessages.length, failedMessages }
|
|
2203
2900
|
},
|
|
2204
2901
|
meta: {
|
|
2205
2902
|
errorMessage: 'Failed to restart some workloads',
|
|
@@ -2225,29 +2922,44 @@ export function useBulkScaleWorkloads() {
|
|
|
2225
2922
|
const queryClient = useQueryClient()
|
|
2226
2923
|
|
|
2227
2924
|
return useMutation({
|
|
2228
|
-
mutationFn: async ({
|
|
2925
|
+
mutationFn: async ({
|
|
2926
|
+
items,
|
|
2927
|
+
replicas,
|
|
2928
|
+
}: {
|
|
2929
|
+
items: BulkWorkloadItem[]
|
|
2930
|
+
replicas: number
|
|
2931
|
+
}): Promise<BulkWorkloadMutationResult> => {
|
|
2229
2932
|
if (items.length === 0) {
|
|
2230
2933
|
return { requested: 0, succeeded: 0, failedMessages: [] }
|
|
2231
2934
|
}
|
|
2232
2935
|
const results = await Promise.allSettled(
|
|
2233
2936
|
items.map(async ({ kind, namespace, name }) => {
|
|
2234
|
-
const response = await apiFetch(
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2937
|
+
const response = await apiFetch(
|
|
2938
|
+
`${getApiBase()}/workloads/${kind}/${namespace}/${name}/scale`,
|
|
2939
|
+
{
|
|
2940
|
+
method: 'POST',
|
|
2941
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2942
|
+
body: JSON.stringify({ replicas }),
|
|
2943
|
+
},
|
|
2944
|
+
)
|
|
2239
2945
|
if (!response.ok) {
|
|
2240
2946
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
2241
2947
|
throw new Error(`${namespace}/${name}: ${error.error || `HTTP ${response.status}`}`)
|
|
2242
2948
|
}
|
|
2243
2949
|
return { kind, namespace, name }
|
|
2244
|
-
})
|
|
2950
|
+
}),
|
|
2245
2951
|
)
|
|
2246
2952
|
const failedMessages = failedBulkWorkloadMessages(results)
|
|
2247
2953
|
if (failedMessages.length === items.length) {
|
|
2248
|
-
throw new Error(
|
|
2954
|
+
throw new Error(
|
|
2955
|
+
bulkWorkloadFailureMessage('scale', failedMessages.length, items.length, failedMessages),
|
|
2956
|
+
)
|
|
2957
|
+
}
|
|
2958
|
+
return {
|
|
2959
|
+
requested: items.length,
|
|
2960
|
+
succeeded: items.length - failedMessages.length,
|
|
2961
|
+
failedMessages,
|
|
2249
2962
|
}
|
|
2250
|
-
return { requested: items.length, succeeded: items.length - failedMessages.length, failedMessages }
|
|
2251
2963
|
},
|
|
2252
2964
|
meta: {
|
|
2253
2965
|
errorMessage: 'Failed to scale some workloads',
|
|
@@ -2281,7 +2993,17 @@ export function useApplyResource() {
|
|
|
2281
2993
|
const queryClient = useQueryClient()
|
|
2282
2994
|
|
|
2283
2995
|
return useMutation({
|
|
2284
|
-
mutationFn: async ({
|
|
2996
|
+
mutationFn: async ({
|
|
2997
|
+
yaml,
|
|
2998
|
+
mode = 'apply',
|
|
2999
|
+
dryRun = false,
|
|
3000
|
+
force = false,
|
|
3001
|
+
}: {
|
|
3002
|
+
yaml: string
|
|
3003
|
+
mode?: 'apply' | 'create'
|
|
3004
|
+
dryRun?: boolean
|
|
3005
|
+
force?: boolean
|
|
3006
|
+
}) => {
|
|
2285
3007
|
const url = new URL(`${getApiBase()}/resources/apply`, window.location.origin)
|
|
2286
3008
|
url.searchParams.set('mode', mode)
|
|
2287
3009
|
if (dryRun) {
|
|
@@ -2314,11 +3036,19 @@ export function useApplyResource() {
|
|
|
2314
3036
|
// CronJob operations
|
|
2315
3037
|
// ============================================================================
|
|
2316
3038
|
|
|
2317
|
-
function invalidateCronJobOperationQueries(
|
|
3039
|
+
function invalidateCronJobOperationQueries(
|
|
3040
|
+
queryClient: ReturnType<typeof useQueryClient>,
|
|
3041
|
+
namespace: string,
|
|
3042
|
+
name: string,
|
|
3043
|
+
) {
|
|
2318
3044
|
queryClient.invalidateQueries({ queryKey: ['resources', 'cronjobs'] })
|
|
2319
3045
|
queryClient.invalidateQueries({ queryKey: ['resources', 'jobs'] })
|
|
2320
|
-
queryClient.invalidateQueries({
|
|
2321
|
-
|
|
3046
|
+
queryClient.invalidateQueries({
|
|
3047
|
+
queryKey: ['resource', 'cronjobs', namespace, name],
|
|
3048
|
+
})
|
|
3049
|
+
queryClient.invalidateQueries({
|
|
3050
|
+
queryKey: ['workload-runs', 'cronjobs', namespace, name],
|
|
3051
|
+
})
|
|
2322
3052
|
queryClient.invalidateQueries({ queryKey: ['applications'] })
|
|
2323
3053
|
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
|
|
2324
3054
|
queryClient.invalidateQueries({ queryKey: ['resource-counts'] })
|
|
@@ -2409,10 +3139,21 @@ export function useRestartWorkload() {
|
|
|
2409
3139
|
const queryClient = useQueryClient()
|
|
2410
3140
|
|
|
2411
3141
|
return useMutation({
|
|
2412
|
-
mutationFn: async ({
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
3142
|
+
mutationFn: async ({
|
|
3143
|
+
kind,
|
|
3144
|
+
namespace,
|
|
3145
|
+
name,
|
|
3146
|
+
}: {
|
|
3147
|
+
kind: string
|
|
3148
|
+
namespace: string
|
|
3149
|
+
name: string
|
|
3150
|
+
}) => {
|
|
3151
|
+
const response = await apiFetch(
|
|
3152
|
+
`${getApiBase()}/workloads/${kind}/${namespace}/${name}/restart`,
|
|
3153
|
+
{
|
|
3154
|
+
method: 'POST',
|
|
3155
|
+
},
|
|
3156
|
+
)
|
|
2416
3157
|
if (!response.ok) {
|
|
2417
3158
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
2418
3159
|
throw new Error(error.error || `HTTP ${response.status}`)
|
|
@@ -2424,7 +3165,9 @@ export function useRestartWorkload() {
|
|
|
2424
3165
|
successMessage: 'Workload restarting',
|
|
2425
3166
|
},
|
|
2426
3167
|
onSuccess: (_, variables) => {
|
|
2427
|
-
queryClient.invalidateQueries({
|
|
3168
|
+
queryClient.invalidateQueries({
|
|
3169
|
+
queryKey: ['resources', variables.kind],
|
|
3170
|
+
})
|
|
2428
3171
|
queryClient.invalidateQueries({ queryKey: ['topology'] })
|
|
2429
3172
|
},
|
|
2430
3173
|
})
|
|
@@ -2435,12 +3178,25 @@ export function useScaleWorkload() {
|
|
|
2435
3178
|
const queryClient = useQueryClient()
|
|
2436
3179
|
|
|
2437
3180
|
return useMutation({
|
|
2438
|
-
mutationFn: async ({
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
3181
|
+
mutationFn: async ({
|
|
3182
|
+
kind,
|
|
3183
|
+
namespace,
|
|
3184
|
+
name,
|
|
3185
|
+
replicas,
|
|
3186
|
+
}: {
|
|
3187
|
+
kind: string
|
|
3188
|
+
namespace: string
|
|
3189
|
+
name: string
|
|
3190
|
+
replicas: number
|
|
3191
|
+
}) => {
|
|
3192
|
+
const response = await apiFetch(
|
|
3193
|
+
`${getApiBase()}/workloads/${kind}/${namespace}/${name}/scale`,
|
|
3194
|
+
{
|
|
3195
|
+
method: 'POST',
|
|
3196
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3197
|
+
body: JSON.stringify({ replicas }),
|
|
3198
|
+
},
|
|
3199
|
+
)
|
|
2444
3200
|
if (!response.ok) {
|
|
2445
3201
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
2446
3202
|
throw new Error(error.error || `HTTP ${response.status}`)
|
|
@@ -2452,8 +3208,12 @@ export function useScaleWorkload() {
|
|
|
2452
3208
|
successMessage: 'Workload scaled',
|
|
2453
3209
|
},
|
|
2454
3210
|
onSuccess: (_, variables) => {
|
|
2455
|
-
queryClient.invalidateQueries({
|
|
2456
|
-
|
|
3211
|
+
queryClient.invalidateQueries({
|
|
3212
|
+
queryKey: ['resources', variables.kind],
|
|
3213
|
+
})
|
|
3214
|
+
queryClient.invalidateQueries({
|
|
3215
|
+
queryKey: ['resource', variables.kind, variables.namespace, variables.name],
|
|
3216
|
+
})
|
|
2457
3217
|
queryClient.invalidateQueries({ queryKey: ['topology'] })
|
|
2458
3218
|
},
|
|
2459
3219
|
})
|
|
@@ -2473,7 +3233,12 @@ export interface WorkloadRevision {
|
|
|
2473
3233
|
template?: string // Pod template spec as YAML (for revision diff)
|
|
2474
3234
|
}
|
|
2475
3235
|
|
|
2476
|
-
export function useWorkloadRevisions(
|
|
3236
|
+
export function useWorkloadRevisions(
|
|
3237
|
+
kind: string,
|
|
3238
|
+
namespace: string,
|
|
3239
|
+
name: string,
|
|
3240
|
+
enabled = true,
|
|
3241
|
+
) {
|
|
2477
3242
|
return useQuery<WorkloadRevision[]>({
|
|
2478
3243
|
queryKey: ['workload-revisions', kind, namespace, name],
|
|
2479
3244
|
queryFn: () => fetchJSON(`/workloads/${kind}/${namespace}/${name}/revisions`),
|
|
@@ -2484,12 +3249,25 @@ export function useWorkloadRevisions(kind: string, namespace: string, name: stri
|
|
|
2484
3249
|
export function useRollbackWorkload() {
|
|
2485
3250
|
const queryClient = useQueryClient()
|
|
2486
3251
|
return useMutation({
|
|
2487
|
-
mutationFn: async ({
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
3252
|
+
mutationFn: async ({
|
|
3253
|
+
kind,
|
|
3254
|
+
namespace,
|
|
3255
|
+
name,
|
|
3256
|
+
revision,
|
|
3257
|
+
}: {
|
|
3258
|
+
kind: string
|
|
3259
|
+
namespace: string
|
|
3260
|
+
name: string
|
|
3261
|
+
revision: number
|
|
3262
|
+
}) => {
|
|
3263
|
+
const response = await apiFetch(
|
|
3264
|
+
`${getApiBase()}/workloads/${kind}/${namespace}/${name}/rollback`,
|
|
3265
|
+
{
|
|
3266
|
+
method: 'POST',
|
|
3267
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3268
|
+
body: JSON.stringify({ revision }),
|
|
3269
|
+
},
|
|
3270
|
+
)
|
|
2493
3271
|
if (!response.ok) {
|
|
2494
3272
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
2495
3273
|
throw new Error(error.error || `HTTP ${response.status}`)
|
|
@@ -2501,9 +3279,15 @@ export function useRollbackWorkload() {
|
|
|
2501
3279
|
successMessage: 'Rollback initiated',
|
|
2502
3280
|
},
|
|
2503
3281
|
onSuccess: (_, variables) => {
|
|
2504
|
-
queryClient.invalidateQueries({
|
|
2505
|
-
|
|
2506
|
-
|
|
3282
|
+
queryClient.invalidateQueries({
|
|
3283
|
+
queryKey: ['resources', variables.kind],
|
|
3284
|
+
})
|
|
3285
|
+
queryClient.invalidateQueries({
|
|
3286
|
+
queryKey: ['resource', variables.kind, variables.namespace, variables.name],
|
|
3287
|
+
})
|
|
3288
|
+
queryClient.invalidateQueries({
|
|
3289
|
+
queryKey: ['workload-revisions', variables.kind, variables.namespace, variables.name],
|
|
3290
|
+
})
|
|
2507
3291
|
queryClient.invalidateQueries({ queryKey: ['topology'] })
|
|
2508
3292
|
},
|
|
2509
3293
|
})
|
|
@@ -2533,7 +3317,9 @@ export function useCordonNode() {
|
|
|
2533
3317
|
},
|
|
2534
3318
|
onSuccess: (_, variables) => {
|
|
2535
3319
|
queryClient.invalidateQueries({ queryKey: ['resources', 'nodes'] })
|
|
2536
|
-
queryClient.invalidateQueries({
|
|
3320
|
+
queryClient.invalidateQueries({
|
|
3321
|
+
queryKey: ['resource', 'nodes', '', variables.name],
|
|
3322
|
+
})
|
|
2537
3323
|
queryClient.invalidateQueries({ queryKey: ['topology'] })
|
|
2538
3324
|
},
|
|
2539
3325
|
})
|
|
@@ -2559,7 +3345,9 @@ export function useUncordonNode() {
|
|
|
2559
3345
|
},
|
|
2560
3346
|
onSuccess: (_, variables) => {
|
|
2561
3347
|
queryClient.invalidateQueries({ queryKey: ['resources', 'nodes'] })
|
|
2562
|
-
queryClient.invalidateQueries({
|
|
3348
|
+
queryClient.invalidateQueries({
|
|
3349
|
+
queryKey: ['resource', 'nodes', '', variables.name],
|
|
3350
|
+
})
|
|
2563
3351
|
queryClient.invalidateQueries({ queryKey: ['topology'] })
|
|
2564
3352
|
},
|
|
2565
3353
|
})
|
|
@@ -2592,7 +3380,9 @@ export function useDrainNode() {
|
|
|
2592
3380
|
},
|
|
2593
3381
|
onSuccess: (data: { evictedPods?: string[]; errors?: string[] }, variables) => {
|
|
2594
3382
|
queryClient.invalidateQueries({ queryKey: ['resources', 'nodes'] })
|
|
2595
|
-
queryClient.invalidateQueries({
|
|
3383
|
+
queryClient.invalidateQueries({
|
|
3384
|
+
queryKey: ['resource', 'nodes', '', variables.name],
|
|
3385
|
+
})
|
|
2596
3386
|
queryClient.invalidateQueries({ queryKey: ['topology'] })
|
|
2597
3387
|
|
|
2598
3388
|
const evicted = data?.evictedPods?.length ?? 0
|
|
@@ -2642,12 +3432,19 @@ export function useHelmRelease(namespace: string, name: string, options?: { enab
|
|
|
2642
3432
|
// `enabled` lets callers skip the query when the user's Cloud role
|
|
2643
3433
|
// would 403 the read — saves a round-trip and avoids a transient
|
|
2644
3434
|
// "error" state that the role-gated empty panel doesn't need.
|
|
2645
|
-
export function useHelmManifest(
|
|
3435
|
+
export function useHelmManifest(
|
|
3436
|
+
namespace: string,
|
|
3437
|
+
name: string,
|
|
3438
|
+
revision?: number,
|
|
3439
|
+
enabled = true,
|
|
3440
|
+
) {
|
|
2646
3441
|
const params = revision ? `?revision=${revision}` : ''
|
|
2647
3442
|
return useQuery<string>({
|
|
2648
3443
|
queryKey: ['helm-manifest', namespace, name, revision],
|
|
2649
3444
|
queryFn: async () => {
|
|
2650
|
-
const response = await apiFetch(
|
|
3445
|
+
const response = await apiFetch(
|
|
3446
|
+
`${getApiBase()}/helm/releases/${namespace}/${name}/manifest${params}`,
|
|
3447
|
+
)
|
|
2651
3448
|
if (!response.ok) {
|
|
2652
3449
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
2653
3450
|
throw new Error(error.error || `HTTP ${response.status}`)
|
|
@@ -2660,7 +3457,13 @@ export function useHelmManifest(namespace: string, name: string, revision?: numb
|
|
|
2660
3457
|
}
|
|
2661
3458
|
|
|
2662
3459
|
// Get values for a Helm release. `enabled` see useHelmManifest.
|
|
2663
|
-
export function useHelmValues(
|
|
3460
|
+
export function useHelmValues(
|
|
3461
|
+
namespace: string,
|
|
3462
|
+
name: string,
|
|
3463
|
+
allValues?: boolean,
|
|
3464
|
+
enabled = true,
|
|
3465
|
+
revision?: number,
|
|
3466
|
+
) {
|
|
2664
3467
|
const params = new URLSearchParams()
|
|
2665
3468
|
if (allValues) params.set('all', 'true')
|
|
2666
3469
|
if (revision && revision > 0) params.set('revision', String(revision))
|
|
@@ -2684,8 +3487,12 @@ export function useHelmManifestDiff(
|
|
|
2684
3487
|
return useQuery<ManifestDiff>({
|
|
2685
3488
|
queryKey: ['helm-diff', namespace, name, revision1, revision2],
|
|
2686
3489
|
queryFn: () =>
|
|
2687
|
-
fetchJSON(
|
|
2688
|
-
|
|
3490
|
+
fetchJSON(
|
|
3491
|
+
`/helm/releases/${namespace}/${name}/diff?revision1=${revision1}&revision2=${revision2}`,
|
|
3492
|
+
),
|
|
3493
|
+
enabled: Boolean(
|
|
3494
|
+
namespace && name && revision1 > 0 && revision2 > 0 && revision1 !== revision2 && enabled,
|
|
3495
|
+
),
|
|
2689
3496
|
staleTime: 60000,
|
|
2690
3497
|
})
|
|
2691
3498
|
}
|
|
@@ -2708,7 +3515,9 @@ export function useHelmValuesDiff(
|
|
|
2708
3515
|
if (allValues) params.set('all', 'true')
|
|
2709
3516
|
return fetchJSON(`/helm/releases/${namespace}/${name}/values/diff?${params.toString()}`)
|
|
2710
3517
|
},
|
|
2711
|
-
enabled: Boolean(
|
|
3518
|
+
enabled: Boolean(
|
|
3519
|
+
namespace && name && revision1 > 0 && revision2 > 0 && revision1 !== revision2 && enabled,
|
|
3520
|
+
),
|
|
2712
3521
|
staleTime: 60000,
|
|
2713
3522
|
})
|
|
2714
3523
|
}
|
|
@@ -2723,8 +3532,12 @@ export function useHelmNotesDiff(
|
|
|
2723
3532
|
return useQuery<NotesDiff>({
|
|
2724
3533
|
queryKey: ['helm-notes-diff', namespace, name, revision1, revision2],
|
|
2725
3534
|
queryFn: () =>
|
|
2726
|
-
fetchJSON(
|
|
2727
|
-
|
|
3535
|
+
fetchJSON(
|
|
3536
|
+
`/helm/releases/${namespace}/${name}/notes/diff?revision1=${revision1}&revision2=${revision2}`,
|
|
3537
|
+
),
|
|
3538
|
+
enabled: Boolean(
|
|
3539
|
+
namespace && name && revision1 > 0 && revision2 > 0 && revision1 !== revision2 && enabled,
|
|
3540
|
+
),
|
|
2728
3541
|
staleTime: 60000,
|
|
2729
3542
|
})
|
|
2730
3543
|
}
|
|
@@ -2739,8 +3552,12 @@ export function useHelmHooksDiff(
|
|
|
2739
3552
|
return useQuery<HooksDiff>({
|
|
2740
3553
|
queryKey: ['helm-hooks-diff', namespace, name, revision1, revision2],
|
|
2741
3554
|
queryFn: () =>
|
|
2742
|
-
fetchJSON(
|
|
2743
|
-
|
|
3555
|
+
fetchJSON(
|
|
3556
|
+
`/helm/releases/${namespace}/${name}/hooks/diff?revision1=${revision1}&revision2=${revision2}`,
|
|
3557
|
+
),
|
|
3558
|
+
enabled: Boolean(
|
|
3559
|
+
namespace && name && revision1 > 0 && revision2 > 0 && revision1 !== revision2 && enabled,
|
|
3560
|
+
),
|
|
2744
3561
|
staleTime: 60000,
|
|
2745
3562
|
})
|
|
2746
3563
|
}
|
|
@@ -2755,8 +3572,12 @@ export function useHelmResourceDiff(
|
|
|
2755
3572
|
return useQuery<ResourceDiff>({
|
|
2756
3573
|
queryKey: ['helm-resource-diff', namespace, name, revision1, revision2],
|
|
2757
3574
|
queryFn: () =>
|
|
2758
|
-
fetchJSON(
|
|
2759
|
-
|
|
3575
|
+
fetchJSON(
|
|
3576
|
+
`/helm/releases/${namespace}/${name}/resources/diff?revision1=${revision1}&revision2=${revision2}`,
|
|
3577
|
+
),
|
|
3578
|
+
enabled: Boolean(
|
|
3579
|
+
namespace && name && revision1 > 0 && revision2 > 0 && revision1 !== revision2 && enabled,
|
|
3580
|
+
),
|
|
2760
3581
|
staleTime: 60000,
|
|
2761
3582
|
})
|
|
2762
3583
|
}
|
|
@@ -2806,10 +3627,21 @@ export function useHelmRollback() {
|
|
|
2806
3627
|
const queryClient = useQueryClient()
|
|
2807
3628
|
|
|
2808
3629
|
return useMutation({
|
|
2809
|
-
mutationFn: async ({
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
3630
|
+
mutationFn: async ({
|
|
3631
|
+
namespace,
|
|
3632
|
+
name,
|
|
3633
|
+
revision,
|
|
3634
|
+
}: {
|
|
3635
|
+
namespace: string
|
|
3636
|
+
name: string
|
|
3637
|
+
revision: number
|
|
3638
|
+
}) => {
|
|
3639
|
+
const response = await apiFetch(
|
|
3640
|
+
`${getApiBase()}/helm/releases/${namespace}/${name}/rollback?revision=${revision}`,
|
|
3641
|
+
{
|
|
3642
|
+
method: 'POST',
|
|
3643
|
+
},
|
|
3644
|
+
)
|
|
2813
3645
|
if (!response.ok) {
|
|
2814
3646
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
2815
3647
|
throw new Error(error.error || `HTTP ${response.status}`)
|
|
@@ -2822,7 +3654,9 @@ export function useHelmRollback() {
|
|
|
2822
3654
|
},
|
|
2823
3655
|
onSuccess: (_, variables) => {
|
|
2824
3656
|
queryClient.invalidateQueries({ queryKey: ['helm-releases'] })
|
|
2825
|
-
queryClient.invalidateQueries({
|
|
3657
|
+
queryClient.invalidateQueries({
|
|
3658
|
+
queryKey: ['helm-release', variables.namespace, variables.name],
|
|
3659
|
+
})
|
|
2826
3660
|
},
|
|
2827
3661
|
})
|
|
2828
3662
|
}
|
|
@@ -2906,7 +3740,9 @@ function streamHelmProgress(
|
|
|
2906
3740
|
return
|
|
2907
3741
|
}
|
|
2908
3742
|
} catch (err) {
|
|
2909
|
-
reject(
|
|
3743
|
+
reject(
|
|
3744
|
+
err instanceof Error ? err : new Error(`${failureLabel}: invalid progress event`),
|
|
3745
|
+
)
|
|
2910
3746
|
return
|
|
2911
3747
|
}
|
|
2912
3748
|
}
|
|
@@ -2927,12 +3763,16 @@ export function upgradeWithProgress(
|
|
|
2927
3763
|
version: string,
|
|
2928
3764
|
repositoryName: string | undefined,
|
|
2929
3765
|
onProgress: (event: InstallProgressEvent) => void,
|
|
2930
|
-
values?: Record<string, unknown
|
|
3766
|
+
values?: Record<string, unknown>,
|
|
2931
3767
|
): Promise<void> {
|
|
2932
3768
|
const params = new URLSearchParams({ version })
|
|
2933
3769
|
if (repositoryName) params.set('repository', repositoryName)
|
|
2934
3770
|
const options: RequestInit = values
|
|
2935
|
-
? {
|
|
3771
|
+
? {
|
|
3772
|
+
method: 'POST',
|
|
3773
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3774
|
+
body: JSON.stringify({ values }),
|
|
3775
|
+
}
|
|
2936
3776
|
: { method: 'POST' }
|
|
2937
3777
|
return streamHelmProgress(
|
|
2938
3778
|
`${getApiBase()}/helm/releases/${namespace}/${name}/upgrade-stream?${params.toString()}`,
|
|
@@ -2947,7 +3787,7 @@ export function rollbackWithProgress(
|
|
|
2947
3787
|
namespace: string,
|
|
2948
3788
|
name: string,
|
|
2949
3789
|
revision: number,
|
|
2950
|
-
onProgress: (event: InstallProgressEvent) => void
|
|
3790
|
+
onProgress: (event: InstallProgressEvent) => void,
|
|
2951
3791
|
): Promise<void> {
|
|
2952
3792
|
return streamHelmProgress(
|
|
2953
3793
|
`${getApiBase()}/helm/releases/${namespace}/${name}/rollback-stream?revision=${revision}`,
|
|
@@ -2960,13 +3800,26 @@ export function rollbackWithProgress(
|
|
|
2960
3800
|
// When `version` is supplied, preview renders against that target chart version
|
|
2961
3801
|
// instead of the release's current chart.
|
|
2962
3802
|
export function useHelmPreviewValues() {
|
|
2963
|
-
return useMutation<
|
|
3803
|
+
return useMutation<
|
|
3804
|
+
ValuesPreviewResponse,
|
|
3805
|
+
Error,
|
|
3806
|
+
{
|
|
3807
|
+
namespace: string
|
|
3808
|
+
name: string
|
|
3809
|
+
values: Record<string, unknown>
|
|
3810
|
+
version?: string
|
|
3811
|
+
repository?: string
|
|
3812
|
+
}
|
|
3813
|
+
>({
|
|
2964
3814
|
mutationFn: async ({ namespace, name, values, version, repository }) => {
|
|
2965
|
-
const response = await apiFetch(
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
3815
|
+
const response = await apiFetch(
|
|
3816
|
+
`${getApiBase()}/helm/releases/${namespace}/${name}/values/preview`,
|
|
3817
|
+
{
|
|
3818
|
+
method: 'POST',
|
|
3819
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3820
|
+
body: JSON.stringify({ values, version, repository }),
|
|
3821
|
+
},
|
|
3822
|
+
)
|
|
2970
3823
|
if (!response.ok) {
|
|
2971
3824
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
2972
3825
|
throw new Error(error.error || `HTTP ${response.status}`)
|
|
@@ -2981,7 +3834,15 @@ export function useHelmApplyValues() {
|
|
|
2981
3834
|
const queryClient = useQueryClient()
|
|
2982
3835
|
|
|
2983
3836
|
return useMutation({
|
|
2984
|
-
mutationFn: async ({
|
|
3837
|
+
mutationFn: async ({
|
|
3838
|
+
namespace,
|
|
3839
|
+
name,
|
|
3840
|
+
values,
|
|
3841
|
+
}: {
|
|
3842
|
+
namespace: string
|
|
3843
|
+
name: string
|
|
3844
|
+
values: Record<string, unknown>
|
|
3845
|
+
}) => {
|
|
2985
3846
|
const response = await apiFetch(`${getApiBase()}/helm/releases/${namespace}/${name}/values`, {
|
|
2986
3847
|
method: 'PUT',
|
|
2987
3848
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -2999,8 +3860,12 @@ export function useHelmApplyValues() {
|
|
|
2999
3860
|
},
|
|
3000
3861
|
onSuccess: (_, variables) => {
|
|
3001
3862
|
queryClient.invalidateQueries({ queryKey: ['helm-releases'] })
|
|
3002
|
-
queryClient.invalidateQueries({
|
|
3003
|
-
|
|
3863
|
+
queryClient.invalidateQueries({
|
|
3864
|
+
queryKey: ['helm-release', variables.namespace, variables.name],
|
|
3865
|
+
})
|
|
3866
|
+
queryClient.invalidateQueries({
|
|
3867
|
+
queryKey: ['helm-values', variables.namespace, variables.name],
|
|
3868
|
+
})
|
|
3004
3869
|
},
|
|
3005
3870
|
})
|
|
3006
3871
|
}
|
|
@@ -3097,7 +3962,10 @@ export function useAddOCISource() {
|
|
|
3097
3962
|
const queryClient = useQueryClient()
|
|
3098
3963
|
return useMutation({
|
|
3099
3964
|
mutationFn: (source: string) => mutateOCISource('POST', source),
|
|
3100
|
-
meta: {
|
|
3965
|
+
meta: {
|
|
3966
|
+
errorMessage: 'Failed to add chart source',
|
|
3967
|
+
successMessage: 'Chart source added',
|
|
3968
|
+
},
|
|
3101
3969
|
onSuccess: () => invalidateHelmAfterSourceChange(queryClient),
|
|
3102
3970
|
})
|
|
3103
3971
|
}
|
|
@@ -3106,7 +3974,10 @@ export function useRemoveOCISource() {
|
|
|
3106
3974
|
const queryClient = useQueryClient()
|
|
3107
3975
|
return useMutation({
|
|
3108
3976
|
mutationFn: (source: string) => mutateOCISource('DELETE', source),
|
|
3109
|
-
meta: {
|
|
3977
|
+
meta: {
|
|
3978
|
+
errorMessage: 'Failed to remove chart source',
|
|
3979
|
+
successMessage: 'Chart source removed',
|
|
3980
|
+
},
|
|
3110
3981
|
onSuccess: () => invalidateHelmAfterSourceChange(queryClient),
|
|
3111
3982
|
})
|
|
3112
3983
|
}
|
|
@@ -3178,11 +4049,15 @@ export interface InstallProgressEvent {
|
|
|
3178
4049
|
// Install a chart with progress streaming via SSE
|
|
3179
4050
|
export function installChartWithProgress(
|
|
3180
4051
|
req: InstallChartRequest,
|
|
3181
|
-
onProgress: (event: InstallProgressEvent) => void
|
|
4052
|
+
onProgress: (event: InstallProgressEvent) => void,
|
|
3182
4053
|
): Promise<HelmRelease> {
|
|
3183
4054
|
return streamHelmProgress(
|
|
3184
4055
|
`${getApiBase()}/helm/releases/install-stream`,
|
|
3185
|
-
{
|
|
4056
|
+
{
|
|
4057
|
+
method: 'POST',
|
|
4058
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4059
|
+
body: JSON.stringify(req),
|
|
4060
|
+
},
|
|
3186
4061
|
onProgress,
|
|
3187
4062
|
'Install failed',
|
|
3188
4063
|
).then((event) => event.release as HelmRelease)
|
|
@@ -3198,8 +4073,14 @@ export type ArtifactHubSortOption = 'relevance' | 'stars' | 'last_updated'
|
|
|
3198
4073
|
// Search charts on ArtifactHub
|
|
3199
4074
|
export function useArtifactHubSearch(
|
|
3200
4075
|
query: string,
|
|
3201
|
-
options?: {
|
|
3202
|
-
|
|
4076
|
+
options?: {
|
|
4077
|
+
offset?: number
|
|
4078
|
+
limit?: number
|
|
4079
|
+
official?: boolean
|
|
4080
|
+
verified?: boolean
|
|
4081
|
+
sort?: ArtifactHubSortOption
|
|
4082
|
+
},
|
|
4083
|
+
enabled = true,
|
|
3203
4084
|
) {
|
|
3204
4085
|
const params = new URLSearchParams()
|
|
3205
4086
|
if (query) params.set('query', query)
|
|
@@ -3210,7 +4091,15 @@ export function useArtifactHubSearch(
|
|
|
3210
4091
|
if (options?.sort && options.sort !== 'relevance') params.set('sort', options.sort)
|
|
3211
4092
|
|
|
3212
4093
|
return useQuery<ArtifactHubSearchResult>({
|
|
3213
|
-
queryKey: [
|
|
4094
|
+
queryKey: [
|
|
4095
|
+
'artifacthub-search',
|
|
4096
|
+
query,
|
|
4097
|
+
options?.offset,
|
|
4098
|
+
options?.limit,
|
|
4099
|
+
options?.official,
|
|
4100
|
+
options?.verified,
|
|
4101
|
+
options?.sort,
|
|
4102
|
+
],
|
|
3214
4103
|
queryFn: () => fetchJSON(`/helm/artifacthub/search?${params.toString()}`),
|
|
3215
4104
|
enabled: enabled && query.length > 0,
|
|
3216
4105
|
staleTime: 60000, // 1 minute
|
|
@@ -3218,7 +4107,12 @@ export function useArtifactHubSearch(
|
|
|
3218
4107
|
}
|
|
3219
4108
|
|
|
3220
4109
|
// Get chart detail from ArtifactHub
|
|
3221
|
-
export function useArtifactHubChart(
|
|
4110
|
+
export function useArtifactHubChart(
|
|
4111
|
+
repoName: string,
|
|
4112
|
+
chartName: string,
|
|
4113
|
+
version?: string,
|
|
4114
|
+
enabled = true,
|
|
4115
|
+
) {
|
|
3222
4116
|
const path = version
|
|
3223
4117
|
? `/helm/artifacthub/charts/${repoName}/${chartName}/${version}`
|
|
3224
4118
|
: `/helm/artifacthub/charts/${repoName}/${chartName}`
|
|
@@ -3239,7 +4133,8 @@ interface GitOpsMutationConfig<TVariables> {
|
|
|
3239
4133
|
getPath: (variables: TVariables) => string
|
|
3240
4134
|
getBody?: (variables: TVariables) => unknown
|
|
3241
4135
|
errorMessage: string
|
|
3242
|
-
successMessage
|
|
4136
|
+
successMessage?: string
|
|
4137
|
+
getSuccessMessage?: (data: GitOpsOperationResponse) => string
|
|
3243
4138
|
getInvalidateKeys: (variables: TVariables) => (string | undefined)[][]
|
|
3244
4139
|
}
|
|
3245
4140
|
|
|
@@ -3267,9 +4162,10 @@ function createGitOpsMutation<TVariables>(config: GitOpsMutationConfig<TVariable
|
|
|
3267
4162
|
errorMessage: config.errorMessage,
|
|
3268
4163
|
successMessage: config.successMessage,
|
|
3269
4164
|
},
|
|
3270
|
-
onSuccess: (
|
|
3271
|
-
config.
|
|
3272
|
-
|
|
4165
|
+
onSuccess: (data, variables) => {
|
|
4166
|
+
if (config.getSuccessMessage) showApiSuccess(config.getSuccessMessage(data))
|
|
4167
|
+
config.getInvalidateKeys(variables).forEach((key) =>
|
|
4168
|
+
queryClient.invalidateQueries({ queryKey: key }),
|
|
3273
4169
|
)
|
|
3274
4170
|
},
|
|
3275
4171
|
})
|
|
@@ -3284,8 +4180,13 @@ type ArgoAppVars = { namespace: string; name: string }
|
|
|
3284
4180
|
// ArgoSyncVars extends ArgoAppVars with the sync request body fields. Only
|
|
3285
4181
|
// useArgoSync sends these — splitting the type prevents callers from passing
|
|
3286
4182
|
// resources/revision/prune to mutations that would silently drop them.
|
|
3287
|
-
type ArgoSyncVars = ArgoAppVars & {
|
|
3288
|
-
resources?: Array<{
|
|
4183
|
+
export type ArgoSyncVars = ArgoAppVars & {
|
|
4184
|
+
resources?: Array<{
|
|
4185
|
+
group?: string
|
|
4186
|
+
kind: string
|
|
4187
|
+
namespace?: string
|
|
4188
|
+
name: string
|
|
4189
|
+
}>
|
|
3289
4190
|
revision?: string
|
|
3290
4191
|
prune?: boolean
|
|
3291
4192
|
dryRun?: boolean
|
|
@@ -3297,6 +4198,31 @@ type ArgoSyncVars = ArgoAppVars & {
|
|
|
3297
4198
|
syncOptions?: string[]
|
|
3298
4199
|
}
|
|
3299
4200
|
|
|
4201
|
+
export interface ArgoResourceValidationResult {
|
|
4202
|
+
outcome: 'succeeded' | 'failed' | 'inconclusive'
|
|
4203
|
+
message: string
|
|
4204
|
+
resource?: {
|
|
4205
|
+
group?: string
|
|
4206
|
+
kind: string
|
|
4207
|
+
namespace?: string
|
|
4208
|
+
name: string
|
|
4209
|
+
status?: string
|
|
4210
|
+
message?: string
|
|
4211
|
+
}
|
|
4212
|
+
}
|
|
4213
|
+
|
|
4214
|
+
export function buildArgoResourceSyncVars(namespace: string, name: string, resource: GitOpsInsightRef, opts: ArgoSyncOpts): ArgoSyncVars {
|
|
4215
|
+
return {
|
|
4216
|
+
namespace,
|
|
4217
|
+
name,
|
|
4218
|
+
...opts,
|
|
4219
|
+
resources: [{ group: resource.group, kind: resource.kind, namespace: resource.namespace, name: resource.name }],
|
|
4220
|
+
revision: undefined,
|
|
4221
|
+
prune: false,
|
|
4222
|
+
applyOnly: false,
|
|
4223
|
+
}
|
|
4224
|
+
}
|
|
4225
|
+
|
|
3300
4226
|
// ArgoRollbackVars targets a specific Argo history entry by ID. Prune and
|
|
3301
4227
|
// DryRun mirror the sync flags so the rollback dialog can offer the same
|
|
3302
4228
|
// safety net.
|
|
@@ -3378,6 +4304,31 @@ export const useArgoSync = createGitOpsMutation<ArgoSyncVars>({
|
|
|
3378
4304
|
getInvalidateKeys: argoInvalidateKeys,
|
|
3379
4305
|
})
|
|
3380
4306
|
|
|
4307
|
+
export function useArgoResourceValidation() {
|
|
4308
|
+
const queryClient = useQueryClient()
|
|
4309
|
+
return useMutation<ArgoResourceValidationResult, Error, ArgoSyncVars>({
|
|
4310
|
+
mutationFn: async (variables) => {
|
|
4311
|
+
const response = await apiFetch(`${getApiBase()}/argo/applications/${variables.namespace}/${variables.name}/validate-resource`, {
|
|
4312
|
+
method: 'POST',
|
|
4313
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4314
|
+
body: JSON.stringify({
|
|
4315
|
+
resources: variables.resources,
|
|
4316
|
+
force: variables.force,
|
|
4317
|
+
syncOptions: variables.syncOptions,
|
|
4318
|
+
}),
|
|
4319
|
+
})
|
|
4320
|
+
if (!response.ok) {
|
|
4321
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
4322
|
+
throw new Error(error.error || `HTTP ${response.status}`)
|
|
4323
|
+
}
|
|
4324
|
+
return response.json() as Promise<ArgoResourceValidationResult>
|
|
4325
|
+
},
|
|
4326
|
+
onSettled: (_, __, variables) => {
|
|
4327
|
+
argoInvalidateKeys(variables).forEach(key => queryClient.invalidateQueries({ queryKey: key }))
|
|
4328
|
+
},
|
|
4329
|
+
})
|
|
4330
|
+
}
|
|
4331
|
+
|
|
3381
4332
|
export const useArgoRollback = createGitOpsMutation<ArgoRollbackVars>({
|
|
3382
4333
|
getPath: (v) => `/argo/applications/${v.namespace}/${v.name}/rollback`,
|
|
3383
4334
|
getBody: (v) => ({ id: v.id, prune: v.prune, dryRun: v.dryRun }),
|
|
@@ -3389,7 +4340,7 @@ export const useArgoRollback = createGitOpsMutation<ArgoRollbackVars>({
|
|
|
3389
4340
|
export const useArgoTerminate = createGitOpsMutation<ArgoAppVars>({
|
|
3390
4341
|
getPath: (v) => `/argo/applications/${v.namespace}/${v.name}/terminate`,
|
|
3391
4342
|
errorMessage: 'Failed to terminate sync',
|
|
3392
|
-
|
|
4343
|
+
getSuccessMessage: (data) => data.message,
|
|
3393
4344
|
getInvalidateKeys: argoInvalidateKeys,
|
|
3394
4345
|
})
|
|
3395
4346
|
|
|
@@ -3412,11 +4363,22 @@ export function useArgoRefresh() {
|
|
|
3412
4363
|
const queryClient = useQueryClient()
|
|
3413
4364
|
|
|
3414
4365
|
return useMutation({
|
|
3415
|
-
mutationFn: async ({
|
|
4366
|
+
mutationFn: async ({
|
|
4367
|
+
namespace,
|
|
4368
|
+
name,
|
|
4369
|
+
hard = false,
|
|
4370
|
+
}: {
|
|
4371
|
+
namespace: string
|
|
4372
|
+
name: string
|
|
4373
|
+
hard?: boolean
|
|
4374
|
+
}) => {
|
|
3416
4375
|
const params = hard ? '?type=hard' : ''
|
|
3417
|
-
const response = await apiFetch(
|
|
3418
|
-
|
|
3419
|
-
|
|
4376
|
+
const response = await apiFetch(
|
|
4377
|
+
`${getApiBase()}/argo/applications/${namespace}/${name}/refresh${params}`,
|
|
4378
|
+
{
|
|
4379
|
+
method: 'POST',
|
|
4380
|
+
},
|
|
4381
|
+
)
|
|
3420
4382
|
if (!response.ok) {
|
|
3421
4383
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
3422
4384
|
throw new Error(error.error || `HTTP ${response.status}`)
|
|
@@ -3433,7 +4395,7 @@ export function useArgoRefresh() {
|
|
|
3433
4395
|
// Refresh — without these two extra keys the user clicks Refresh and
|
|
3434
4396
|
// sees stale insight/tree data until the next staleTime tick.
|
|
3435
4397
|
argoInvalidateKeys(variables).forEach((key) =>
|
|
3436
|
-
queryClient.invalidateQueries({ queryKey: key })
|
|
4398
|
+
queryClient.invalidateQueries({ queryKey: key }),
|
|
3437
4399
|
)
|
|
3438
4400
|
},
|
|
3439
4401
|
})
|
|
@@ -3491,7 +4453,9 @@ export function useSwitchContext() {
|
|
|
3491
4453
|
} catch (error) {
|
|
3492
4454
|
clearTimeout(timeoutId)
|
|
3493
4455
|
if (error instanceof Error && error.name === 'AbortError') {
|
|
3494
|
-
throw new Error('Context switch timed out. The cluster may be unreachable.', {
|
|
4456
|
+
throw new Error('Context switch timed out. The cluster may be unreachable.', {
|
|
4457
|
+
cause: error,
|
|
4458
|
+
})
|
|
3495
4459
|
}
|
|
3496
4460
|
throw error
|
|
3497
4461
|
}
|
|
@@ -3609,9 +4573,12 @@ export function useSetActiveNamespace() {
|
|
|
3609
4573
|
error: error instanceof Error ? error.message : String(error),
|
|
3610
4574
|
})
|
|
3611
4575
|
if (error instanceof Error && error.name === 'AbortError') {
|
|
3612
|
-
throw new Error(
|
|
3613
|
-
|
|
3614
|
-
|
|
4576
|
+
throw new Error(
|
|
4577
|
+
isRescope
|
|
4578
|
+
? 'Namespace rescope timed out. The cluster may still be loading.'
|
|
4579
|
+
: 'Namespace switch timed out. The cluster may be unreachable.',
|
|
4580
|
+
{ cause: error },
|
|
4581
|
+
)
|
|
3615
4582
|
}
|
|
3616
4583
|
throw error
|
|
3617
4584
|
}
|
|
@@ -3623,7 +4590,9 @@ export function useSetActiveNamespace() {
|
|
|
3623
4590
|
accessibleCount: scope.accessibleNamespaces.length,
|
|
3624
4591
|
})
|
|
3625
4592
|
if (scope.cacheScoped) {
|
|
3626
|
-
queryClient.removeQueries({
|
|
4593
|
+
queryClient.removeQueries({
|
|
4594
|
+
predicate: (query) => query.queryKey[0] !== 'namespace-scope',
|
|
4595
|
+
})
|
|
3627
4596
|
}
|
|
3628
4597
|
queryClient.setQueryData<NamespaceScope>(['namespace-scope'], scope)
|
|
3629
4598
|
if (scope.cacheScoped) {
|
|
@@ -3654,7 +4623,7 @@ export function useImageMetadata(
|
|
|
3654
4623
|
namespace: string,
|
|
3655
4624
|
podName: string,
|
|
3656
4625
|
pullSecrets: string[],
|
|
3657
|
-
enabled = true
|
|
4626
|
+
enabled = true,
|
|
3658
4627
|
) {
|
|
3659
4628
|
const params = new URLSearchParams()
|
|
3660
4629
|
params.set('image', image)
|
|
@@ -3677,7 +4646,7 @@ export function useImageFilesystem(
|
|
|
3677
4646
|
namespace: string,
|
|
3678
4647
|
podName: string,
|
|
3679
4648
|
pullSecrets: string[],
|
|
3680
|
-
enabled = true
|
|
4649
|
+
enabled = true,
|
|
3681
4650
|
) {
|
|
3682
4651
|
const params = new URLSearchParams()
|
|
3683
4652
|
params.set('image', image)
|
|
@@ -3690,9 +4659,7 @@ export function useImageFilesystem(
|
|
|
3690
4659
|
return useQuery<ImageFilesystem>({
|
|
3691
4660
|
queryKey: ['image-filesystem', image, namespace, podName, pullSecrets.join(',')],
|
|
3692
4661
|
// Use skipToken to completely prevent the query from running when disabled
|
|
3693
|
-
queryFn: shouldFetch
|
|
3694
|
-
? () => fetchJSON(`/images/inspect?${params.toString()}`)
|
|
3695
|
-
: skipToken,
|
|
4662
|
+
queryFn: shouldFetch ? () => fetchJSON(`/images/inspect?${params.toString()}`) : skipToken,
|
|
3696
4663
|
staleTime: 300000, // 5 minutes - image content doesn't change
|
|
3697
4664
|
retry: false, // Don't retry on auth errors
|
|
3698
4665
|
})
|
|
@@ -3766,7 +4733,13 @@ export function useWorkloadPods(kind: string, namespace: string, name: string) {
|
|
|
3766
4733
|
})
|
|
3767
4734
|
}
|
|
3768
4735
|
|
|
3769
|
-
export function useWorkloadRuns(
|
|
4736
|
+
export function useWorkloadRuns(
|
|
4737
|
+
kind: string,
|
|
4738
|
+
namespace: string,
|
|
4739
|
+
name: string,
|
|
4740
|
+
enabled = true,
|
|
4741
|
+
options?: { refetchActive?: boolean; clusterScoped?: boolean },
|
|
4742
|
+
) {
|
|
3770
4743
|
const clusterScoped = options?.clusterScoped ?? false
|
|
3771
4744
|
const ns = clusterScoped ? '_' : namespace
|
|
3772
4745
|
const params = new URLSearchParams()
|
|
@@ -3775,11 +4748,12 @@ export function useWorkloadRuns(kind: string, namespace: string, name: string, e
|
|
|
3775
4748
|
|
|
3776
4749
|
return useQuery<WorkloadRunsResponse>({
|
|
3777
4750
|
queryKey: ['workload-runs', kind, namespace, name, clusterScoped],
|
|
3778
|
-
queryFn: () =>
|
|
4751
|
+
queryFn: () =>
|
|
4752
|
+
fetchJSON(`/workloads/${kind}/${ns}/${name}/runs${queryString ? `?${queryString}` : ''}`),
|
|
3779
4753
|
enabled: enabled && Boolean(kind && name && (namespace || clusterScoped)),
|
|
3780
4754
|
staleTime: 10000,
|
|
3781
4755
|
refetchInterval: options?.refetchActive
|
|
3782
|
-
? (query) => query.state.data?.runs?.some((run) => run.active) ? 5000 : 30000
|
|
4756
|
+
? (query) => (query.state.data?.runs?.some((run) => run.active) ? 5000 : 30000)
|
|
3783
4757
|
: false,
|
|
3784
4758
|
})
|
|
3785
4759
|
}
|
|
@@ -3793,7 +4767,7 @@ export function useWorkloadLogs(
|
|
|
3793
4767
|
container?: string
|
|
3794
4768
|
tailLines?: number
|
|
3795
4769
|
sinceSeconds?: number
|
|
3796
|
-
}
|
|
4770
|
+
},
|
|
3797
4771
|
) {
|
|
3798
4772
|
const params = new URLSearchParams()
|
|
3799
4773
|
if (options?.container) params.set('container', options.container)
|
|
@@ -3802,8 +4776,19 @@ export function useWorkloadLogs(
|
|
|
3802
4776
|
const queryString = params.toString()
|
|
3803
4777
|
|
|
3804
4778
|
return useQuery<WorkloadLogsResponse>({
|
|
3805
|
-
queryKey: [
|
|
3806
|
-
|
|
4779
|
+
queryKey: [
|
|
4780
|
+
'workload-logs',
|
|
4781
|
+
kind,
|
|
4782
|
+
namespace,
|
|
4783
|
+
name,
|
|
4784
|
+
options?.container,
|
|
4785
|
+
options?.tailLines,
|
|
4786
|
+
options?.sinceSeconds,
|
|
4787
|
+
],
|
|
4788
|
+
queryFn: () =>
|
|
4789
|
+
fetchJSON(
|
|
4790
|
+
`/workloads/${kind}/${namespace}/${name}/logs${queryString ? `?${queryString}` : ''}`,
|
|
4791
|
+
),
|
|
3807
4792
|
enabled: Boolean(kind && namespace && name),
|
|
3808
4793
|
staleTime: 5000,
|
|
3809
4794
|
})
|
|
@@ -3818,7 +4803,7 @@ export function createWorkloadLogStream(
|
|
|
3818
4803
|
container?: string
|
|
3819
4804
|
tailLines?: number
|
|
3820
4805
|
sinceSeconds?: number
|
|
3821
|
-
}
|
|
4806
|
+
},
|
|
3822
4807
|
): EventSource {
|
|
3823
4808
|
const params = new URLSearchParams()
|
|
3824
4809
|
if (options?.container) params.set('container', options.container)
|
|
@@ -3826,9 +4811,12 @@ export function createWorkloadLogStream(
|
|
|
3826
4811
|
if (options?.sinceSeconds) params.set('sinceSeconds', String(options.sinceSeconds))
|
|
3827
4812
|
const queryString = params.toString()
|
|
3828
4813
|
|
|
3829
|
-
return new EventSource(
|
|
3830
|
-
|
|
3831
|
-
|
|
4814
|
+
return new EventSource(
|
|
4815
|
+
`${getApiBase()}/workloads/${kind}/${namespace}/${name}/logs/stream${queryString ? `?${queryString}` : ''}`,
|
|
4816
|
+
{
|
|
4817
|
+
withCredentials: getCredentialsMode() === 'include',
|
|
4818
|
+
},
|
|
4819
|
+
)
|
|
3832
4820
|
}
|
|
3833
4821
|
|
|
3834
4822
|
// ============================================================================
|