@skyhook-io/radar-app 1.14.6 → 1.16.0
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 -0
- package/package.json +6 -5
- package/src/App.tsx +65 -17
- package/src/RadarApp.tsx +51 -21
- package/src/api/apiResources.test.ts +19 -1
- package/src/api/apiResources.ts +18 -3
- package/src/api/client.admission.test.ts +34 -0
- package/src/api/client.capacity.test.ts +15 -0
- package/src/api/client.trace.test.ts +9 -1
- package/src/api/client.ts +269 -204
- package/src/api/diagnose.test.ts +102 -1
- package/src/api/diagnose.ts +42 -11
- package/src/api/drain-plan.test.ts +0 -10
- package/src/api/httpErrors.test.ts +95 -0
- package/src/api/httpErrors.ts +101 -0
- package/src/api/policy.ts +33 -5
- package/src/api/radarFeatures.render.test.tsx +190 -0
- package/src/api/radarFeatures.test.ts +133 -0
- package/src/api/radarFeatures.ts +147 -0
- package/src/api/timelineSource.test.ts +2 -10
- package/src/api/timelineSource.ts +3 -2
- package/src/api/traffic.ts +4 -3
- package/src/api/usage-data.fixtures.ts +33 -0
- package/src/api/usage-data.ts +165 -0
- package/src/components/CloudFunnelButton.test.tsx +2 -2
- package/src/components/CloudFunnelButton.tsx +99 -55
- package/src/components/NamespaceSwitcher.test.tsx +47 -0
- package/src/components/NamespaceSwitcher.tsx +17 -1
- package/src/components/SelfManagedStart.tsx +95 -0
- package/src/components/applications/ApplicationsView.tsx +20 -6
- package/src/components/audit/UpgradeReadinessView.test.ts +14 -6
- package/src/components/audit/UpgradeReadinessView.tsx +9 -13
- package/src/components/capacity/CapacityView.test.tsx +13 -4
- package/src/components/capacity/shared.tsx +14 -13
- package/src/components/cloudConnectHandoff.test.ts +1 -1
- package/src/components/cloudConnectHandoff.ts +1 -1
- package/src/components/cost/ApplicationCostTab.tsx +9 -1
- package/src/components/cost/CostConnectionAction.tsx +30 -0
- package/src/components/cost/CostTrendChart.tsx +2 -0
- package/src/components/cost/CostView.tsx +4 -24
- package/src/components/cost/WorkloadCostTab.tsx +7 -1
- package/src/components/cost/source.test.ts +30 -3
- package/src/components/cost/source.ts +37 -2
- package/src/components/curl/ServiceCurlButton.tsx +7 -3
- package/src/components/diagnose/ActivityTurn.tsx +3 -2
- package/src/components/diagnose/ApplyDialog.tsx +4 -2
- package/src/components/dialog-names.test.ts +69 -0
- package/src/components/dock/NodeTerminalTab.tsx +2 -1
- package/src/components/dock/TerminalTab.tsx +2 -1
- package/src/components/dock/TrafficFlowListTab.tsx +2 -2
- package/src/components/execution/BatchExecutionView.tsx +1 -1
- package/src/components/execution/JobSetAdmission.test.tsx +160 -37
- package/src/components/execution/JobSetAdmission.tsx +16 -7
- package/src/components/gitops/GitOpsView.tsx +12 -0
- package/src/components/helm/TrackChartSourceDialog.tsx +5 -3
- package/src/components/home/HomeView.tsx +7 -3
- package/src/components/home/MCPSetupDialog.tsx +37 -37
- package/src/components/home/RadarVersionLine.test.tsx +14 -0
- package/src/components/home/RadarVersionLine.tsx +28 -3
- package/src/components/nav/PrimaryNavRail.test.tsx +10 -1
- package/src/components/nav/PrimaryNavRail.tsx +25 -3
- package/src/components/resource/PVCUsageBar.render.test.tsx +16 -0
- package/src/components/resource/PVCUsageBar.tsx +4 -1
- package/src/components/resource/PrometheusChartsGrid.render.test.tsx +1 -0
- package/src/components/resource/PrometheusChartsGrid.tsx +4 -0
- package/src/components/resources/ImageFilesystemModal.tsx +2 -1
- package/src/components/resources/PodFilePreview.tsx +4 -1
- package/src/components/resources/PodFilesystemModal.tsx +3 -2
- package/src/components/resources/ResourcesView.tsx +2 -1
- package/src/components/resources/renderers/JobAdmissionRenderers.test.tsx +18 -0
- package/src/components/resources/renderers/JobAdmissionRenderers.tsx +18 -0
- package/src/components/resources/renderers/RayJobRenderer.test.tsx +40 -0
- package/src/components/resources/renderers/RayJobRenderer.tsx +45 -0
- package/src/components/rightsizing/RightsizingScanView.tsx +4 -1
- package/src/components/settings/ConnectionHeadersEditor.tsx +109 -0
- package/src/components/settings/FormSaveActions.tsx +76 -0
- package/src/components/settings/IntegrationConnectionForms.tsx +391 -0
- package/src/components/settings/LocalConfigurationDetails.tsx +389 -0
- package/src/components/settings/LocalConnectionSettings.test.tsx +189 -0
- package/src/components/settings/LocalConnectionSettings.tsx +1015 -0
- package/src/components/settings/LocalIntegrationStatus.tsx +50 -0
- package/src/components/settings/PrivacySection.test.tsx +84 -0
- package/src/components/settings/PrivacySection.tsx +140 -0
- package/src/components/settings/PrometheusConfigField.tsx +30 -0
- package/src/components/settings/PrometheusConnectionForm.tsx +295 -0
- package/src/components/settings/SettingsDialog.tsx +393 -571
- package/src/components/settings/controls.tsx +55 -0
- package/src/components/settings/currency-options.test.ts +1 -1
- package/src/components/settings/currency-options.ts +1 -1
- package/src/components/settings/settings-state.test.ts +17 -10
- package/src/components/settings/settings-state.ts +21 -12
- package/src/components/timeline/TimelineList.test.ts +56 -0
- package/src/components/timeline/TimelineList.tsx +7 -1
- package/src/components/traffic/TrafficFlowList.tsx +5 -21
- package/src/components/traffic/TrafficFlowListContext.tsx +6 -4
- package/src/components/traffic/TrafficGraph.tsx +79 -48
- package/src/components/traffic/TrafficView.tsx +61 -37
- package/src/components/traffic/TrafficWizard.tsx +4 -2
- package/src/components/traffic/trafficFilters.test.ts +154 -1
- package/src/components/traffic/trafficFilters.ts +141 -0
- package/src/components/ui/ErrorBoundary.tsx +5 -0
- package/src/components/ui/Omnibar.tsx +17 -1
- package/src/components/ui/UpdateNotification.tsx +70 -21
- package/src/components/ui/command-items.ts +14 -0
- package/src/components/usage-data/UsageDataAsk.test.tsx +41 -0
- package/src/components/usage-data/UsageDataAsk.tsx +106 -0
- package/src/components/usage-data/UsageDataPrompt.test.ts +53 -0
- package/src/components/usage-data/UsageDataPrompt.tsx +148 -0
- package/src/components/whats-new/WhatsNew.test.ts +110 -0
- package/src/components/whats-new/WhatsNew.tsx +423 -0
- package/src/components/whats-new/WhatsNewDialog.test.tsx +229 -0
- package/src/components/whats-new/check-whats-new.test.ts +65 -0
- package/src/components/whats-new/releaseNotes.ts +121 -0
- package/src/components/workload/WorkloadView.tsx +47 -14
- package/src/components/workload/historyPaging.test.ts +72 -0
- package/src/components/workload/historyPaging.ts +147 -0
- package/src/components/workload/useHistoryPaging.test.tsx +81 -0
- package/src/context/ConnectionContext.tsx +2 -1
- package/src/context/RadarUpgrade.tsx +30 -0
- package/src/context/RadarUpgradeHost.tsx +23 -0
- package/src/hooks/usePreviousIntegrationSettings.render.test.tsx +62 -0
- package/src/hooks/usePreviousIntegrationSettings.test.ts +62 -0
- package/src/hooks/usePreviousIntegrationSettings.ts +89 -0
- package/src/index.ts +1 -0
- package/src/k8s-ui-exports.test.ts +73 -0
- package/src/utils/navigation.test.ts +17 -1
- package/src/utils/navigation.ts +16 -0
- package/src/utils/version.ts +11 -0
package/src/api/client.ts
CHANGED
|
@@ -59,6 +59,15 @@ import type {
|
|
|
59
59
|
} from '../types'
|
|
60
60
|
import type { GitOpsOperationResponse } from '../types/gitops'
|
|
61
61
|
import { apiUrl, getApiBase, getAuthHeaders, getCredentialsMode, getBasename, routePath, stripBasename } from './config'
|
|
62
|
+
import { httpStatusMessage, readErrorBody, readErrorResponse } from './httpErrors'
|
|
63
|
+
import {
|
|
64
|
+
RadarFeatureUnsupportedError,
|
|
65
|
+
guardRadarFeature,
|
|
66
|
+
radarFeatureSupport,
|
|
67
|
+
shouldRetryRadarQuery,
|
|
68
|
+
type RadarFeature,
|
|
69
|
+
} from './radarFeatures'
|
|
70
|
+
import { useRadarUpgradeHost } from '../context/RadarUpgradeHost'
|
|
62
71
|
import { apiVersionToGroup } from '../utils/navigation'
|
|
63
72
|
import type { DeploymentMode } from '../types'
|
|
64
73
|
|
|
@@ -195,11 +204,19 @@ export function apiFetch(
|
|
|
195
204
|
export class ApiError extends Error {
|
|
196
205
|
status: number;
|
|
197
206
|
data?: Record<string, unknown>;
|
|
198
|
-
|
|
207
|
+
/** The router has no such route: chi's own 404, not a handler's. See isUnknownRouteResponse. */
|
|
208
|
+
unknownRoute: boolean;
|
|
209
|
+
constructor(
|
|
210
|
+
message: string,
|
|
211
|
+
status: number,
|
|
212
|
+
data?: Record<string, unknown>,
|
|
213
|
+
options?: { unknownRoute?: boolean },
|
|
214
|
+
) {
|
|
199
215
|
super(message);
|
|
200
216
|
this.name = "ApiError";
|
|
201
217
|
this.status = status;
|
|
202
218
|
this.data = data;
|
|
219
|
+
this.unknownRoute = options?.unknownRoute ?? false;
|
|
203
220
|
}
|
|
204
221
|
}
|
|
205
222
|
|
|
@@ -222,6 +239,7 @@ export function shouldRetryCapacityQuery(
|
|
|
222
239
|
failureCount: number,
|
|
223
240
|
error: unknown,
|
|
224
241
|
): boolean {
|
|
242
|
+
if (error instanceof RadarFeatureUnsupportedError) return false;
|
|
225
243
|
if (
|
|
226
244
|
error instanceof ApiError &&
|
|
227
245
|
(error.status === 400 || error.status === 403 || error.status === 404)
|
|
@@ -336,13 +354,12 @@ export async function fetchJSON<T>(
|
|
|
336
354
|
const requestInit = init instanceof AbortSignal ? { signal: init } : init;
|
|
337
355
|
const response = await apiFetch(`${getApiBase()}${path}`, requestInit);
|
|
338
356
|
if (!response.ok) {
|
|
339
|
-
const
|
|
340
|
-
.json()
|
|
341
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
357
|
+
const { body, unknownRoute } = await readErrorResponse(response);
|
|
342
358
|
throw new ApiError(
|
|
343
|
-
|
|
359
|
+
body.error || httpStatusMessage(response.status, response.statusText),
|
|
344
360
|
response.status,
|
|
345
|
-
|
|
361
|
+
body,
|
|
362
|
+
{ unknownRoute },
|
|
346
363
|
);
|
|
347
364
|
}
|
|
348
365
|
return response.json();
|
|
@@ -669,16 +686,18 @@ export function useUpgradeReadiness(target?: string) {
|
|
|
669
686
|
// refetches read that memo; only the explicit manual refresh passes
|
|
670
687
|
// refresh=true to force a fresh live scan (e.g. after fixing a finding).
|
|
671
688
|
const forceRefresh = useRef(false)
|
|
689
|
+
const { guard, gatedKey } = useRadarFeature('upgradeReadiness')
|
|
672
690
|
const query = useQuery<UpgradeReadinessResponse>({
|
|
673
|
-
queryKey: ['upgrade-readiness', target ?? 'next'],
|
|
691
|
+
queryKey: ['upgrade-readiness', target ?? 'next', ...gatedKey],
|
|
674
692
|
queryFn: ({ signal }) => {
|
|
675
693
|
const params = new URLSearchParams()
|
|
676
694
|
if (target) params.set('target', target)
|
|
677
695
|
if (forceRefresh.current) params.set('refresh', 'true')
|
|
678
696
|
forceRefresh.current = false
|
|
679
697
|
const qs = params.toString()
|
|
680
|
-
return fetchJSON(`/upgrade-readiness${qs ? `?${qs}` : ''}`, signal)
|
|
698
|
+
return guard(() => fetchJSON<UpgradeReadinessResponse>(`/upgrade-readiness${qs ? `?${qs}` : ''}`, signal))
|
|
681
699
|
},
|
|
700
|
+
retry: shouldRetryRadarQuery,
|
|
682
701
|
staleTime: 30000,
|
|
683
702
|
placeholderData: (previous) => previous,
|
|
684
703
|
})
|
|
@@ -756,9 +775,13 @@ export function useResourceIssues(
|
|
|
756
775
|
if (group) params.set("group", group);
|
|
757
776
|
const path = `/issues/resource/${encodeURIComponent(kind)}/${pathNs}/${encodeURIComponent(name)}`;
|
|
758
777
|
const qs = params.toString();
|
|
778
|
+
// An older Radar without the endpoint settles as unsupported, so the
|
|
779
|
+
// renderers fall back to their own status-derived problem banners.
|
|
780
|
+
const { guard, gatedKey } = useRadarFeature("resourceIssues");
|
|
759
781
|
return useQuery<Issue[]>({
|
|
760
|
-
queryKey: ["issues", "resource", kind, group ?? "", namespace, name],
|
|
761
|
-
queryFn: () => fetchJSON(`${path}${qs ? `?${qs}` : ""}`),
|
|
782
|
+
queryKey: ["issues", "resource", kind, group ?? "", namespace, name, ...gatedKey],
|
|
783
|
+
queryFn: () => guard(() => fetchJSON<Issue[]>(`${path}${qs ? `?${qs}` : ""}`)),
|
|
784
|
+
retry: shouldRetryRadarQuery,
|
|
762
785
|
// No refetchInterval: a drawer doesn't need to poll; staleTime keeps it fresh
|
|
763
786
|
// on reopen without re-running an uncapped Compose every 30s.
|
|
764
787
|
staleTime: 30000,
|
|
@@ -832,13 +855,14 @@ export async function runInClusterMerged(kind: string, namespace: string, name:
|
|
|
832
855
|
headers: { 'Content-Type': 'application/json' },
|
|
833
856
|
body: JSON.stringify({ path: path ?? '/' }),
|
|
834
857
|
})
|
|
835
|
-
// A gateway/proxy failure (chi's 60s timeout, a Hub ingress 502)
|
|
836
|
-
// HTML
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
throw new Error(body?.error || `In-cluster test failed (${response.status})`)
|
|
858
|
+
// A gateway/proxy failure (chi's 60s timeout, a Hub ingress 502) answers in
|
|
859
|
+
// HTML or plain text, so a failure reads its body tolerantly.
|
|
860
|
+
if (!response.ok) {
|
|
861
|
+
const error = await readErrorBody(response)
|
|
862
|
+
throw new Error(error.error || `In-cluster test failed (${response.status})`)
|
|
841
863
|
}
|
|
864
|
+
const body = await response.json().catch(() => undefined)
|
|
865
|
+
if (body?.error) throw new Error(body.error)
|
|
842
866
|
if (body === undefined) {
|
|
843
867
|
throw new Error('In-cluster test returned an unreadable response')
|
|
844
868
|
}
|
|
@@ -869,9 +893,7 @@ export function useUpdateAuditSettings() {
|
|
|
869
893
|
body: JSON.stringify(settings),
|
|
870
894
|
});
|
|
871
895
|
if (!resp.ok) {
|
|
872
|
-
const body = await resp
|
|
873
|
-
.json()
|
|
874
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
896
|
+
const body = await readErrorBody(resp);
|
|
875
897
|
throw new Error(body.error || `HTTP ${resp.status}`);
|
|
876
898
|
}
|
|
877
899
|
return resp.json();
|
|
@@ -957,6 +979,8 @@ export type CostUnavailableReason =
|
|
|
957
979
|
| "authentication_error"
|
|
958
980
|
| "configuration_mismatch"
|
|
959
981
|
| "deployment_configuration_error"
|
|
982
|
+
| "metrics_settings_error"
|
|
983
|
+
| "cost_settings_error"
|
|
960
984
|
| "history_unsupported"
|
|
961
985
|
| "insufficient_history";
|
|
962
986
|
|
|
@@ -1009,10 +1033,11 @@ function costRefetchInterval(
|
|
|
1009
1033
|
};
|
|
1010
1034
|
}
|
|
1011
1035
|
|
|
1012
|
-
export function useOpenCostSummary() {
|
|
1036
|
+
export function useOpenCostSummary(enabled = true) {
|
|
1013
1037
|
const clusterInfo = useClusterInfo();
|
|
1014
1038
|
return useQuery<OpenCostSummary>({
|
|
1015
1039
|
queryKey: ["opencost-summary"],
|
|
1040
|
+
enabled,
|
|
1016
1041
|
queryFn: () => fetchJSON("/opencost/summary"),
|
|
1017
1042
|
refetchInterval: costRefetchInterval(
|
|
1018
1043
|
COST_REFRESH_INTERVAL_MS,
|
|
@@ -1383,11 +1408,12 @@ export function capacityRefetchInterval(
|
|
|
1383
1408
|
enabled: boolean,
|
|
1384
1409
|
cursor?: string,
|
|
1385
1410
|
) {
|
|
1386
|
-
return (query: { state: { data?: unknown } }): number | false => {
|
|
1411
|
+
return (query: { state: { data?: unknown; error?: unknown } }): number | false => {
|
|
1387
1412
|
// A held cursor means the user deliberately paged forward — background
|
|
1388
1413
|
// polling there races membership changes and yanks them back to page 1.
|
|
1389
1414
|
if (cursor) return false;
|
|
1390
1415
|
if (!enabled) return false;
|
|
1416
|
+
if (query.state.error instanceof RadarFeatureUnsupportedError) return false;
|
|
1391
1417
|
// Syncing is a transient bootstrap state (context switch, informer
|
|
1392
1418
|
// startup). At the normal cadence the syncing screen can sit for a full
|
|
1393
1419
|
// interval looking stuck — poll fast until the server settles.
|
|
@@ -1399,12 +1425,15 @@ export function capacityRefetchInterval(
|
|
|
1399
1425
|
|
|
1400
1426
|
export function useCapacityOverview(options?: CapacityQueryOptions) {
|
|
1401
1427
|
const enabled = options?.enabled ?? true;
|
|
1428
|
+
const { guard, gatedKey } = useRadarFeature("capacity");
|
|
1402
1429
|
return useQuery<CapacityOverviewResponse>({
|
|
1403
|
-
queryKey: ["capacity", "overview"],
|
|
1430
|
+
queryKey: ["capacity", "overview", ...gatedKey],
|
|
1404
1431
|
queryFn: ({ signal }) =>
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1432
|
+
guard(() =>
|
|
1433
|
+
fetchJSON<CapacityOverviewResponse>(
|
|
1434
|
+
`/capacity${capacityPageQuery(options)}`,
|
|
1435
|
+
signal,
|
|
1436
|
+
),
|
|
1408
1437
|
),
|
|
1409
1438
|
enabled,
|
|
1410
1439
|
staleTime: 15_000,
|
|
@@ -1418,12 +1447,15 @@ export function useCapacityPools(options?: CapacityPageQueryOptions) {
|
|
|
1418
1447
|
const limit = options?.limit;
|
|
1419
1448
|
const cursor = options?.cursor;
|
|
1420
1449
|
const queryKey = ["capacity", "pools", limit, cursor];
|
|
1450
|
+
const { guard, gatedKey } = useRadarFeature("capacity");
|
|
1421
1451
|
return useQuery<CapacityPoolListResponse>({
|
|
1422
|
-
queryKey,
|
|
1452
|
+
queryKey: [...queryKey, ...gatedKey],
|
|
1423
1453
|
queryFn: ({ signal }) =>
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1454
|
+
guard(() =>
|
|
1455
|
+
fetchJSON<CapacityPoolListResponse>(
|
|
1456
|
+
`/capacity/pools${capacityPageQuery(options)}`,
|
|
1457
|
+
signal,
|
|
1458
|
+
),
|
|
1427
1459
|
),
|
|
1428
1460
|
enabled,
|
|
1429
1461
|
staleTime: 15_000,
|
|
@@ -1441,12 +1473,15 @@ export function useCapacityPoolDetail(
|
|
|
1441
1473
|
options?: CapacityQueryOptions,
|
|
1442
1474
|
) {
|
|
1443
1475
|
const enabled = Boolean(name) && (options?.enabled ?? true);
|
|
1476
|
+
const { guard, gatedKey } = useRadarFeature("capacity");
|
|
1444
1477
|
return useQuery<CapacityPoolDetailResponse>({
|
|
1445
|
-
queryKey: ["capacity", "pool", name],
|
|
1478
|
+
queryKey: ["capacity", "pool", name, ...gatedKey],
|
|
1446
1479
|
queryFn: ({ signal }) =>
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1480
|
+
guard(() =>
|
|
1481
|
+
fetchJSON<CapacityPoolDetailResponse>(
|
|
1482
|
+
`/capacity/pools/${encodeURIComponent(name ?? "")}${capacityPageQuery(options)}`,
|
|
1483
|
+
signal,
|
|
1484
|
+
),
|
|
1450
1485
|
),
|
|
1451
1486
|
enabled,
|
|
1452
1487
|
staleTime: 15_000,
|
|
@@ -1470,12 +1505,15 @@ export function useCapacityPoolMembers(
|
|
|
1470
1505
|
const pageQuery = capacityPageQuery(options);
|
|
1471
1506
|
const separator = pageQuery ? "&" : "?";
|
|
1472
1507
|
const queryKey = ["capacity", "pool", name, "members", type, limit, cursor];
|
|
1508
|
+
const { guard, gatedKey } = useRadarFeature("capacity");
|
|
1473
1509
|
return useQuery<CapacityMemberListResponse>({
|
|
1474
|
-
queryKey,
|
|
1510
|
+
queryKey: [...queryKey, ...gatedKey],
|
|
1475
1511
|
queryFn: ({ signal }) =>
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1512
|
+
guard(() =>
|
|
1513
|
+
fetchJSON<CapacityMemberListResponse>(
|
|
1514
|
+
`/capacity/pools/${encodeURIComponent(name ?? "")}/members${pageQuery}${separator}type=${encodeURIComponent(type)}`,
|
|
1515
|
+
signal,
|
|
1516
|
+
),
|
|
1479
1517
|
),
|
|
1480
1518
|
enabled,
|
|
1481
1519
|
staleTime: 15_000,
|
|
@@ -1525,12 +1563,15 @@ export function useCapacityDemand(options?: CapacityDemandQueryOptions) {
|
|
|
1525
1563
|
options?.owner,
|
|
1526
1564
|
options?.pod,
|
|
1527
1565
|
];
|
|
1566
|
+
const { guard, gatedKey } = useRadarFeature("capacity");
|
|
1528
1567
|
return useQuery<CapacityDemandResponse>({
|
|
1529
|
-
queryKey,
|
|
1568
|
+
queryKey: [...queryKey, ...gatedKey],
|
|
1530
1569
|
queryFn: ({ signal }) =>
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1570
|
+
guard(() =>
|
|
1571
|
+
fetchJSON<CapacityDemandResponse>(
|
|
1572
|
+
`/capacity/demand${query ? `?${query}` : ""}`,
|
|
1573
|
+
signal,
|
|
1574
|
+
),
|
|
1534
1575
|
),
|
|
1535
1576
|
enabled,
|
|
1536
1577
|
staleTime: 15_000,
|
|
@@ -1570,12 +1611,15 @@ export function useCapacityActivity(options?: CapacityActivityQueryOptions) {
|
|
|
1570
1611
|
options?.node,
|
|
1571
1612
|
options?.type,
|
|
1572
1613
|
];
|
|
1614
|
+
const { guard, gatedKey } = useRadarFeature("capacity");
|
|
1573
1615
|
return useQuery<CapacityActivityResponse>({
|
|
1574
|
-
queryKey,
|
|
1616
|
+
queryKey: [...queryKey, ...gatedKey],
|
|
1575
1617
|
queryFn: ({ signal }) =>
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1618
|
+
guard(() =>
|
|
1619
|
+
fetchJSON<CapacityActivityResponse>(
|
|
1620
|
+
`/capacity/activity${query ? `?${query}` : ""}`,
|
|
1621
|
+
signal,
|
|
1622
|
+
),
|
|
1579
1623
|
),
|
|
1580
1624
|
enabled,
|
|
1581
1625
|
staleTime: 15_000,
|
|
@@ -1604,6 +1648,37 @@ export function useClusterInfo() {
|
|
|
1604
1648
|
export type InstallMethod =
|
|
1605
1649
|
"homebrew" | "krew" | "scoop" | "direct" | "desktop";
|
|
1606
1650
|
|
|
1651
|
+
export interface WhatsNewState {
|
|
1652
|
+
currentVersion: string;
|
|
1653
|
+
// "server": seenVersion / priorInstall are authoritative (local installs).
|
|
1654
|
+
// "browser": the client keeps its own record (in-cluster).
|
|
1655
|
+
storage: "server" | "browser";
|
|
1656
|
+
seenVersion?: string;
|
|
1657
|
+
priorInstall?: boolean;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
export function useWhatsNewState(enabled: boolean) {
|
|
1661
|
+
return useQuery<WhatsNewState>({
|
|
1662
|
+
queryKey: ["whats-new", getApiBase()],
|
|
1663
|
+
queryFn: () => fetchJSON("/whats-new"),
|
|
1664
|
+
enabled,
|
|
1665
|
+
// Refetched on focus (the app default is off), so another tab's
|
|
1666
|
+
// acknowledgment clears this tab's dot.
|
|
1667
|
+
staleTime: 0,
|
|
1668
|
+
refetchOnWindowFocus: true,
|
|
1669
|
+
retry: false,
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
export async function markWhatsNewSeen(version: string): Promise<void> {
|
|
1674
|
+
const response = await apiFetch(`${getApiBase()}/whats-new/seen`, {
|
|
1675
|
+
method: "POST",
|
|
1676
|
+
headers: { "Content-Type": "application/json" },
|
|
1677
|
+
body: JSON.stringify({ version }),
|
|
1678
|
+
});
|
|
1679
|
+
if (!response.ok) throw new ApiError(`HTTP ${response.status}`, response.status);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1607
1682
|
export interface VersionInfo {
|
|
1608
1683
|
currentVersion: string;
|
|
1609
1684
|
latestVersion?: string;
|
|
@@ -1658,6 +1733,15 @@ export async function triggerDailyUpdateCheck(
|
|
|
1658
1733
|
})
|
|
1659
1734
|
}
|
|
1660
1735
|
|
|
1736
|
+
function versionCheckQueryOptions(apiBase: string) {
|
|
1737
|
+
return {
|
|
1738
|
+
queryKey: ["version-check", apiBase],
|
|
1739
|
+
queryFn: () => fetchJSON<VersionInfo>('/version-check'),
|
|
1740
|
+
staleTime: 60 * 60 * 1000, // 1 hour
|
|
1741
|
+
retry: false, // Don't retry on failure
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1661
1745
|
export function useVersionCheck() {
|
|
1662
1746
|
const capabilities = useCapabilities()
|
|
1663
1747
|
const apiBase = getApiBase()
|
|
@@ -1665,12 +1749,7 @@ export function useVersionCheck() {
|
|
|
1665
1749
|
? (capabilities.data.deployment?.mode ?? 'local')
|
|
1666
1750
|
: undefined
|
|
1667
1751
|
|
|
1668
|
-
const query = useQuery<VersionInfo>(
|
|
1669
|
-
queryKey: ["version-check", apiBase],
|
|
1670
|
-
queryFn: () => fetchJSON('/version-check'),
|
|
1671
|
-
staleTime: 60 * 60 * 1000, // 1 hour
|
|
1672
|
-
retry: false, // Don't retry on failure
|
|
1673
|
-
});
|
|
1752
|
+
const query = useQuery<VersionInfo>(versionCheckQueryOptions(apiBase));
|
|
1674
1753
|
|
|
1675
1754
|
useEffect(() => {
|
|
1676
1755
|
if (query.isSuccess) void triggerDailyUpdateCheck(deploymentMode).catch(() => {})
|
|
@@ -1679,6 +1758,36 @@ export function useVersionCheck() {
|
|
|
1679
1758
|
return query;
|
|
1680
1759
|
}
|
|
1681
1760
|
|
|
1761
|
+
/**
|
|
1762
|
+
* Guards a fetch for an endpoint the connected Radar may predate (see
|
|
1763
|
+
* radarFeatures.ts). A Radar known to be too old is never asked; one whose
|
|
1764
|
+
* version is unknown is asked, and chi's unknown-route 404 is read as the same
|
|
1765
|
+
* answer. Either way the caller gets a RadarFeatureUnsupportedError, which the
|
|
1766
|
+
* sections render as an upgrade note instead of an error.
|
|
1767
|
+
*
|
|
1768
|
+
* Nothing waits on /version-check: an agent that advertises the feature flag
|
|
1769
|
+
* is fetched immediately, and an unknown version falls through to the fetch.
|
|
1770
|
+
*
|
|
1771
|
+
* Spread `gatedKey` into the query key. A gate decided from early information
|
|
1772
|
+
* (a host-supplied version before /capabilities answers) can lift later, and
|
|
1773
|
+
* the key change is what makes the query fetch for real; ungated keys are
|
|
1774
|
+
* unchanged.
|
|
1775
|
+
*/
|
|
1776
|
+
export function useRadarFeature(feature: RadarFeature) {
|
|
1777
|
+
const { data: capabilities } = useCapabilities()
|
|
1778
|
+
const { data: versionInfo } = useQuery<VersionInfo>(versionCheckQueryOptions(getApiBase()))
|
|
1779
|
+
const { radarVersion } = useRadarUpgradeHost()
|
|
1780
|
+
const currentVersion = versionInfo?.currentVersion || radarVersion
|
|
1781
|
+
const latestVersion = versionInfo?.latestVersion
|
|
1782
|
+
const support = radarFeatureSupport(feature, capabilities, currentVersion)
|
|
1783
|
+
|
|
1784
|
+
return {
|
|
1785
|
+
gatedKey: support === 'unsupported' ? ['radar-feature-unsupported'] : [],
|
|
1786
|
+
guard: <T,>(request: () => Promise<T>): Promise<T> =>
|
|
1787
|
+
guardRadarFeature(feature, support, { currentVersion, latestVersion }, request),
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1682
1791
|
// ============================================================================
|
|
1683
1792
|
// Desktop Update API hooks
|
|
1684
1793
|
// ============================================================================
|
|
@@ -1700,9 +1809,7 @@ export function useStartDesktopUpdate() {
|
|
|
1700
1809
|
method: "POST",
|
|
1701
1810
|
});
|
|
1702
1811
|
if (!response.ok) {
|
|
1703
|
-
const error = await response
|
|
1704
|
-
.json()
|
|
1705
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
1812
|
+
const error = await readErrorBody(response);
|
|
1706
1813
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
1707
1814
|
}
|
|
1708
1815
|
return response.json();
|
|
@@ -1730,9 +1837,7 @@ export function useApplyDesktopUpdate() {
|
|
|
1730
1837
|
method: "POST",
|
|
1731
1838
|
});
|
|
1732
1839
|
if (!response.ok) {
|
|
1733
|
-
const error = await response
|
|
1734
|
-
.json()
|
|
1735
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
1840
|
+
const error = await readErrorBody(response);
|
|
1736
1841
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
1737
1842
|
}
|
|
1738
1843
|
return response.json();
|
|
@@ -2289,15 +2394,24 @@ export function useApplications(
|
|
|
2289
2394
|
const queryString = params.toString();
|
|
2290
2395
|
|
|
2291
2396
|
const enabled = options?.enabled !== false;
|
|
2397
|
+
const { guard, gatedKey } = useRadarFeature("applications");
|
|
2292
2398
|
return useQuery<{ applications: AppRow[] }>({
|
|
2293
|
-
queryKey: ["applications", namespaces],
|
|
2399
|
+
queryKey: ["applications", namespaces, ...gatedKey],
|
|
2294
2400
|
queryFn: () =>
|
|
2295
|
-
|
|
2401
|
+
guard(() =>
|
|
2402
|
+
fetchJSON<{ applications: AppRow[] }>(
|
|
2403
|
+
`/applications${queryString ? `?${queryString}` : ""}`,
|
|
2404
|
+
),
|
|
2405
|
+
),
|
|
2296
2406
|
staleTime: 30_000,
|
|
2407
|
+
retry: shouldRetryRadarQuery,
|
|
2297
2408
|
// Only poll while a consumer needs the index; gated off it must not keep the
|
|
2298
|
-
// background refetch alive.
|
|
2409
|
+
// background refetch alive. A Radar that predates the endpoint never will.
|
|
2299
2410
|
enabled,
|
|
2300
|
-
refetchInterval:
|
|
2411
|
+
refetchInterval: (query) =>
|
|
2412
|
+
enabled && !(query.state.error instanceof RadarFeatureUnsupportedError)
|
|
2413
|
+
? APPLICATIONS_REFRESH_INTERVAL_MS
|
|
2414
|
+
: false,
|
|
2301
2415
|
});
|
|
2302
2416
|
}
|
|
2303
2417
|
|
|
@@ -2489,7 +2603,7 @@ export function useResource<T>(
|
|
|
2489
2603
|
retry: (failureCount, error) => {
|
|
2490
2604
|
if (isStillLoadingError(error)) return true;
|
|
2491
2605
|
if (isKindSyncFailed(error)) return false;
|
|
2492
|
-
return failureCount < 1; //
|
|
2606
|
+
return failureCount < 1; // one retry, 4xx included, unlike the QueryClient default
|
|
2493
2607
|
},
|
|
2494
2608
|
retryDelay: (failureCount, error) =>
|
|
2495
2609
|
isStillLoadingError(error) ? 2000 : Math.min(1000 * 2 ** failureCount, 30000),
|
|
@@ -2521,7 +2635,7 @@ export function useResourceWithRelationships<T>(
|
|
|
2521
2635
|
retry: (failureCount, error) => {
|
|
2522
2636
|
if (isStillLoadingError(error)) return true;
|
|
2523
2637
|
if (isKindSyncFailed(error)) return false;
|
|
2524
|
-
return failureCount < 1; //
|
|
2638
|
+
return failureCount < 1; // one retry, 4xx included, unlike the QueryClient default
|
|
2525
2639
|
},
|
|
2526
2640
|
retryDelay: (failureCount, error) =>
|
|
2527
2641
|
isStillLoadingError(error) ? 2000 : Math.min(1000 * 2 ** failureCount, 30000),
|
|
@@ -2553,7 +2667,7 @@ export function useResources<T>(
|
|
|
2553
2667
|
retry: (failureCount, error) => {
|
|
2554
2668
|
if (isStillLoadingError(error)) return true;
|
|
2555
2669
|
if (isKindSyncFailed(error)) return false;
|
|
2556
|
-
return failureCount < 1; //
|
|
2670
|
+
return failureCount < 1; // one retry, 4xx included, unlike the QueryClient default
|
|
2557
2671
|
},
|
|
2558
2672
|
retryDelay: (failureCount, error) =>
|
|
2559
2673
|
isStillLoadingError(error) ? 2000 : Math.min(1000 * 2 ** failureCount, 30000),
|
|
@@ -2621,9 +2735,7 @@ async function fetchChangesPage(
|
|
|
2621
2735
|
signal ? { signal } : undefined,
|
|
2622
2736
|
);
|
|
2623
2737
|
if (!response.ok) {
|
|
2624
|
-
const errorData = await response
|
|
2625
|
-
.json()
|
|
2626
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
2738
|
+
const errorData = await readErrorBody(response);
|
|
2627
2739
|
throw new ApiError(
|
|
2628
2740
|
errorData.error || `HTTP ${response.status}`,
|
|
2629
2741
|
response.status,
|
|
@@ -2815,6 +2927,39 @@ export function useChanges(options: UseChangesOptions = {}) {
|
|
|
2815
2927
|
});
|
|
2816
2928
|
}
|
|
2817
2929
|
|
|
2930
|
+
export interface WorkloadHistoryPage {
|
|
2931
|
+
events: TimelineEvent[];
|
|
2932
|
+
truncated: boolean;
|
|
2933
|
+
nextBeforeSeq?: number;
|
|
2934
|
+
/** The workload has more related resources than the history follows. */
|
|
2935
|
+
incomplete?: boolean;
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
function workloadHistoryPath(kind: string, namespace: string, name: string, group?: string, beforeSeq?: number): string {
|
|
2939
|
+
const params = new URLSearchParams();
|
|
2940
|
+
if (group) params.set("group", group);
|
|
2941
|
+
if (beforeSeq) params.set("before_seq", String(beforeSeq));
|
|
2942
|
+
const query = params.toString();
|
|
2943
|
+
return `/workloads/${encodeURIComponent(kind)}/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/history${query ? `?${query}` : ""}`;
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
// The newest page of one workload's history: the workload, what it owns, K8s
|
|
2947
|
+
// Events about those, and the resources attached to it. Keyed under
|
|
2948
|
+
// "changes" so the live-update invalidation refreshes it.
|
|
2949
|
+
export function useWorkloadHistory(kind: string, namespace: string, name: string, group?: string, enabled = true) {
|
|
2950
|
+
return useQuery<WorkloadHistoryPage>({
|
|
2951
|
+
queryKey: ["changes", "workload-history", kind, namespace, name, group ?? ""],
|
|
2952
|
+
queryFn: ({ signal }) => fetchJSON(workloadHistoryPath(kind, namespace, name, group), signal),
|
|
2953
|
+
staleTime: 5000,
|
|
2954
|
+
refetchInterval: CHANGES_REFRESH_INTERVAL_MS,
|
|
2955
|
+
enabled,
|
|
2956
|
+
});
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
export function fetchWorkloadHistoryPage(kind: string, namespace: string, name: string, group: string | undefined, beforeSeq: number): Promise<WorkloadHistoryPage> {
|
|
2960
|
+
return fetchJSON(workloadHistoryPath(kind, namespace, name, group, beforeSeq));
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2818
2963
|
export interface ResourceEventsResult {
|
|
2819
2964
|
k8sEvents: TimelineEvent[];
|
|
2820
2965
|
updates: TimelineEvent[];
|
|
@@ -2964,11 +3109,13 @@ export function usePodMetrics(
|
|
|
2964
3109
|
}
|
|
2965
3110
|
|
|
2966
3111
|
export function usePodEnvironment(namespace: string, podName: string, enabled = true) {
|
|
3112
|
+
const { guard, gatedKey } = useRadarFeature('podEnvironment')
|
|
2967
3113
|
return useQuery<PodEnvironmentResponse>({
|
|
2968
|
-
queryKey: ['pod-environment', namespace, podName],
|
|
2969
|
-
queryFn: () => fetchJSON(
|
|
3114
|
+
queryKey: ['pod-environment', namespace, podName, ...gatedKey],
|
|
3115
|
+
queryFn: () => guard(() => fetchJSON<PodEnvironmentResponse>(
|
|
2970
3116
|
`/pods/${encodeURIComponent(namespace)}/${encodeURIComponent(podName)}/environment`,
|
|
2971
|
-
),
|
|
3117
|
+
)),
|
|
3118
|
+
retry: shouldRetryRadarQuery,
|
|
2972
3119
|
enabled: enabled && Boolean(namespace && podName),
|
|
2973
3120
|
staleTime: 10000,
|
|
2974
3121
|
})
|
|
@@ -3392,9 +3539,10 @@ export function prometheusStatusRefetchInterval(
|
|
|
3392
3539
|
}
|
|
3393
3540
|
|
|
3394
3541
|
// Check Prometheus availability
|
|
3395
|
-
export function usePrometheusStatus() {
|
|
3542
|
+
export function usePrometheusStatus(enabled = true) {
|
|
3396
3543
|
return useQuery<PrometheusStatus>({
|
|
3397
3544
|
queryKey: ["prometheus-status"],
|
|
3545
|
+
enabled,
|
|
3398
3546
|
queryFn: () => fetchJSON("/prometheus/status"),
|
|
3399
3547
|
staleTime: 30000,
|
|
3400
3548
|
refetchInterval: (query) =>
|
|
@@ -3433,9 +3581,7 @@ export function usePrometheusConnect() {
|
|
|
3433
3581
|
method: "POST",
|
|
3434
3582
|
});
|
|
3435
3583
|
if (!resp.ok) {
|
|
3436
|
-
const body = await resp
|
|
3437
|
-
.json()
|
|
3438
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
3584
|
+
const body = await readErrorBody(resp);
|
|
3439
3585
|
throw new Error(body.error || `HTTP ${resp.status}`);
|
|
3440
3586
|
}
|
|
3441
3587
|
return resp.json() as Promise<PrometheusStatus>;
|
|
@@ -3930,9 +4076,7 @@ export function useUpdateResource() {
|
|
|
3930
4076
|
body: yaml,
|
|
3931
4077
|
});
|
|
3932
4078
|
if (!response.ok) {
|
|
3933
|
-
const error = await response
|
|
3934
|
-
.json()
|
|
3935
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
4079
|
+
const error = await readErrorBody(response);
|
|
3936
4080
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
3937
4081
|
}
|
|
3938
4082
|
return response.json();
|
|
@@ -4044,9 +4188,7 @@ export function useDeleteResource() {
|
|
|
4044
4188
|
method: "DELETE",
|
|
4045
4189
|
});
|
|
4046
4190
|
if (!response.ok) {
|
|
4047
|
-
const error = await response
|
|
4048
|
-
.json()
|
|
4049
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
4191
|
+
const error = await readErrorBody(response);
|
|
4050
4192
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
4051
4193
|
}
|
|
4052
4194
|
// DELETE returns 204 No Content, no body to parse
|
|
@@ -4091,9 +4233,7 @@ export function useBulkDeleteResources() {
|
|
|
4091
4233
|
if (force) url.searchParams.set("force", "true");
|
|
4092
4234
|
const response = await apiFetch(url.toString(), { method: "DELETE" });
|
|
4093
4235
|
if (!response.ok) {
|
|
4094
|
-
const error = await response
|
|
4095
|
-
.json()
|
|
4096
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
4236
|
+
const error = await readErrorBody(response);
|
|
4097
4237
|
throw new Error(
|
|
4098
4238
|
error.error || `Failed to delete ${namespace}/${name}`,
|
|
4099
4239
|
);
|
|
@@ -4175,9 +4315,7 @@ export function useBulkRestartWorkloads() {
|
|
|
4175
4315
|
},
|
|
4176
4316
|
);
|
|
4177
4317
|
if (!response.ok) {
|
|
4178
|
-
const error = await response
|
|
4179
|
-
.json()
|
|
4180
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
4318
|
+
const error = await readErrorBody(response);
|
|
4181
4319
|
throw new Error(
|
|
4182
4320
|
`${namespace}/${name}: ${error.error || `HTTP ${response.status}`}`,
|
|
4183
4321
|
);
|
|
@@ -4247,9 +4385,7 @@ export function useBulkScaleWorkloads() {
|
|
|
4247
4385
|
},
|
|
4248
4386
|
);
|
|
4249
4387
|
if (!response.ok) {
|
|
4250
|
-
const error = await response
|
|
4251
|
-
.json()
|
|
4252
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
4388
|
+
const error = await readErrorBody(response);
|
|
4253
4389
|
throw new Error(
|
|
4254
4390
|
`${namespace}/${name}: ${error.error || `HTTP ${response.status}`}`,
|
|
4255
4391
|
);
|
|
@@ -4433,7 +4569,7 @@ export function usePreviewResources() {
|
|
|
4433
4569
|
body: JSON.stringify(request),
|
|
4434
4570
|
})
|
|
4435
4571
|
if (!response.ok) {
|
|
4436
|
-
const error = await response
|
|
4572
|
+
const error = await readErrorBody(response)
|
|
4437
4573
|
throw new Error(error.error || `HTTP ${response.status}`)
|
|
4438
4574
|
}
|
|
4439
4575
|
return response.json() as Promise<YamlPreviewResponse>
|
|
@@ -4483,9 +4619,7 @@ export function useApplyResource() {
|
|
|
4483
4619
|
body: yaml,
|
|
4484
4620
|
});
|
|
4485
4621
|
if (!response.ok) {
|
|
4486
|
-
const error = (await response
|
|
4487
|
-
.json()
|
|
4488
|
-
.catch(() => ({ error: 'Unknown error' }))) as ApplyResourceErrorResponse
|
|
4622
|
+
const error = (await readErrorBody(response)) as ApplyResourceErrorResponse
|
|
4489
4623
|
throw new ApplyResourceError(error, response.status)
|
|
4490
4624
|
}
|
|
4491
4625
|
return response.json() as Promise<ApplyResourceResult[]>;
|
|
@@ -4541,9 +4675,7 @@ export function useTriggerCronJob() {
|
|
|
4541
4675
|
},
|
|
4542
4676
|
);
|
|
4543
4677
|
if (!response.ok) {
|
|
4544
|
-
const error = await response
|
|
4545
|
-
.json()
|
|
4546
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
4678
|
+
const error = await readErrorBody(response);
|
|
4547
4679
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
4548
4680
|
}
|
|
4549
4681
|
return response.json();
|
|
@@ -4581,9 +4713,7 @@ export function useSuspendCronJob() {
|
|
|
4581
4713
|
},
|
|
4582
4714
|
);
|
|
4583
4715
|
if (!response.ok) {
|
|
4584
|
-
const error = await response
|
|
4585
|
-
.json()
|
|
4586
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
4716
|
+
const error = await readErrorBody(response);
|
|
4587
4717
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
4588
4718
|
}
|
|
4589
4719
|
return response.json();
|
|
@@ -4621,9 +4751,7 @@ export function useResumeCronJob() {
|
|
|
4621
4751
|
},
|
|
4622
4752
|
);
|
|
4623
4753
|
if (!response.ok) {
|
|
4624
|
-
const error = await response
|
|
4625
|
-
.json()
|
|
4626
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
4754
|
+
const error = await readErrorBody(response);
|
|
4627
4755
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
4628
4756
|
}
|
|
4629
4757
|
return response.json();
|
|
@@ -4667,9 +4795,7 @@ export function useRestartWorkload() {
|
|
|
4667
4795
|
},
|
|
4668
4796
|
);
|
|
4669
4797
|
if (!response.ok) {
|
|
4670
|
-
const error = await response
|
|
4671
|
-
.json()
|
|
4672
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
4798
|
+
const error = await readErrorBody(response);
|
|
4673
4799
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
4674
4800
|
}
|
|
4675
4801
|
return response.json();
|
|
@@ -4796,9 +4922,7 @@ export function useScaleWorkload() {
|
|
|
4796
4922
|
},
|
|
4797
4923
|
);
|
|
4798
4924
|
if (!response.ok) {
|
|
4799
|
-
const error = await response
|
|
4800
|
-
.json()
|
|
4801
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
4925
|
+
const error = await readErrorBody(response);
|
|
4802
4926
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
4803
4927
|
}
|
|
4804
4928
|
return response.json();
|
|
@@ -4879,9 +5003,7 @@ export function useRollbackWorkload() {
|
|
|
4879
5003
|
},
|
|
4880
5004
|
);
|
|
4881
5005
|
if (!response.ok) {
|
|
4882
|
-
const error = await response
|
|
4883
|
-
.json()
|
|
4884
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
5006
|
+
const error = await readErrorBody(response);
|
|
4885
5007
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
4886
5008
|
}
|
|
4887
5009
|
return response.json();
|
|
@@ -5021,9 +5143,7 @@ export function useRolloutAction(options?: { reportErrors?: boolean }) {
|
|
|
5021
5143
|
{ method: "POST" },
|
|
5022
5144
|
);
|
|
5023
5145
|
if (!response.ok) {
|
|
5024
|
-
const error = await response
|
|
5025
|
-
.json()
|
|
5026
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
5146
|
+
const error = await readErrorBody(response);
|
|
5027
5147
|
// The code, not the status, says whether retrying can help: a lost cluster
|
|
5028
5148
|
// connection answers 503 on this route too.
|
|
5029
5149
|
throw new ApiError(
|
|
@@ -5081,9 +5201,7 @@ export function useCordonNode() {
|
|
|
5081
5201
|
method: "POST",
|
|
5082
5202
|
});
|
|
5083
5203
|
if (!response.ok) {
|
|
5084
|
-
const error = await response
|
|
5085
|
-
.json()
|
|
5086
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
5204
|
+
const error = await readErrorBody(response);
|
|
5087
5205
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
5088
5206
|
}
|
|
5089
5207
|
return response.json();
|
|
@@ -5114,9 +5232,7 @@ export function useUncordonNode() {
|
|
|
5114
5232
|
},
|
|
5115
5233
|
);
|
|
5116
5234
|
if (!response.ok) {
|
|
5117
|
-
const error = await response
|
|
5118
|
-
.json()
|
|
5119
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
5235
|
+
const error = await readErrorBody(response);
|
|
5120
5236
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
5121
5237
|
}
|
|
5122
5238
|
return response.json();
|
|
@@ -5158,51 +5274,27 @@ export function drainPlanBody(options: DrainPlanRequestOptions): string {
|
|
|
5158
5274
|
});
|
|
5159
5275
|
}
|
|
5160
5276
|
|
|
5161
|
-
/**
|
|
5162
|
-
* The connected radar predates the drain-plan endpoint. Distinguished from a
|
|
5163
|
-
* node-not-found 404 by the body: handlers answer with a JSON error envelope,
|
|
5164
|
-
* while an unknown route gets the router's plain-text 404. Hosts serving a newer
|
|
5165
|
-
* frontend against an older radar (Radar Hub) use this to fall back to the
|
|
5166
|
-
* plan-less drain dialog instead of leaving Drain permanently disabled.
|
|
5167
|
-
*/
|
|
5168
|
-
export class DrainPlanUnsupportedError extends Error {
|
|
5169
|
-
constructor() {
|
|
5170
|
-
super("This radar does not support drain plans");
|
|
5171
|
-
this.name = "DrainPlanUnsupportedError";
|
|
5172
|
-
}
|
|
5173
|
-
}
|
|
5174
|
-
|
|
5175
|
-
export function drainPlanFetchError(
|
|
5176
|
-
status: number,
|
|
5177
|
-
body: { error?: string } | null,
|
|
5178
|
-
): Error {
|
|
5179
|
-
if (status === 404 && body === null) {
|
|
5180
|
-
return new DrainPlanUnsupportedError();
|
|
5181
|
-
}
|
|
5182
|
-
return new Error(body?.error || `HTTP ${status}`);
|
|
5183
|
-
}
|
|
5184
|
-
|
|
5185
5277
|
// Read-only drain plan: what a drain with these options would do to each pod on the node.
|
|
5186
5278
|
// Modelled as a mutation because it is a POST with a body and is fetched on demand
|
|
5187
5279
|
// while the drain dialog is open; it performs no cluster mutation.
|
|
5280
|
+
// An older Radar without the endpoint fails with RadarFeatureUnsupportedError,
|
|
5281
|
+
// which hosts (Radar Hub) use to fall back to the plan-less drain dialog
|
|
5282
|
+
// instead of leaving Drain permanently disabled.
|
|
5188
5283
|
export function useDrainPlan() {
|
|
5284
|
+
const { guard } = useRadarFeature("drainPlan");
|
|
5189
5285
|
return useMutation<
|
|
5190
5286
|
DrainPlan,
|
|
5191
5287
|
Error,
|
|
5192
5288
|
{ name: string; options: DrainPlanRequestOptions }
|
|
5193
5289
|
>({
|
|
5194
|
-
mutationFn:
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
throw drainPlanFetchError(response.status, body);
|
|
5203
|
-
}
|
|
5204
|
-
return response.json();
|
|
5205
|
-
},
|
|
5290
|
+
mutationFn: ({ name, options }) =>
|
|
5291
|
+
guard(() =>
|
|
5292
|
+
fetchJSON<DrainPlan>(drainPlanPath(name), {
|
|
5293
|
+
method: "POST",
|
|
5294
|
+
headers: { "Content-Type": "application/json" },
|
|
5295
|
+
body: drainPlanBody(options),
|
|
5296
|
+
}),
|
|
5297
|
+
),
|
|
5206
5298
|
// Deliberately no meta toast keys: the plan is fetched only from the drain dialog, which
|
|
5207
5299
|
// renders a failure inline and keeps Drain disabled while it shows; a global toast would
|
|
5208
5300
|
// report the same failure twice.
|
|
@@ -5226,9 +5318,7 @@ export function useDrainNode() {
|
|
|
5226
5318
|
body: options ? JSON.stringify(options) : undefined,
|
|
5227
5319
|
});
|
|
5228
5320
|
if (!response.ok) {
|
|
5229
|
-
const error = await response
|
|
5230
|
-
.json()
|
|
5231
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
5321
|
+
const error = await readErrorBody(response);
|
|
5232
5322
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
5233
5323
|
}
|
|
5234
5324
|
return response.json();
|
|
@@ -5363,9 +5453,7 @@ export function useHelmManifest(
|
|
|
5363
5453
|
`${getApiBase()}/helm/releases/${namespace}/${name}/manifest${params}`,
|
|
5364
5454
|
);
|
|
5365
5455
|
if (!response.ok) {
|
|
5366
|
-
const error = await response
|
|
5367
|
-
.json()
|
|
5368
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
5456
|
+
const error = await readErrorBody(response);
|
|
5369
5457
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
5370
5458
|
}
|
|
5371
5459
|
return response.text();
|
|
@@ -5609,9 +5697,7 @@ export function useHelmRollback() {
|
|
|
5609
5697
|
},
|
|
5610
5698
|
);
|
|
5611
5699
|
if (!response.ok) {
|
|
5612
|
-
const error = await response
|
|
5613
|
-
.json()
|
|
5614
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
5700
|
+
const error = await readErrorBody(response);
|
|
5615
5701
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
5616
5702
|
}
|
|
5617
5703
|
return response.json();
|
|
@@ -5648,9 +5734,7 @@ export function useHelmUninstall() {
|
|
|
5648
5734
|
},
|
|
5649
5735
|
);
|
|
5650
5736
|
if (!response.ok) {
|
|
5651
|
-
const error = await response
|
|
5652
|
-
.json()
|
|
5653
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
5737
|
+
const error = await readErrorBody(response);
|
|
5654
5738
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
5655
5739
|
}
|
|
5656
5740
|
return response.json();
|
|
@@ -5682,9 +5766,7 @@ function streamHelmProgress(
|
|
|
5682
5766
|
fetch(url, { credentials: getCredentialsMode(), ...options, headers })
|
|
5683
5767
|
.then(async (response) => {
|
|
5684
5768
|
if (!response.ok) {
|
|
5685
|
-
const error = await response
|
|
5686
|
-
.json()
|
|
5687
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
5769
|
+
const error = await readErrorBody(response);
|
|
5688
5770
|
reject(new Error(error.error || `HTTP ${response.status}`));
|
|
5689
5771
|
return;
|
|
5690
5772
|
}
|
|
@@ -5804,9 +5886,7 @@ export function useHelmPreviewValues() {
|
|
|
5804
5886
|
},
|
|
5805
5887
|
);
|
|
5806
5888
|
if (!response.ok) {
|
|
5807
|
-
const error = await response
|
|
5808
|
-
.json()
|
|
5809
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
5889
|
+
const error = await readErrorBody(response);
|
|
5810
5890
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
5811
5891
|
}
|
|
5812
5892
|
return response.json();
|
|
@@ -5837,9 +5917,7 @@ export function useHelmApplyValues() {
|
|
|
5837
5917
|
},
|
|
5838
5918
|
);
|
|
5839
5919
|
if (!response.ok) {
|
|
5840
|
-
const error = await response
|
|
5841
|
-
.json()
|
|
5842
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
5920
|
+
const error = await readErrorBody(response);
|
|
5843
5921
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
5844
5922
|
}
|
|
5845
5923
|
return response.json();
|
|
@@ -5884,9 +5962,7 @@ async function updateRepositoryFn(repoName: string): Promise<unknown> {
|
|
|
5884
5962
|
},
|
|
5885
5963
|
);
|
|
5886
5964
|
if (!response.ok) {
|
|
5887
|
-
const error = await response
|
|
5888
|
-
.json()
|
|
5889
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
5965
|
+
const error = await readErrorBody(response);
|
|
5890
5966
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
5891
5967
|
}
|
|
5892
5968
|
return response.json();
|
|
@@ -5944,9 +6020,7 @@ async function mutateOCISource(
|
|
|
5944
6020
|
body: JSON.stringify({ source }),
|
|
5945
6021
|
});
|
|
5946
6022
|
if (!response.ok) {
|
|
5947
|
-
const error = await response
|
|
5948
|
-
.json()
|
|
5949
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
6023
|
+
const error = await readErrorBody(response);
|
|
5950
6024
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
5951
6025
|
}
|
|
5952
6026
|
return response.json();
|
|
@@ -6035,9 +6109,7 @@ export function useInstallChart() {
|
|
|
6035
6109
|
body: JSON.stringify(req),
|
|
6036
6110
|
});
|
|
6037
6111
|
if (!response.ok) {
|
|
6038
|
-
const error = await response
|
|
6039
|
-
.json()
|
|
6040
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
6112
|
+
const error = await readErrorBody(response);
|
|
6041
6113
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
6042
6114
|
}
|
|
6043
6115
|
return response.json() as Promise<HelmRelease>;
|
|
@@ -6180,9 +6252,7 @@ function createGitOpsMutation<TVariables>(
|
|
|
6180
6252
|
},
|
|
6181
6253
|
);
|
|
6182
6254
|
if (!response.ok) {
|
|
6183
|
-
const error = await response
|
|
6184
|
-
.json()
|
|
6185
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
6255
|
+
const error = await readErrorBody(response);
|
|
6186
6256
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
6187
6257
|
}
|
|
6188
6258
|
return response.json() as Promise<GitOpsOperationResponse>;
|
|
@@ -6363,9 +6433,7 @@ export function useArgoResourceValidation() {
|
|
|
6363
6433
|
},
|
|
6364
6434
|
);
|
|
6365
6435
|
if (!response.ok) {
|
|
6366
|
-
const error = await response
|
|
6367
|
-
.json()
|
|
6368
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
6436
|
+
const error = await readErrorBody(response);
|
|
6369
6437
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
6370
6438
|
}
|
|
6371
6439
|
return response.json() as Promise<ArgoResourceValidationResult>;
|
|
@@ -6429,9 +6497,7 @@ export function useArgoRefresh() {
|
|
|
6429
6497
|
},
|
|
6430
6498
|
);
|
|
6431
6499
|
if (!response.ok) {
|
|
6432
|
-
const error = await response
|
|
6433
|
-
.json()
|
|
6434
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
6500
|
+
const error = await readErrorBody(response);
|
|
6435
6501
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
6436
6502
|
}
|
|
6437
6503
|
return response.json();
|
|
@@ -6515,9 +6581,7 @@ export function useSwitchContext() {
|
|
|
6515
6581
|
clearTimeout(timeoutId);
|
|
6516
6582
|
|
|
6517
6583
|
if (!response.ok) {
|
|
6518
|
-
const error = await response
|
|
6519
|
-
.json()
|
|
6520
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
6584
|
+
const error = await readErrorBody(response);
|
|
6521
6585
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
6522
6586
|
}
|
|
6523
6587
|
return response.json();
|
|
@@ -6644,9 +6708,7 @@ export function useSetActiveNamespace() {
|
|
|
6644
6708
|
durationMs: Math.round(performance.now() - startedAt),
|
|
6645
6709
|
});
|
|
6646
6710
|
if (!response.ok) {
|
|
6647
|
-
const error = await response
|
|
6648
|
-
.json()
|
|
6649
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
6711
|
+
const error = await readErrorBody(response);
|
|
6650
6712
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
6651
6713
|
}
|
|
6652
6714
|
return response.json();
|
|
@@ -6905,15 +6967,18 @@ export function useWorkloadRuns(
|
|
|
6905
6967
|
});
|
|
6906
6968
|
}
|
|
6907
6969
|
|
|
6908
|
-
export function useKueueAdmission(namespace: string, name: string, uid: string | undefined) {
|
|
6970
|
+
export function useKueueAdmission(namespace: string, name: string, uid: string | undefined, options?: { isJob?: boolean; isRayJob?: boolean; hinted?: boolean; terminal?: boolean }) {
|
|
6971
|
+
const group = options?.isRayJob ? 'ray.io' : options?.isJob ? 'batch' : 'jobset.x-k8s.io'
|
|
6972
|
+
const kind = options?.isRayJob ? 'rayjobs' : options?.isJob ? 'jobs' : 'jobsets'
|
|
6909
6973
|
return useQuery<KueueAdmissionResponse>({
|
|
6910
|
-
queryKey: ['kueue-admission',
|
|
6911
|
-
queryFn: () => fetchJSON(`/kueue/admission
|
|
6974
|
+
queryKey: ['kueue-admission', group, kind, namespace, name, uid],
|
|
6975
|
+
queryFn: () => fetchJSON(`/kueue/admission/${kind}/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}?group=${group}`),
|
|
6912
6976
|
enabled: Boolean(namespace && name && uid),
|
|
6913
6977
|
staleTime: 5000,
|
|
6914
6978
|
refetchInterval: (query) => {
|
|
6915
6979
|
if (query.state.error instanceof ApiError && query.state.error.status < 500) return false
|
|
6916
6980
|
if (query.state.data?.uid === uid && query.state.data?.installed === false) return false
|
|
6981
|
+
if ((options?.isJob || options?.isRayJob) && (options.terminal || (!options.hinted && !query.state.data?.workloads.length))) return 30000
|
|
6917
6982
|
return 5000
|
|
6918
6983
|
},
|
|
6919
6984
|
retry: (count, error) => !(error instanceof ApiError && error.status < 500) && count < 2,
|