@skyhook-io/radar-app 1.14.5 → 1.14.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -9
- package/package.json +6 -6
- package/src/App.tsx +91 -328
- package/src/RadarApp.tsx +3 -8
- package/src/api/client.jobset.test.ts +18 -0
- package/src/api/client.resource-identity.test.ts +44 -0
- package/src/api/client.rightsizing.test.ts +7 -3
- package/src/api/client.ts +233 -78
- package/src/api/preferences.test.ts +54 -0
- package/src/api/preferences.ts +38 -0
- package/src/components/CloudFunnelButton.test.tsx +155 -0
- package/src/components/CloudFunnelButton.tsx +14 -8
- package/src/components/ContextSwitcher.tsx +13 -132
- package/src/components/applications/ApplicationsView.identity.test.ts +39 -0
- package/src/components/applications/ApplicationsView.tsx +5 -3
- package/src/components/audit/AuditSettingsDialog.tsx +15 -11
- package/src/components/diagnose/AgentControls.tsx +26 -3
- package/src/components/diagnose/DiagnoseContext.tsx +1 -0
- package/src/components/diagnose/agentCatalog.ts +6 -0
- package/src/components/diagnose/launch.test.ts +23 -0
- package/src/components/diagnose/launch.ts +4 -0
- package/src/components/diagnose/parts.test.tsx +18 -0
- package/src/components/dock/NodeTerminalTab.test.tsx +34 -0
- package/src/components/dock/NodeTerminalTab.tsx +5 -3
- package/src/components/dock/WorkloadLogsTab.test.tsx +66 -0
- package/src/components/dock/WorkloadLogsTab.tsx +2 -1
- package/src/components/execution/BatchExecutionView.render.test.tsx +2 -1
- package/src/components/execution/BatchExecutionView.test.ts +52 -1
- package/src/components/execution/BatchExecutionView.tsx +324 -65
- package/src/components/execution/JobSetAdmission.test.tsx +42 -0
- package/src/components/execution/JobSetAdmission.tsx +17 -0
- package/src/components/execution/JobSetMemberComparison.test.tsx +30 -0
- package/src/components/execution/JobSetMemberComparison.tsx +88 -0
- package/src/components/execution/batch-run-actions.test.ts +1 -0
- package/src/components/execution/batch-timeline.test.ts +3 -0
- package/src/components/execution/execution-definition.test.ts +15 -1
- package/src/components/execution/execution-definition.ts +4 -0
- package/src/components/execution/member-collection.render.test.tsx +142 -0
- package/src/components/gitops/GitOpsView.tsx +33 -2
- package/src/components/gitops/destination-toast.test.ts +33 -0
- package/src/components/gitops/destination-toast.ts +46 -0
- package/src/components/gitops/useDestinationCluster.ts +30 -0
- package/src/components/helm/TrackChartSourceDialog.tsx +20 -11
- package/src/components/home/MCPSetupDialog.tsx +10 -0
- package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +75 -22
- package/src/components/logs/WorkloadLogsViewer.tsx +7 -4
- package/src/components/nav/PrimaryNavRail.tsx +2 -5
- package/src/components/resource/PVCUsageBar.render.test.tsx +168 -0
- package/src/components/resource/PVCUsageBar.tsx +62 -16
- package/src/components/resource/PrometheusChartsGrid.render.test.tsx +1 -1
- package/src/components/resources/ResourcesView.tsx +3 -3
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +8 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.test.tsx +39 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.tsx +37 -0
- package/src/components/resources/renderers/RayClusterRenderer.test.tsx +18 -0
- package/src/components/resources/renderers/RayClusterRenderer.tsx +17 -0
- package/src/components/resources/renderers/RayServiceRenderer.test.tsx +69 -0
- package/src/components/resources/renderers/RayServiceRenderer.tsx +38 -0
- package/src/components/rightsizing/RightsizingScanView.tsx +34 -22
- package/src/components/rightsizing/copy.test.ts +7 -0
- package/src/components/rightsizing/model.test.ts +2 -1
- package/src/components/rightsizing/notices.test.tsx +4 -3
- package/src/components/settings/OperatorManagedNotice.tsx +17 -0
- package/src/components/settings/SettingsDialog.tsx +157 -42
- package/src/components/settings/settings-state.test.ts +18 -0
- package/src/components/settings/settings-state.ts +18 -0
- package/src/components/timeline/TimelineView.tsx +9 -3
- package/src/components/timeline/TimelineView.urlparams.test.ts +18 -1
- package/src/components/ui/command-items.ts +0 -3
- package/src/components/useContextSwitchFlow.tsx +147 -0
- package/src/components/workload/WorkloadView.test.ts +17 -1
- package/src/components/workload/WorkloadView.tsx +36 -18
- package/src/context/ContextSwitchContext.tsx +18 -2
- package/src/context/NavCustomization.tsx +4 -58
- package/src/context/ThemeContext.tsx +3 -11
- package/src/hooks/useClusterLoadState.ts +2 -2
- package/src/hooks/useFavorites.ts +3 -5
- package/src/utils/auditBadges.ts +1 -1
- package/src/utils/navigation.test.ts +18 -1
- package/src/utils/navigation.ts +12 -5
- package/src/utils/topology-selection.test.ts +56 -0
- package/src/utils/topology-selection.ts +9 -9
- package/src/components/ui/CommandPalette.tsx +0 -261
package/src/RadarApp.tsx
CHANGED
|
@@ -84,12 +84,7 @@ export interface RadarAppProps {
|
|
|
84
84
|
* prefer to share its client rather than nest two providers.
|
|
85
85
|
*/
|
|
86
86
|
queryClient?: QueryClient;
|
|
87
|
-
/**
|
|
88
|
-
* Slot-based customization of Radar's top nav. Use to inject host-app
|
|
89
|
-
* brand, replace the kubeconfig context picker with a product-level
|
|
90
|
-
* cluster switcher, and append items to the right action bar.
|
|
91
|
-
* See ./context/NavCustomization for the slot shape.
|
|
92
|
-
*/
|
|
87
|
+
/** Embedded layout and host navigation hooks for Radar Hub. */
|
|
93
88
|
navSlots?: NavCustomization;
|
|
94
89
|
/**
|
|
95
90
|
* Whether Radar may set the browser tab title (`document.title`) per view.
|
|
@@ -128,14 +123,14 @@ export interface RadarAppProps {
|
|
|
128
123
|
/**
|
|
129
124
|
* Initial route for `router: 'memory'` (ignored for 'browser'). Lets a host
|
|
130
125
|
* deep-link a specific view (e.g. '/topology') without owning the URL bar —
|
|
131
|
-
* used with `navSlots.
|
|
126
|
+
* used with `navSlots.embedded: true` to render a single per-cluster view
|
|
132
127
|
* chromeless under the host's own chrome (Radar Hub's per-cluster destinations).
|
|
133
128
|
*/
|
|
134
129
|
initialPath?: string;
|
|
135
130
|
/**
|
|
136
131
|
* Reports cluster-data warmup after the main connection is usable. Embedders
|
|
137
132
|
* with their own chrome (Radar Hub) can render this in their topbar while
|
|
138
|
-
* Radar runs with `navSlots.
|
|
133
|
+
* Radar runs with `navSlots.embedded: true`.
|
|
139
134
|
*/
|
|
140
135
|
onClusterLoadStateChange?: (state: ClusterLoadState) => void;
|
|
141
136
|
/**
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { useQuery } from '@tanstack/react-query'
|
|
3
|
+
import { useWorkloadRuns, type WorkloadRunsResponse } from './client'
|
|
4
|
+
|
|
5
|
+
vi.mock('@tanstack/react-query', async (original) => ({ ...await original<typeof import('@tanstack/react-query')>(), useQuery: vi.fn(() => ({})) }))
|
|
6
|
+
|
|
7
|
+
describe('JobSet member polling', () => {
|
|
8
|
+
it('refreshes an active selected member even when the displayed window is idle', () => {
|
|
9
|
+
useWorkloadRuns('jobsets', 'training', 'distributed', true, { refetchActive: true, role: 'prepare', selected: 'jobs/training/worker' })
|
|
10
|
+
const options = vi.mocked(useQuery).mock.calls.at(-1)![0]
|
|
11
|
+
const interval = options.refetchInterval as (query: { state: { data: WorkloadRunsResponse } }) => number
|
|
12
|
+
const run = { group: 'batch', kind: 'jobs', namespace: 'training', name: 'prepare', phase: 'Succeeded', active: false }
|
|
13
|
+
const response: WorkloadRunsResponse = { collection: 'members', runs: [run], selected: { ...run, name: 'worker', phase: 'Running', active: true }, total: 2, filteredTotal: 1, truncated: false }
|
|
14
|
+
expect(interval({ state: { data: response } })).toBe(5000)
|
|
15
|
+
expect(interval({ state: { data: { ...response, selected: run } } })).toBe(30000)
|
|
16
|
+
expect(interval({ state: { data: { ...response, runs: [{ ...run, active: true }], selected: undefined } } })).toBe(5000)
|
|
17
|
+
})
|
|
18
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { useQuery } from '@tanstack/react-query'
|
|
3
|
+
import { useResourceAudit, useResourceEvents } from './client'
|
|
4
|
+
|
|
5
|
+
vi.mock('@tanstack/react-query', async (original) => ({
|
|
6
|
+
...await original<typeof import('@tanstack/react-query')>(),
|
|
7
|
+
useQuery: vi.fn(() => ({})),
|
|
8
|
+
}))
|
|
9
|
+
|
|
10
|
+
afterEach(() => { vi.clearAllMocks(); vi.unstubAllGlobals() })
|
|
11
|
+
|
|
12
|
+
describe('resource history identity', () => {
|
|
13
|
+
it('separates same-name core and Volcano history in requests and cache keys', async () => {
|
|
14
|
+
const urls: URL[] = []
|
|
15
|
+
vi.stubGlobal('fetch', vi.fn(async (input: string) => {
|
|
16
|
+
urls.push(new URL(input, 'http://localhost'))
|
|
17
|
+
return new Response('[]', {status: 200})
|
|
18
|
+
}))
|
|
19
|
+
useResourceEvents('jobs', 'ml', 'shared', 'batch')
|
|
20
|
+
useResourceEvents('jobs', 'ml', 'shared', 'batch.volcano.sh')
|
|
21
|
+
const queries = vi.mocked(useQuery).mock.calls.map(call => call[0])
|
|
22
|
+
expect(queries[0].queryKey).not.toEqual(queries[2].queryKey)
|
|
23
|
+
expect(queries[1].queryKey).not.toEqual(queries[3].queryKey)
|
|
24
|
+
for (const query of queries) await (query.queryFn as () => Promise<unknown>)()
|
|
25
|
+
expect(urls.map(url => url.searchParams.get('group'))).toEqual(['batch', 'batch', 'batch.volcano.sh', 'batch.volcano.sh'])
|
|
26
|
+
expect(urls.every(url => url.searchParams.get('kind') === 'Job')).toBe(true)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('does not query an unresolved custom identity', () => {
|
|
30
|
+
useResourceEvents('Widget', 'ml', 'shared')
|
|
31
|
+
expect(vi.mocked(useQuery).mock.calls.every(call => call[0].enabled === false)).toBe(true)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('preserves a core group and an exact audit group', async () => {
|
|
35
|
+
const fetchMock = vi.fn<typeof fetch>(async () => new Response('[]', {status: 200}))
|
|
36
|
+
vi.stubGlobal('fetch', fetchMock)
|
|
37
|
+
useResourceEvents('pods', 'ml', 'shared', '')
|
|
38
|
+
await (vi.mocked(useQuery).mock.calls[0][0].queryFn as () => Promise<unknown>)()
|
|
39
|
+
expect(new URL(String(fetchMock.mock.calls[0][0]), 'http://localhost').searchParams.get('group')).toBe('')
|
|
40
|
+
useResourceAudit('IngressRoute', 'ml', 'shared', 'traefik.io')
|
|
41
|
+
await (vi.mocked(useQuery).mock.calls.at(-1)![0].queryFn as () => Promise<unknown>)()
|
|
42
|
+
expect(fetchMock).toHaveBeenLastCalledWith('/api/audit/resource/IngressRoute/ml/shared?group=traefik.io', expect.anything())
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { skipToken } from '@tanstack/react-query'
|
|
3
2
|
import { getRightsizingScanCacheConfig } from './client'
|
|
4
3
|
|
|
5
4
|
describe('rightsizing scan cache config', () => {
|
|
6
|
-
it('
|
|
5
|
+
it('retains result lookups briefly', () => {
|
|
7
6
|
const config = getRightsizingScanCacheConfig(['staging', 'default'], 'cluster-a')
|
|
8
7
|
|
|
9
|
-
expect(config.queryFn).toBe(skipToken)
|
|
10
8
|
expect(config.gcTime).toBe(5 * 60 * 1000)
|
|
11
9
|
})
|
|
12
10
|
|
|
11
|
+
it('isolates identities', () => {
|
|
12
|
+
expect(getRightsizingScanCacheConfig([], 'a', 'alice').queryKey).not.toEqual(getRightsizingScanCacheConfig([], 'a', 'bob').queryKey)
|
|
13
|
+
})
|
|
14
|
+
|
|
13
15
|
it('normalizes namespace order and isolates cluster and namespace scopes', () => {
|
|
14
16
|
const config = getRightsizingScanCacheConfig(['staging', 'default'], 'cluster-a')
|
|
15
17
|
|
|
16
18
|
expect(config.namespaceKey).toBe('default,staging')
|
|
17
19
|
expect(config.queryKey).toEqual([
|
|
18
20
|
'prometheus-rightsizing-scan',
|
|
21
|
+
'/api',
|
|
22
|
+
'',
|
|
19
23
|
'cluster-a',
|
|
20
24
|
'default,staging',
|
|
21
25
|
])
|
package/src/api/client.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { canonicalResourceGroup } from '@skyhook-io/k8s-ui/utils/api-resources'
|
|
2
|
+
import { knownKindForPluralWithGroup, pluralToKind } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
1
3
|
import { useEffect, useRef } from 'react'
|
|
4
|
+
import type { KueueAdmissionResponse } from '@skyhook-io/k8s-ui/types/scheduling'
|
|
2
5
|
import type {
|
|
3
6
|
AppHistory,
|
|
4
7
|
AppRow,
|
|
@@ -56,7 +59,7 @@ import type {
|
|
|
56
59
|
} from '../types'
|
|
57
60
|
import type { GitOpsOperationResponse } from '../types/gitops'
|
|
58
61
|
import { apiUrl, getApiBase, getAuthHeaders, getCredentialsMode, getBasename, routePath, stripBasename } from './config'
|
|
59
|
-
import { apiVersionToGroup
|
|
62
|
+
import { apiVersionToGroup } from '../utils/navigation'
|
|
60
63
|
import type { DeploymentMode } from '../types'
|
|
61
64
|
|
|
62
65
|
// Auto-refresh cadences (ms) — named constants for each polled hook's
|
|
@@ -723,10 +726,14 @@ export function useResourceAudit(
|
|
|
723
726
|
kind: string,
|
|
724
727
|
namespace: string,
|
|
725
728
|
name: string,
|
|
729
|
+
group?: string,
|
|
726
730
|
) {
|
|
731
|
+
const params = new URLSearchParams();
|
|
732
|
+
if (group) params.set("group", group);
|
|
733
|
+
const query = params.toString();
|
|
727
734
|
return useQuery<AuditFinding[]>({
|
|
728
|
-
queryKey: ["audit", "resource", kind, namespace, name],
|
|
729
|
-
queryFn: () => fetchJSON(`/audit/resource/${kind}/${namespace}/${name}`),
|
|
735
|
+
queryKey: ["audit", "resource", kind, group ?? "", namespace, name],
|
|
736
|
+
queryFn: () => fetchJSON(`/audit/resource/${kind}/${namespace}/${name}${query ? `?${query}` : ""}`),
|
|
730
737
|
staleTime: 30000,
|
|
731
738
|
});
|
|
732
739
|
}
|
|
@@ -2808,30 +2815,6 @@ export function useChanges(options: UseChangesOptions = {}) {
|
|
|
2808
2815
|
});
|
|
2809
2816
|
}
|
|
2810
2817
|
|
|
2811
|
-
// Children changes for a parent workload (e.g., ReplicaSets and Pods under a Deployment)
|
|
2812
|
-
export function useResourceChildren(
|
|
2813
|
-
kind: string,
|
|
2814
|
-
namespace: string,
|
|
2815
|
-
name: string,
|
|
2816
|
-
timeRange: TimeRange = "1h",
|
|
2817
|
-
) {
|
|
2818
|
-
const sinceDate = getTimeRangeDate(timeRange);
|
|
2819
|
-
const params = new URLSearchParams();
|
|
2820
|
-
if (sinceDate) {
|
|
2821
|
-
params.set("since", sinceDate.toISOString());
|
|
2822
|
-
}
|
|
2823
|
-
|
|
2824
|
-
return useQuery<TimelineEvent[]>({
|
|
2825
|
-
queryKey: ["resource-children", kind, namespace, name, timeRange],
|
|
2826
|
-
queryFn: () =>
|
|
2827
|
-
fetchJSON(
|
|
2828
|
-
`/changes/${kind}/${namespace}/${name}/children?${params.toString()}`,
|
|
2829
|
-
),
|
|
2830
|
-
enabled: Boolean(kind && namespace && name),
|
|
2831
|
-
refetchInterval: 15000, // Refresh every 15 seconds
|
|
2832
|
-
});
|
|
2833
|
-
}
|
|
2834
|
-
|
|
2835
2818
|
export interface ResourceEventsResult {
|
|
2836
2819
|
k8sEvents: TimelineEvent[];
|
|
2837
2820
|
updates: TimelineEvent[];
|
|
@@ -2850,10 +2833,12 @@ export function useResourceEvents(
|
|
|
2850
2833
|
kind: string,
|
|
2851
2834
|
namespace: string,
|
|
2852
2835
|
name: string,
|
|
2836
|
+
group?: string,
|
|
2853
2837
|
): ResourceEventsResult {
|
|
2854
2838
|
// The timeline store keys events by their K8s Kind (singular PascalCase, e.g. "Pod"),
|
|
2855
2839
|
// but callers pass the URL-form kind ("pods").
|
|
2856
|
-
const singularKind = pluralToKind(kind);
|
|
2840
|
+
const singularKind = knownKindForPluralWithGroup(kind, group ?? "") ?? pluralToKind(kind);
|
|
2841
|
+
const resolvedGroup = canonicalResourceGroup(singularKind, group);
|
|
2857
2842
|
const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
|
2858
2843
|
|
|
2859
2844
|
// Include managed resources — when viewing a specific resource (e.g. a Pod owned
|
|
@@ -2864,18 +2849,19 @@ export function useResourceEvents(
|
|
|
2864
2849
|
p.set("namespace", namespace);
|
|
2865
2850
|
p.set("kind", singularKind);
|
|
2866
2851
|
p.set("name", name);
|
|
2852
|
+
if (resolvedGroup !== undefined) p.set("group", resolvedGroup);
|
|
2867
2853
|
p.set("include_managed", "true");
|
|
2868
2854
|
p.set("since", since);
|
|
2869
2855
|
return p;
|
|
2870
2856
|
};
|
|
2871
2857
|
|
|
2872
|
-
const enabled = Boolean(kind && namespace && name);
|
|
2858
|
+
const enabled = Boolean(kind && namespace && name && resolvedGroup !== undefined);
|
|
2873
2859
|
|
|
2874
2860
|
// K8s events: high limit so the full set is always returned. The number of
|
|
2875
2861
|
// distinct K8s events per resource is naturally bounded — kubelet/controllers
|
|
2876
2862
|
// dedupe via Reason+InvolvedObject and bump count.
|
|
2877
2863
|
const k8sQuery = useQuery<TimelineEvent[]>({
|
|
2878
|
-
queryKey: ["resource-events", "k8s", singularKind, namespace, name],
|
|
2864
|
+
queryKey: ["resource-events", "k8s", singularKind, resolvedGroup, namespace, name],
|
|
2879
2865
|
queryFn: async () => {
|
|
2880
2866
|
const params = baseParams();
|
|
2881
2867
|
params.set("sources", "k8s_event");
|
|
@@ -2889,7 +2875,7 @@ export function useResourceEvents(
|
|
|
2889
2875
|
// Resource updates (informer diffs + historical): bounded so a flapping
|
|
2890
2876
|
// resource doesn't return an unbounded payload.
|
|
2891
2877
|
const updatesQuery = useQuery<TimelineEvent[]>({
|
|
2892
|
-
queryKey: ["resource-events", "updates", singularKind, namespace, name],
|
|
2878
|
+
queryKey: ["resource-events", "updates", singularKind, resolvedGroup, namespace, name],
|
|
2893
2879
|
queryFn: async () => {
|
|
2894
2880
|
const params = baseParams();
|
|
2895
2881
|
params.set("sources", "informer,historical");
|
|
@@ -3272,9 +3258,9 @@ export type PrometheusTimeRange =
|
|
|
3272
3258
|
"10m" | "30m" | "1h" | "3h" | "6h" | "12h" | "24h" | "48h" | "7d" | "14d";
|
|
3273
3259
|
|
|
3274
3260
|
// PVC usage at a moment in time, derived from kubelet_volume_stats_*.
|
|
3275
|
-
// HasData=false silently indicates the CSI driver doesn't report or Prom
|
|
3276
|
-
// isn't scraping kubelet endpoints — UI should hide the gauge in that case.
|
|
3277
3261
|
export interface PrometheusPVCUsage {
|
|
3262
|
+
// Hub packages the frontend independently from per-cluster agent upgrades.
|
|
3263
|
+
status?: "available" | "no_series" | "invalid_data" | "query_failed";
|
|
3278
3264
|
namespace: string;
|
|
3279
3265
|
name: string;
|
|
3280
3266
|
used: number;
|
|
@@ -3364,6 +3350,7 @@ export interface RightsizingScanCoverage {
|
|
|
3364
3350
|
workloadsWithData: number;
|
|
3365
3351
|
batches: number;
|
|
3366
3352
|
completedBatches: number;
|
|
3353
|
+
attemptedBatches: number;
|
|
3367
3354
|
restrictedKinds?: string[];
|
|
3368
3355
|
unavailableKinds?: string[];
|
|
3369
3356
|
partiallyCachedKinds?: string[];
|
|
@@ -3371,6 +3358,11 @@ export interface RightsizingScanCoverage {
|
|
|
3371
3358
|
}
|
|
3372
3359
|
|
|
3373
3360
|
export interface RightsizingScanResponse {
|
|
3361
|
+
scanId: string;
|
|
3362
|
+
scanStatus: 'running' | 'finished' | 'cancelled' | 'timed_out';
|
|
3363
|
+
deadlineAt: string;
|
|
3364
|
+
expiresAt?: string;
|
|
3365
|
+
pollAfterSeconds?: number;
|
|
3374
3366
|
state: RightsizingScanState;
|
|
3375
3367
|
scannedAt: string;
|
|
3376
3368
|
window: string;
|
|
@@ -3629,7 +3621,6 @@ export function usePrometheusClusterMetrics(
|
|
|
3629
3621
|
});
|
|
3630
3622
|
}
|
|
3631
3623
|
|
|
3632
|
-
// Fetch PVC usage. hasData=false when no series — UI should hide the gauge.
|
|
3633
3624
|
export function usePrometheusPVCUsage(
|
|
3634
3625
|
namespace: string,
|
|
3635
3626
|
name: string,
|
|
@@ -3640,7 +3631,10 @@ export function usePrometheusPVCUsage(
|
|
|
3640
3631
|
queryFn: () => fetchJSON(`/prometheus/pvc/${namespace}/${name}`),
|
|
3641
3632
|
enabled: enabled && Boolean(namespace && name),
|
|
3642
3633
|
staleTime: 60000,
|
|
3643
|
-
refetchInterval:
|
|
3634
|
+
refetchInterval: (query) =>
|
|
3635
|
+
isForbiddenError(query.state.error) ? false : 120000,
|
|
3636
|
+
retry: (failureCount, error) =>
|
|
3637
|
+
!isForbiddenError(error) && failureCount < 1,
|
|
3644
3638
|
});
|
|
3645
3639
|
}
|
|
3646
3640
|
|
|
@@ -3663,55 +3657,83 @@ export function usePrometheusRightsizing(
|
|
|
3663
3657
|
|
|
3664
3658
|
const RIGHTSIZING_SCAN_CACHE_TIME = 5 * 60 * 1000;
|
|
3665
3659
|
|
|
3666
|
-
export function getRightsizingScanCacheConfig(
|
|
3667
|
-
|
|
3668
|
-
context = "",
|
|
3669
|
-
): {
|
|
3670
|
-
namespaceKey: string;
|
|
3671
|
-
queryKey: readonly ["prometheus-rightsizing-scan", string, string];
|
|
3672
|
-
queryFn: typeof skipToken;
|
|
3673
|
-
gcTime: number;
|
|
3674
|
-
} {
|
|
3675
|
-
const namespaceKey = [...namespaces].sort().join(",");
|
|
3660
|
+
export function getRightsizingScanCacheConfig(namespaces: string[], context = "", identity = "") {
|
|
3661
|
+
const namespaceKey = [...new Set(namespaces)].sort().join(",");
|
|
3676
3662
|
return {
|
|
3677
3663
|
namespaceKey,
|
|
3678
|
-
queryKey: ["prometheus-rightsizing-scan", context, namespaceKey] as const,
|
|
3679
|
-
queryFn: skipToken,
|
|
3664
|
+
queryKey: ["prometheus-rightsizing-scan", getApiBase(), identity, context, namespaceKey] as const,
|
|
3680
3665
|
gcTime: RIGHTSIZING_SCAN_CACHE_TIME,
|
|
3681
3666
|
};
|
|
3682
3667
|
}
|
|
3683
3668
|
|
|
3684
|
-
//
|
|
3685
|
-
// Prometheus history for many containers, so navigation alone must never run it.
|
|
3669
|
+
// Navigation retrieves a retained scan; only an explicit POST starts work.
|
|
3686
3670
|
export function useRightsizingScan(namespaces: string[], context = "") {
|
|
3687
3671
|
const queryClient = useQueryClient();
|
|
3688
|
-
const {
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
);
|
|
3692
|
-
const
|
|
3693
|
-
|
|
3672
|
+
const { data: auth } = useAuthMe();
|
|
3673
|
+
const identity = JSON.stringify([auth?.username, [...(auth?.groups ?? [])].sort()]);
|
|
3674
|
+
const { namespaceKey, ...cache } = getRightsizingScanCacheConfig(namespaces, context, identity);
|
|
3675
|
+
const scope = JSON.stringify(cache.queryKey);
|
|
3676
|
+
const currentScope = useRef(scope);
|
|
3677
|
+
currentScope.current = scope;
|
|
3678
|
+
const previous = useRef<{ scope: string; result: RightsizingScanResponse } | null>(null);
|
|
3679
|
+
const params = new URLSearchParams();
|
|
3680
|
+
if (namespaceKey) params.set('namespaces', namespaceKey);
|
|
3681
|
+
const path = `/prometheus/rightsizing/scan?${params}`;
|
|
3682
|
+
const snapshot = useQuery<RightsizingScanResponse | null>({
|
|
3683
|
+
...cache,
|
|
3684
|
+
enabled: Boolean(context && auth),
|
|
3685
|
+
queryFn: ({ signal }) => fetchJSON(path, { signal }),
|
|
3686
|
+
refetchOnMount: 'always',
|
|
3687
|
+
retry: (count, error) => !(error instanceof ApiError && [403, 404, 409].includes(error.status)) && count < 1,
|
|
3688
|
+
refetchInterval: (query) => !query.state.error && query.state.data?.scanStatus === 'running' ? 5000 : false,
|
|
3689
|
+
});
|
|
3694
3690
|
const mutation = useMutation({
|
|
3695
|
-
mutationFn: async (
|
|
3696
|
-
|
|
3697
|
-
if (
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
},
|
|
3705
|
-
);
|
|
3691
|
+
mutationFn: async (started: { scope: string; path: string; queryKey: typeof cache.queryKey; previousScanId?: string }) => {
|
|
3692
|
+
await queryClient.cancelQueries({ queryKey: started.queryKey });
|
|
3693
|
+
if (currentScope.current !== started.scope) throw new Error('Scan scope changed; run the scan again.');
|
|
3694
|
+
return fetchJSON<RightsizingScanResponse>(started.path, { method: 'POST' });
|
|
3695
|
+
},
|
|
3696
|
+
onSuccess: (result, started) => queryClient.setQueryData(started.queryKey, result),
|
|
3697
|
+
onError: (_error, started) => {
|
|
3698
|
+
// A lost POST response may still have started a scan. Read before retrying.
|
|
3699
|
+
void queryClient.invalidateQueries({ queryKey: started.queryKey });
|
|
3706
3700
|
},
|
|
3707
|
-
onSuccess: (result, startedScope) =>
|
|
3708
|
-
queryClient.setQueryData(startedScope.queryKey, result),
|
|
3709
3701
|
});
|
|
3702
|
+
const stop = useMutation({
|
|
3703
|
+
mutationFn: async (started: { id: string; scope: string; queryKey: typeof cache.queryKey }) => {
|
|
3704
|
+
await queryClient.cancelQueries({ queryKey: started.queryKey });
|
|
3705
|
+
if (currentScope.current !== started.scope) throw new Error('Scan scope changed.');
|
|
3706
|
+
return fetchJSON<RightsizingScanResponse>(`/prometheus/rightsizing/scan/${encodeURIComponent(started.id)}`, { method: 'DELETE' });
|
|
3707
|
+
},
|
|
3708
|
+
onSuccess: (result, started) => queryClient.setQueryData(started.queryKey, result),
|
|
3709
|
+
});
|
|
3710
|
+
const inaccessible = snapshot.error instanceof ApiError && [403, 404, 409].includes(snapshot.error.status);
|
|
3711
|
+
const current = inaccessible ? null : snapshot.data;
|
|
3712
|
+
useEffect(() => {
|
|
3713
|
+
if (inaccessible) {
|
|
3714
|
+
previous.current = null;
|
|
3715
|
+
queryClient.setQueryData(cache.queryKey, null);
|
|
3716
|
+
} else if (current && current.coverage.workloadsEvaluated > 0) {
|
|
3717
|
+
previous.current = { scope, result: current };
|
|
3718
|
+
}
|
|
3719
|
+
}, [current, inaccessible, scope, queryClient, cache.queryKey]);
|
|
3720
|
+
const showingPrevious = current?.scanStatus === 'running' && current.coverage.workloadsEvaluated === 0 && previous.current?.scope === scope;
|
|
3721
|
+
const startRecovered = current && current.scanId !== mutation.variables?.previousScanId;
|
|
3722
|
+
const stopRecovered = current && (current.scanId !== stop.variables?.id || current.scanStatus !== 'running');
|
|
3710
3723
|
return {
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3724
|
+
data: showingPrevious ? previous.current!.result : current,
|
|
3725
|
+
progress: current,
|
|
3726
|
+
showingPrevious,
|
|
3727
|
+
isStarting: mutation.isPending,
|
|
3728
|
+
isPending: mutation.isPending || current?.scanStatus === 'running',
|
|
3729
|
+
isLoading: snapshot.isLoading,
|
|
3730
|
+
statusError: snapshot.error,
|
|
3731
|
+
error: snapshot.error || (stop.variables?.scope === scope && !stopRecovered ? stop.error : null) || (mutation.variables?.scope === scope && !startRecovered ? mutation.error : null),
|
|
3732
|
+
reset: () => { mutation.reset(); stop.reset(); },
|
|
3733
|
+
mutateAsync: () => mutation.mutateAsync({ scope, path, queryKey: cache.queryKey, previousScanId: current?.scanId }),
|
|
3734
|
+
retryStatus: () => snapshot.refetch(),
|
|
3735
|
+
stop: () => { if (!mutation.isPending && current?.scanStatus === 'running') stop.mutate({ id: current.scanId, scope, queryKey: cache.queryKey }); },
|
|
3736
|
+
isStopping: stop.isPending,
|
|
3715
3737
|
};
|
|
3716
3738
|
}
|
|
3717
3739
|
|
|
@@ -6443,6 +6465,18 @@ export function useContexts() {
|
|
|
6443
6465
|
});
|
|
6444
6466
|
}
|
|
6445
6467
|
|
|
6468
|
+
// Where a remote Argo Application or Flux object (spec.kubeConfig) deploys:
|
|
6469
|
+
// the destination's host for display, and the kubeconfig contexts that reach it.
|
|
6470
|
+
export function useGitOpsDestination(kind: string, namespace: string, name: string, enabled: boolean) {
|
|
6471
|
+
return useQuery<{ server: string; contexts: string[] }>({
|
|
6472
|
+
queryKey: ["gitops-destination", kind, namespace, name],
|
|
6473
|
+
queryFn: () => fetchJSON(`/gitops/destination/${encodeURIComponent(kind)}/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`),
|
|
6474
|
+
enabled,
|
|
6475
|
+
staleTime: 60000,
|
|
6476
|
+
retry: false,
|
|
6477
|
+
});
|
|
6478
|
+
}
|
|
6479
|
+
|
|
6446
6480
|
// Session counts for context switch confirmation
|
|
6447
6481
|
export interface SessionCounts {
|
|
6448
6482
|
portForwards: number;
|
|
@@ -6737,6 +6771,8 @@ export function useImageFilesystem(
|
|
|
6737
6771
|
// Response from workload pods endpoint
|
|
6738
6772
|
export interface WorkloadPodsResponse {
|
|
6739
6773
|
pods: WorkloadPodInfo[];
|
|
6774
|
+
total: number;
|
|
6775
|
+
truncated: boolean;
|
|
6740
6776
|
}
|
|
6741
6777
|
|
|
6742
6778
|
// Response from workload logs endpoint (non-streaming)
|
|
@@ -6754,10 +6790,12 @@ export interface WorkloadLogsResponse {
|
|
|
6754
6790
|
}
|
|
6755
6791
|
|
|
6756
6792
|
export interface WorkloadRun {
|
|
6793
|
+
group: string;
|
|
6757
6794
|
kind: string;
|
|
6758
6795
|
namespace: string;
|
|
6759
6796
|
name: string;
|
|
6760
6797
|
phase: string;
|
|
6798
|
+
deleting?: boolean;
|
|
6761
6799
|
active: boolean;
|
|
6762
6800
|
startedAt?: string;
|
|
6763
6801
|
finishedAt?: string;
|
|
@@ -6771,6 +6809,7 @@ export interface WorkloadRun {
|
|
|
6771
6809
|
parallelism?: number;
|
|
6772
6810
|
progress?: string;
|
|
6773
6811
|
template?: string;
|
|
6812
|
+
jobset?: JobSetMember;
|
|
6774
6813
|
launcher?: {
|
|
6775
6814
|
kind: string;
|
|
6776
6815
|
namespace?: string;
|
|
@@ -6784,17 +6823,51 @@ export interface WorkloadRun {
|
|
|
6784
6823
|
podPending?: number;
|
|
6785
6824
|
}
|
|
6786
6825
|
|
|
6826
|
+
export interface JobSetMember {
|
|
6827
|
+
replicatedJob?: string;
|
|
6828
|
+
replicatedJobReplicas?: string;
|
|
6829
|
+
jobIndex?: string;
|
|
6830
|
+
globalReplicas?: string;
|
|
6831
|
+
globalIndex?: string;
|
|
6832
|
+
groupName?: string;
|
|
6833
|
+
groupReplicas?: string;
|
|
6834
|
+
groupIndex?: string;
|
|
6835
|
+
restartAttempt?: string;
|
|
6836
|
+
jobRestartAttempt?: string;
|
|
6837
|
+
}
|
|
6838
|
+
|
|
6787
6839
|
export interface WorkloadRunsResponse {
|
|
6840
|
+
collection: "runs" | "members";
|
|
6841
|
+
filteredTotal?: number;
|
|
6842
|
+
selected?: WorkloadRun;
|
|
6788
6843
|
runs: WorkloadRun[];
|
|
6844
|
+
total: number;
|
|
6845
|
+
truncated: boolean;
|
|
6789
6846
|
}
|
|
6790
6847
|
|
|
6791
6848
|
// Fetch pods for a workload
|
|
6792
|
-
export function useWorkloadPods(
|
|
6849
|
+
export function useWorkloadPods(
|
|
6850
|
+
kind: string,
|
|
6851
|
+
namespace: string,
|
|
6852
|
+
name: string,
|
|
6853
|
+
options?: { limit?: number; refetchInterval?: number | false; ownerUID?: string; nodeType?: string; workerGroup?: string },
|
|
6854
|
+
) {
|
|
6855
|
+
const limit = options?.limit;
|
|
6856
|
+
const params = new URLSearchParams();
|
|
6857
|
+
if (limit) params.set('limit', String(limit));
|
|
6858
|
+
for (const key of ['ownerUID', 'nodeType', 'workerGroup'] as const) {
|
|
6859
|
+
if (options?.[key]) params.set(key, options[key]);
|
|
6860
|
+
}
|
|
6861
|
+
const queryString = params.size ? `?${params}` : '';
|
|
6862
|
+
const ownerKey = options?.ownerUID ? [options.ownerUID, options.nodeType ?? '', options.workerGroup ?? ''] : [];
|
|
6793
6863
|
return useQuery<WorkloadPodsResponse>({
|
|
6794
|
-
queryKey: ["workload-pods", kind, namespace, name],
|
|
6795
|
-
queryFn: () =>
|
|
6864
|
+
queryKey: ["workload-pods", kind, namespace, name, limit ?? 0, ...ownerKey],
|
|
6865
|
+
queryFn: () =>
|
|
6866
|
+
fetchJSON(`/workloads/${kind}/${namespace}/${name}/pods${queryString}`),
|
|
6796
6867
|
enabled: Boolean(kind && namespace && name),
|
|
6797
6868
|
staleTime: 10000, // 10 seconds - pods can change
|
|
6869
|
+
refetchInterval: options?.refetchInterval ?? false,
|
|
6870
|
+
retry: options?.ownerUID ? (count, error) => !(error instanceof ApiError && error.status === 409) && count < 2 : undefined,
|
|
6798
6871
|
});
|
|
6799
6872
|
}
|
|
6800
6873
|
|
|
@@ -6803,16 +6876,22 @@ export function useWorkloadRuns(
|
|
|
6803
6876
|
namespace: string,
|
|
6804
6877
|
name: string,
|
|
6805
6878
|
enabled = true,
|
|
6806
|
-
options?: { refetchActive?: boolean; clusterScoped?: boolean },
|
|
6879
|
+
options?: { refetchActive?: boolean; clusterScoped?: boolean; role?: string; search?: string; state?: string; selected?: string },
|
|
6807
6880
|
) {
|
|
6808
6881
|
const clusterScoped = options?.clusterScoped ?? false;
|
|
6809
6882
|
const ns = clusterScoped ? "_" : namespace;
|
|
6810
6883
|
const params = new URLSearchParams();
|
|
6811
6884
|
if (clusterScoped) params.set("clusterScoped", "true");
|
|
6885
|
+
for (const key of ["role", "search", "state", "selected"] as const) { if (options?.[key]) params.set(key, options[key]); }
|
|
6812
6886
|
const queryString = params.toString();
|
|
6813
6887
|
|
|
6814
6888
|
return useQuery<WorkloadRunsResponse>({
|
|
6815
|
-
queryKey: ["workload-runs", kind, namespace, name, clusterScoped],
|
|
6889
|
+
queryKey: ["workload-runs", kind, namespace, name, clusterScoped, options?.role ?? '', options?.search ?? '', options?.state ?? '', options?.selected ?? ''],
|
|
6890
|
+
placeholderData: (previous, query) => {
|
|
6891
|
+
if (previous?.collection !== 'members') return undefined
|
|
6892
|
+
const identity = ["workload-runs", kind, namespace, name, clusterScoped, options?.role ?? '', options?.search ?? '', options?.state ?? '']
|
|
6893
|
+
return identity.every((value, index) => query?.queryKey[index] === value) ? previous : undefined
|
|
6894
|
+
},
|
|
6816
6895
|
queryFn: () =>
|
|
6817
6896
|
fetchJSON(
|
|
6818
6897
|
`/workloads/${kind}/${ns}/${name}/runs${queryString ? `?${queryString}` : ""}`,
|
|
@@ -6821,11 +6900,26 @@ export function useWorkloadRuns(
|
|
|
6821
6900
|
staleTime: 10000,
|
|
6822
6901
|
refetchInterval: options?.refetchActive
|
|
6823
6902
|
? (query) =>
|
|
6824
|
-
query.state.data?.runs?.some((run) => run.active) ? 5000 : 30000
|
|
6903
|
+
query.state.data?.selected?.active || query.state.data?.runs?.some((run) => run.active) ? 5000 : 30000
|
|
6825
6904
|
: false,
|
|
6826
6905
|
});
|
|
6827
6906
|
}
|
|
6828
6907
|
|
|
6908
|
+
export function useKueueAdmission(namespace: string, name: string, uid: string | undefined) {
|
|
6909
|
+
return useQuery<KueueAdmissionResponse>({
|
|
6910
|
+
queryKey: ['kueue-admission', 'jobset.x-k8s.io', namespace, name, uid],
|
|
6911
|
+
queryFn: () => fetchJSON(`/kueue/admission/jobsets/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}?group=jobset.x-k8s.io`),
|
|
6912
|
+
enabled: Boolean(namespace && name && uid),
|
|
6913
|
+
staleTime: 5000,
|
|
6914
|
+
refetchInterval: (query) => {
|
|
6915
|
+
if (query.state.error instanceof ApiError && query.state.error.status < 500) return false
|
|
6916
|
+
if (query.state.data?.uid === uid && query.state.data?.installed === false) return false
|
|
6917
|
+
return 5000
|
|
6918
|
+
},
|
|
6919
|
+
retry: (count, error) => !(error instanceof ApiError && error.status < 500) && count < 2,
|
|
6920
|
+
})
|
|
6921
|
+
}
|
|
6922
|
+
|
|
6829
6923
|
// Fetch logs for a workload (non-streaming)
|
|
6830
6924
|
export function useWorkloadLogs(
|
|
6831
6925
|
kind: string,
|
|
@@ -7119,3 +7213,64 @@ export function useDiagnostics(enabled: boolean) {
|
|
|
7119
7213
|
gcTime: 0,
|
|
7120
7214
|
});
|
|
7121
7215
|
}
|
|
7216
|
+
|
|
7217
|
+
export interface JobSetUsage {
|
|
7218
|
+
runningPods: number
|
|
7219
|
+
reportingPods: number
|
|
7220
|
+
stalePods: number
|
|
7221
|
+
cpu: number | null
|
|
7222
|
+
memory: number | null
|
|
7223
|
+
cpuRequest: number
|
|
7224
|
+
memoryRequest: number
|
|
7225
|
+
observedAt?: string
|
|
7226
|
+
extendedRequests?: Record<string, string>
|
|
7227
|
+
}
|
|
7228
|
+
export interface JobSetResources {
|
|
7229
|
+
uid: string
|
|
7230
|
+
total: JobSetUsage
|
|
7231
|
+
members: Record<string, JobSetUsage>
|
|
7232
|
+
source: string
|
|
7233
|
+
unavailable?: string
|
|
7234
|
+
}
|
|
7235
|
+
export function useJobSetResources(namespace: string, name: string, uid: string | undefined, options: { role: string; search: string; state: string }) {
|
|
7236
|
+
const params = new URLSearchParams()
|
|
7237
|
+
for (const key of ["role", "search", "state"] as const) if (options[key]) params.set(key, options[key])
|
|
7238
|
+
return useQuery<JobSetResources>({
|
|
7239
|
+
queryKey: ['jobset-resources', namespace, name, uid, params.toString()],
|
|
7240
|
+
queryFn: ({ signal }) => fetchJSON(`/jobsets/${namespace}/${name}/resources?${params}`, { signal }),
|
|
7241
|
+
enabled: Boolean(namespace && name && uid),
|
|
7242
|
+
staleTime: 25000,
|
|
7243
|
+
refetchInterval: 30000,
|
|
7244
|
+
retry: false,
|
|
7245
|
+
})
|
|
7246
|
+
}
|
|
7247
|
+
|
|
7248
|
+
|
|
7249
|
+
export interface KueueProvisioningResponse {
|
|
7250
|
+
uid: string
|
|
7251
|
+
installed: boolean
|
|
7252
|
+
total: number
|
|
7253
|
+
truncated: boolean
|
|
7254
|
+
requests: Array<{
|
|
7255
|
+
apiVersion: string
|
|
7256
|
+
kind: string
|
|
7257
|
+
metadata: { name: string; namespace: string; uid: string; generation?: number; creationTimestamp?: string; deletionTimestamp?: string }
|
|
7258
|
+
spec: { provisioningClassName: string }
|
|
7259
|
+
status?: { conditions: Array<{ type: string; status: string; reason?: string; message?: string; observedGeneration?: number }> }
|
|
7260
|
+
}>
|
|
7261
|
+
}
|
|
7262
|
+
|
|
7263
|
+
export function useKueueProvisioning(namespace: string, name: string, uid: string | undefined, enabled: boolean) {
|
|
7264
|
+
return useQuery<KueueProvisioningResponse>({
|
|
7265
|
+
queryKey: ['kueue-provisioning', namespace, name, uid],
|
|
7266
|
+
queryFn: () => fetchJSON(`/kueue/provisioning/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`),
|
|
7267
|
+
enabled: enabled && Boolean(namespace && name && uid),
|
|
7268
|
+
staleTime: 5000,
|
|
7269
|
+
refetchInterval: query => {
|
|
7270
|
+
if (query.state.error instanceof ApiError && query.state.error.status < 500) return false
|
|
7271
|
+
if (query.state.data?.uid === uid && query.state.data?.installed === false) return false
|
|
7272
|
+
return 10000
|
|
7273
|
+
},
|
|
7274
|
+
retry: (count, error) => !(error instanceof ApiError && error.status < 500) && count < 2,
|
|
7275
|
+
})
|
|
7276
|
+
}
|