@skyhook-io/radar-app 1.15.0 → 1.16.1
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 +14 -11
- 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.capacity.test.ts +15 -0
- package/src/api/client.trace.test.ts +9 -1
- package/src/api/client.ts +247 -201
- 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 +223 -0
- package/src/api/radarFeatures.test.ts +142 -0
- package/src/api/radarFeatures.ts +148 -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/components/CloudFunnelButton.tsx +9 -9
- package/src/components/NamespaceSwitcher.test.tsx +47 -0
- package/src/components/NamespaceSwitcher.tsx +17 -1
- package/src/components/SelfManagedStart.tsx +5 -6
- 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/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/dock/NodeTerminalTab.tsx +2 -1
- package/src/components/dock/TerminalTab.tsx +2 -1
- package/src/components/dock/TrafficFlowListTab.tsx +2 -2
- package/src/components/gitops/GitOpsView.tsx +12 -0
- package/src/components/home/HomeView.tsx +1 -1
- package/src/components/home/MCPSetupDialog.tsx +37 -37
- 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/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/PrometheusConfigField.tsx +30 -0
- package/src/components/settings/PrometheusConnectionForm.tsx +295 -0
- package/src/components/settings/SettingsDialog.tsx +382 -521
- 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 +20 -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/Omnibar.tsx +1 -1
- package/src/components/ui/UpdateNotification.tsx +57 -12
- package/src/components/workload/WorkloadView.tsx +44 -6
- 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/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,
|
|
@@ -1689,6 +1733,15 @@ export async function triggerDailyUpdateCheck(
|
|
|
1689
1733
|
})
|
|
1690
1734
|
}
|
|
1691
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
|
+
|
|
1692
1745
|
export function useVersionCheck() {
|
|
1693
1746
|
const capabilities = useCapabilities()
|
|
1694
1747
|
const apiBase = getApiBase()
|
|
@@ -1696,12 +1749,7 @@ export function useVersionCheck() {
|
|
|
1696
1749
|
? (capabilities.data.deployment?.mode ?? 'local')
|
|
1697
1750
|
: undefined
|
|
1698
1751
|
|
|
1699
|
-
const query = useQuery<VersionInfo>(
|
|
1700
|
-
queryKey: ["version-check", apiBase],
|
|
1701
|
-
queryFn: () => fetchJSON('/version-check'),
|
|
1702
|
-
staleTime: 60 * 60 * 1000, // 1 hour
|
|
1703
|
-
retry: false, // Don't retry on failure
|
|
1704
|
-
});
|
|
1752
|
+
const query = useQuery<VersionInfo>(versionCheckQueryOptions(apiBase));
|
|
1705
1753
|
|
|
1706
1754
|
useEffect(() => {
|
|
1707
1755
|
if (query.isSuccess) void triggerDailyUpdateCheck(deploymentMode).catch(() => {})
|
|
@@ -1710,6 +1758,37 @@ export function useVersionCheck() {
|
|
|
1710
1758
|
return query;
|
|
1711
1759
|
}
|
|
1712
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
|
+
support,
|
|
1786
|
+
gatedKey: support === 'unsupported' ? ['radar-feature-unsupported'] : [],
|
|
1787
|
+
guard: <T,>(request: () => Promise<T>): Promise<T> =>
|
|
1788
|
+
guardRadarFeature(feature, support, { currentVersion, latestVersion }, request),
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1713
1792
|
// ============================================================================
|
|
1714
1793
|
// Desktop Update API hooks
|
|
1715
1794
|
// ============================================================================
|
|
@@ -1731,9 +1810,7 @@ export function useStartDesktopUpdate() {
|
|
|
1731
1810
|
method: "POST",
|
|
1732
1811
|
});
|
|
1733
1812
|
if (!response.ok) {
|
|
1734
|
-
const error = await response
|
|
1735
|
-
.json()
|
|
1736
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
1813
|
+
const error = await readErrorBody(response);
|
|
1737
1814
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
1738
1815
|
}
|
|
1739
1816
|
return response.json();
|
|
@@ -1761,9 +1838,7 @@ export function useApplyDesktopUpdate() {
|
|
|
1761
1838
|
method: "POST",
|
|
1762
1839
|
});
|
|
1763
1840
|
if (!response.ok) {
|
|
1764
|
-
const error = await response
|
|
1765
|
-
.json()
|
|
1766
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
1841
|
+
const error = await readErrorBody(response);
|
|
1767
1842
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
1768
1843
|
}
|
|
1769
1844
|
return response.json();
|
|
@@ -2320,15 +2395,24 @@ export function useApplications(
|
|
|
2320
2395
|
const queryString = params.toString();
|
|
2321
2396
|
|
|
2322
2397
|
const enabled = options?.enabled !== false;
|
|
2398
|
+
const { guard, gatedKey } = useRadarFeature("applications");
|
|
2323
2399
|
return useQuery<{ applications: AppRow[] }>({
|
|
2324
|
-
queryKey: ["applications", namespaces],
|
|
2400
|
+
queryKey: ["applications", namespaces, ...gatedKey],
|
|
2325
2401
|
queryFn: () =>
|
|
2326
|
-
|
|
2402
|
+
guard(() =>
|
|
2403
|
+
fetchJSON<{ applications: AppRow[] }>(
|
|
2404
|
+
`/applications${queryString ? `?${queryString}` : ""}`,
|
|
2405
|
+
),
|
|
2406
|
+
),
|
|
2327
2407
|
staleTime: 30_000,
|
|
2408
|
+
retry: shouldRetryRadarQuery,
|
|
2328
2409
|
// Only poll while a consumer needs the index; gated off it must not keep the
|
|
2329
|
-
// background refetch alive.
|
|
2410
|
+
// background refetch alive. A Radar that predates the endpoint never will.
|
|
2330
2411
|
enabled,
|
|
2331
|
-
refetchInterval:
|
|
2412
|
+
refetchInterval: (query) =>
|
|
2413
|
+
enabled && !(query.state.error instanceof RadarFeatureUnsupportedError)
|
|
2414
|
+
? APPLICATIONS_REFRESH_INTERVAL_MS
|
|
2415
|
+
: false,
|
|
2332
2416
|
});
|
|
2333
2417
|
}
|
|
2334
2418
|
|
|
@@ -2520,7 +2604,7 @@ export function useResource<T>(
|
|
|
2520
2604
|
retry: (failureCount, error) => {
|
|
2521
2605
|
if (isStillLoadingError(error)) return true;
|
|
2522
2606
|
if (isKindSyncFailed(error)) return false;
|
|
2523
|
-
return failureCount < 1; //
|
|
2607
|
+
return failureCount < 1; // one retry, 4xx included, unlike the QueryClient default
|
|
2524
2608
|
},
|
|
2525
2609
|
retryDelay: (failureCount, error) =>
|
|
2526
2610
|
isStillLoadingError(error) ? 2000 : Math.min(1000 * 2 ** failureCount, 30000),
|
|
@@ -2552,7 +2636,7 @@ export function useResourceWithRelationships<T>(
|
|
|
2552
2636
|
retry: (failureCount, error) => {
|
|
2553
2637
|
if (isStillLoadingError(error)) return true;
|
|
2554
2638
|
if (isKindSyncFailed(error)) return false;
|
|
2555
|
-
return failureCount < 1; //
|
|
2639
|
+
return failureCount < 1; // one retry, 4xx included, unlike the QueryClient default
|
|
2556
2640
|
},
|
|
2557
2641
|
retryDelay: (failureCount, error) =>
|
|
2558
2642
|
isStillLoadingError(error) ? 2000 : Math.min(1000 * 2 ** failureCount, 30000),
|
|
@@ -2584,7 +2668,7 @@ export function useResources<T>(
|
|
|
2584
2668
|
retry: (failureCount, error) => {
|
|
2585
2669
|
if (isStillLoadingError(error)) return true;
|
|
2586
2670
|
if (isKindSyncFailed(error)) return false;
|
|
2587
|
-
return failureCount < 1; //
|
|
2671
|
+
return failureCount < 1; // one retry, 4xx included, unlike the QueryClient default
|
|
2588
2672
|
},
|
|
2589
2673
|
retryDelay: (failureCount, error) =>
|
|
2590
2674
|
isStillLoadingError(error) ? 2000 : Math.min(1000 * 2 ** failureCount, 30000),
|
|
@@ -2652,9 +2736,7 @@ async function fetchChangesPage(
|
|
|
2652
2736
|
signal ? { signal } : undefined,
|
|
2653
2737
|
);
|
|
2654
2738
|
if (!response.ok) {
|
|
2655
|
-
const errorData = await response
|
|
2656
|
-
.json()
|
|
2657
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
2739
|
+
const errorData = await readErrorBody(response);
|
|
2658
2740
|
throw new ApiError(
|
|
2659
2741
|
errorData.error || `HTTP ${response.status}`,
|
|
2660
2742
|
response.status,
|
|
@@ -2846,6 +2928,53 @@ export function useChanges(options: UseChangesOptions = {}) {
|
|
|
2846
2928
|
});
|
|
2847
2929
|
}
|
|
2848
2930
|
|
|
2931
|
+
export interface WorkloadHistoryPage {
|
|
2932
|
+
events: TimelineEvent[];
|
|
2933
|
+
truncated: boolean;
|
|
2934
|
+
nextBeforeSeq?: number;
|
|
2935
|
+
/** The workload has more related resources than the history follows. */
|
|
2936
|
+
incomplete?: boolean;
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
function workloadHistoryPath(kind: string, namespace: string, name: string, group?: string, beforeSeq?: number): string {
|
|
2940
|
+
const params = new URLSearchParams();
|
|
2941
|
+
if (group) params.set("group", group);
|
|
2942
|
+
if (beforeSeq) params.set("before_seq", String(beforeSeq));
|
|
2943
|
+
const query = params.toString();
|
|
2944
|
+
return `/workloads/${encodeURIComponent(kind)}/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/history${query ? `?${query}` : ""}`;
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
// The newest page of one workload's history: the workload, what it owns, K8s
|
|
2948
|
+
// Events about those, and the resources attached to it. Keyed under
|
|
2949
|
+
// "changes" so the live-update invalidation refreshes it.
|
|
2950
|
+
// A Radar that predates the scoped history endpoint settles as unsupported;
|
|
2951
|
+
// WorkloadView then falls back to the namespace's changes.
|
|
2952
|
+
export function useWorkloadHistory(kind: string, namespace: string, name: string, group?: string, enabled = true) {
|
|
2953
|
+
const { guard, gatedKey, support } = useRadarFeature("workloadHistory");
|
|
2954
|
+
const query = useQuery<WorkloadHistoryPage>({
|
|
2955
|
+
queryKey: ["changes", "workload-history", kind, namespace, name, group ?? "", ...gatedKey],
|
|
2956
|
+
queryFn: ({ signal }) =>
|
|
2957
|
+
guard(() => fetchJSON<WorkloadHistoryPage>(workloadHistoryPath(kind, namespace, name, group), signal)),
|
|
2958
|
+
staleTime: 5000,
|
|
2959
|
+
retry: shouldRetryRadarQuery,
|
|
2960
|
+
refetchInterval: (query) =>
|
|
2961
|
+
query.state.error instanceof RadarFeatureUnsupportedError ? false : CHANGES_REFRESH_INTERVAL_MS,
|
|
2962
|
+
enabled,
|
|
2963
|
+
});
|
|
2964
|
+
// A probe answered by an older Radar settles as unsupported and stops
|
|
2965
|
+
// polling; once /capabilities confirms the feature (the agent was upgraded),
|
|
2966
|
+
// ask again rather than staying on the fallback.
|
|
2967
|
+
const { error, refetch } = query;
|
|
2968
|
+
useEffect(() => {
|
|
2969
|
+
if (support === "supported" && error instanceof RadarFeatureUnsupportedError) void refetch();
|
|
2970
|
+
}, [support, error, refetch]);
|
|
2971
|
+
return query;
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
export function fetchWorkloadHistoryPage(kind: string, namespace: string, name: string, group: string | undefined, beforeSeq: number): Promise<WorkloadHistoryPage> {
|
|
2975
|
+
return fetchJSON(workloadHistoryPath(kind, namespace, name, group, beforeSeq));
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2849
2978
|
export interface ResourceEventsResult {
|
|
2850
2979
|
k8sEvents: TimelineEvent[];
|
|
2851
2980
|
updates: TimelineEvent[];
|
|
@@ -2995,11 +3124,13 @@ export function usePodMetrics(
|
|
|
2995
3124
|
}
|
|
2996
3125
|
|
|
2997
3126
|
export function usePodEnvironment(namespace: string, podName: string, enabled = true) {
|
|
3127
|
+
const { guard, gatedKey } = useRadarFeature('podEnvironment')
|
|
2998
3128
|
return useQuery<PodEnvironmentResponse>({
|
|
2999
|
-
queryKey: ['pod-environment', namespace, podName],
|
|
3000
|
-
queryFn: () => fetchJSON(
|
|
3129
|
+
queryKey: ['pod-environment', namespace, podName, ...gatedKey],
|
|
3130
|
+
queryFn: () => guard(() => fetchJSON<PodEnvironmentResponse>(
|
|
3001
3131
|
`/pods/${encodeURIComponent(namespace)}/${encodeURIComponent(podName)}/environment`,
|
|
3002
|
-
),
|
|
3132
|
+
)),
|
|
3133
|
+
retry: shouldRetryRadarQuery,
|
|
3003
3134
|
enabled: enabled && Boolean(namespace && podName),
|
|
3004
3135
|
staleTime: 10000,
|
|
3005
3136
|
})
|
|
@@ -3423,9 +3554,10 @@ export function prometheusStatusRefetchInterval(
|
|
|
3423
3554
|
}
|
|
3424
3555
|
|
|
3425
3556
|
// Check Prometheus availability
|
|
3426
|
-
export function usePrometheusStatus() {
|
|
3557
|
+
export function usePrometheusStatus(enabled = true) {
|
|
3427
3558
|
return useQuery<PrometheusStatus>({
|
|
3428
3559
|
queryKey: ["prometheus-status"],
|
|
3560
|
+
enabled,
|
|
3429
3561
|
queryFn: () => fetchJSON("/prometheus/status"),
|
|
3430
3562
|
staleTime: 30000,
|
|
3431
3563
|
refetchInterval: (query) =>
|
|
@@ -3464,9 +3596,7 @@ export function usePrometheusConnect() {
|
|
|
3464
3596
|
method: "POST",
|
|
3465
3597
|
});
|
|
3466
3598
|
if (!resp.ok) {
|
|
3467
|
-
const body = await resp
|
|
3468
|
-
.json()
|
|
3469
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
3599
|
+
const body = await readErrorBody(resp);
|
|
3470
3600
|
throw new Error(body.error || `HTTP ${resp.status}`);
|
|
3471
3601
|
}
|
|
3472
3602
|
return resp.json() as Promise<PrometheusStatus>;
|
|
@@ -3961,9 +4091,7 @@ export function useUpdateResource() {
|
|
|
3961
4091
|
body: yaml,
|
|
3962
4092
|
});
|
|
3963
4093
|
if (!response.ok) {
|
|
3964
|
-
const error = await response
|
|
3965
|
-
.json()
|
|
3966
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
4094
|
+
const error = await readErrorBody(response);
|
|
3967
4095
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
3968
4096
|
}
|
|
3969
4097
|
return response.json();
|
|
@@ -4075,9 +4203,7 @@ export function useDeleteResource() {
|
|
|
4075
4203
|
method: "DELETE",
|
|
4076
4204
|
});
|
|
4077
4205
|
if (!response.ok) {
|
|
4078
|
-
const error = await response
|
|
4079
|
-
.json()
|
|
4080
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
4206
|
+
const error = await readErrorBody(response);
|
|
4081
4207
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
4082
4208
|
}
|
|
4083
4209
|
// DELETE returns 204 No Content, no body to parse
|
|
@@ -4122,9 +4248,7 @@ export function useBulkDeleteResources() {
|
|
|
4122
4248
|
if (force) url.searchParams.set("force", "true");
|
|
4123
4249
|
const response = await apiFetch(url.toString(), { method: "DELETE" });
|
|
4124
4250
|
if (!response.ok) {
|
|
4125
|
-
const error = await response
|
|
4126
|
-
.json()
|
|
4127
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
4251
|
+
const error = await readErrorBody(response);
|
|
4128
4252
|
throw new Error(
|
|
4129
4253
|
error.error || `Failed to delete ${namespace}/${name}`,
|
|
4130
4254
|
);
|
|
@@ -4206,9 +4330,7 @@ export function useBulkRestartWorkloads() {
|
|
|
4206
4330
|
},
|
|
4207
4331
|
);
|
|
4208
4332
|
if (!response.ok) {
|
|
4209
|
-
const error = await response
|
|
4210
|
-
.json()
|
|
4211
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
4333
|
+
const error = await readErrorBody(response);
|
|
4212
4334
|
throw new Error(
|
|
4213
4335
|
`${namespace}/${name}: ${error.error || `HTTP ${response.status}`}`,
|
|
4214
4336
|
);
|
|
@@ -4278,9 +4400,7 @@ export function useBulkScaleWorkloads() {
|
|
|
4278
4400
|
},
|
|
4279
4401
|
);
|
|
4280
4402
|
if (!response.ok) {
|
|
4281
|
-
const error = await response
|
|
4282
|
-
.json()
|
|
4283
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
4403
|
+
const error = await readErrorBody(response);
|
|
4284
4404
|
throw new Error(
|
|
4285
4405
|
`${namespace}/${name}: ${error.error || `HTTP ${response.status}`}`,
|
|
4286
4406
|
);
|
|
@@ -4464,7 +4584,7 @@ export function usePreviewResources() {
|
|
|
4464
4584
|
body: JSON.stringify(request),
|
|
4465
4585
|
})
|
|
4466
4586
|
if (!response.ok) {
|
|
4467
|
-
const error = await response
|
|
4587
|
+
const error = await readErrorBody(response)
|
|
4468
4588
|
throw new Error(error.error || `HTTP ${response.status}`)
|
|
4469
4589
|
}
|
|
4470
4590
|
return response.json() as Promise<YamlPreviewResponse>
|
|
@@ -4514,9 +4634,7 @@ export function useApplyResource() {
|
|
|
4514
4634
|
body: yaml,
|
|
4515
4635
|
});
|
|
4516
4636
|
if (!response.ok) {
|
|
4517
|
-
const error = (await response
|
|
4518
|
-
.json()
|
|
4519
|
-
.catch(() => ({ error: 'Unknown error' }))) as ApplyResourceErrorResponse
|
|
4637
|
+
const error = (await readErrorBody(response)) as ApplyResourceErrorResponse
|
|
4520
4638
|
throw new ApplyResourceError(error, response.status)
|
|
4521
4639
|
}
|
|
4522
4640
|
return response.json() as Promise<ApplyResourceResult[]>;
|
|
@@ -4572,9 +4690,7 @@ export function useTriggerCronJob() {
|
|
|
4572
4690
|
},
|
|
4573
4691
|
);
|
|
4574
4692
|
if (!response.ok) {
|
|
4575
|
-
const error = await response
|
|
4576
|
-
.json()
|
|
4577
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
4693
|
+
const error = await readErrorBody(response);
|
|
4578
4694
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
4579
4695
|
}
|
|
4580
4696
|
return response.json();
|
|
@@ -4612,9 +4728,7 @@ export function useSuspendCronJob() {
|
|
|
4612
4728
|
},
|
|
4613
4729
|
);
|
|
4614
4730
|
if (!response.ok) {
|
|
4615
|
-
const error = await response
|
|
4616
|
-
.json()
|
|
4617
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
4731
|
+
const error = await readErrorBody(response);
|
|
4618
4732
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
4619
4733
|
}
|
|
4620
4734
|
return response.json();
|
|
@@ -4652,9 +4766,7 @@ export function useResumeCronJob() {
|
|
|
4652
4766
|
},
|
|
4653
4767
|
);
|
|
4654
4768
|
if (!response.ok) {
|
|
4655
|
-
const error = await response
|
|
4656
|
-
.json()
|
|
4657
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
4769
|
+
const error = await readErrorBody(response);
|
|
4658
4770
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
4659
4771
|
}
|
|
4660
4772
|
return response.json();
|
|
@@ -4698,9 +4810,7 @@ export function useRestartWorkload() {
|
|
|
4698
4810
|
},
|
|
4699
4811
|
);
|
|
4700
4812
|
if (!response.ok) {
|
|
4701
|
-
const error = await response
|
|
4702
|
-
.json()
|
|
4703
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
4813
|
+
const error = await readErrorBody(response);
|
|
4704
4814
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
4705
4815
|
}
|
|
4706
4816
|
return response.json();
|
|
@@ -4827,9 +4937,7 @@ export function useScaleWorkload() {
|
|
|
4827
4937
|
},
|
|
4828
4938
|
);
|
|
4829
4939
|
if (!response.ok) {
|
|
4830
|
-
const error = await response
|
|
4831
|
-
.json()
|
|
4832
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
4940
|
+
const error = await readErrorBody(response);
|
|
4833
4941
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
4834
4942
|
}
|
|
4835
4943
|
return response.json();
|
|
@@ -4910,9 +5018,7 @@ export function useRollbackWorkload() {
|
|
|
4910
5018
|
},
|
|
4911
5019
|
);
|
|
4912
5020
|
if (!response.ok) {
|
|
4913
|
-
const error = await response
|
|
4914
|
-
.json()
|
|
4915
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
5021
|
+
const error = await readErrorBody(response);
|
|
4916
5022
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
4917
5023
|
}
|
|
4918
5024
|
return response.json();
|
|
@@ -5052,9 +5158,7 @@ export function useRolloutAction(options?: { reportErrors?: boolean }) {
|
|
|
5052
5158
|
{ method: "POST" },
|
|
5053
5159
|
);
|
|
5054
5160
|
if (!response.ok) {
|
|
5055
|
-
const error = await response
|
|
5056
|
-
.json()
|
|
5057
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
5161
|
+
const error = await readErrorBody(response);
|
|
5058
5162
|
// The code, not the status, says whether retrying can help: a lost cluster
|
|
5059
5163
|
// connection answers 503 on this route too.
|
|
5060
5164
|
throw new ApiError(
|
|
@@ -5112,9 +5216,7 @@ export function useCordonNode() {
|
|
|
5112
5216
|
method: "POST",
|
|
5113
5217
|
});
|
|
5114
5218
|
if (!response.ok) {
|
|
5115
|
-
const error = await response
|
|
5116
|
-
.json()
|
|
5117
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
5219
|
+
const error = await readErrorBody(response);
|
|
5118
5220
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
5119
5221
|
}
|
|
5120
5222
|
return response.json();
|
|
@@ -5145,9 +5247,7 @@ export function useUncordonNode() {
|
|
|
5145
5247
|
},
|
|
5146
5248
|
);
|
|
5147
5249
|
if (!response.ok) {
|
|
5148
|
-
const error = await response
|
|
5149
|
-
.json()
|
|
5150
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
5250
|
+
const error = await readErrorBody(response);
|
|
5151
5251
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
5152
5252
|
}
|
|
5153
5253
|
return response.json();
|
|
@@ -5189,51 +5289,27 @@ export function drainPlanBody(options: DrainPlanRequestOptions): string {
|
|
|
5189
5289
|
});
|
|
5190
5290
|
}
|
|
5191
5291
|
|
|
5192
|
-
/**
|
|
5193
|
-
* The connected radar predates the drain-plan endpoint. Distinguished from a
|
|
5194
|
-
* node-not-found 404 by the body: handlers answer with a JSON error envelope,
|
|
5195
|
-
* while an unknown route gets the router's plain-text 404. Hosts serving a newer
|
|
5196
|
-
* frontend against an older radar (Radar Hub) use this to fall back to the
|
|
5197
|
-
* plan-less drain dialog instead of leaving Drain permanently disabled.
|
|
5198
|
-
*/
|
|
5199
|
-
export class DrainPlanUnsupportedError extends Error {
|
|
5200
|
-
constructor() {
|
|
5201
|
-
super("This radar does not support drain plans");
|
|
5202
|
-
this.name = "DrainPlanUnsupportedError";
|
|
5203
|
-
}
|
|
5204
|
-
}
|
|
5205
|
-
|
|
5206
|
-
export function drainPlanFetchError(
|
|
5207
|
-
status: number,
|
|
5208
|
-
body: { error?: string } | null,
|
|
5209
|
-
): Error {
|
|
5210
|
-
if (status === 404 && body === null) {
|
|
5211
|
-
return new DrainPlanUnsupportedError();
|
|
5212
|
-
}
|
|
5213
|
-
return new Error(body?.error || `HTTP ${status}`);
|
|
5214
|
-
}
|
|
5215
|
-
|
|
5216
5292
|
// Read-only drain plan: what a drain with these options would do to each pod on the node.
|
|
5217
5293
|
// Modelled as a mutation because it is a POST with a body and is fetched on demand
|
|
5218
5294
|
// while the drain dialog is open; it performs no cluster mutation.
|
|
5295
|
+
// An older Radar without the endpoint fails with RadarFeatureUnsupportedError,
|
|
5296
|
+
// which hosts (Radar Hub) use to fall back to the plan-less drain dialog
|
|
5297
|
+
// instead of leaving Drain permanently disabled.
|
|
5219
5298
|
export function useDrainPlan() {
|
|
5299
|
+
const { guard } = useRadarFeature("drainPlan");
|
|
5220
5300
|
return useMutation<
|
|
5221
5301
|
DrainPlan,
|
|
5222
5302
|
Error,
|
|
5223
5303
|
{ name: string; options: DrainPlanRequestOptions }
|
|
5224
5304
|
>({
|
|
5225
|
-
mutationFn:
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
throw drainPlanFetchError(response.status, body);
|
|
5234
|
-
}
|
|
5235
|
-
return response.json();
|
|
5236
|
-
},
|
|
5305
|
+
mutationFn: ({ name, options }) =>
|
|
5306
|
+
guard(() =>
|
|
5307
|
+
fetchJSON<DrainPlan>(drainPlanPath(name), {
|
|
5308
|
+
method: "POST",
|
|
5309
|
+
headers: { "Content-Type": "application/json" },
|
|
5310
|
+
body: drainPlanBody(options),
|
|
5311
|
+
}),
|
|
5312
|
+
),
|
|
5237
5313
|
// Deliberately no meta toast keys: the plan is fetched only from the drain dialog, which
|
|
5238
5314
|
// renders a failure inline and keeps Drain disabled while it shows; a global toast would
|
|
5239
5315
|
// report the same failure twice.
|
|
@@ -5257,9 +5333,7 @@ export function useDrainNode() {
|
|
|
5257
5333
|
body: options ? JSON.stringify(options) : undefined,
|
|
5258
5334
|
});
|
|
5259
5335
|
if (!response.ok) {
|
|
5260
|
-
const error = await response
|
|
5261
|
-
.json()
|
|
5262
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
5336
|
+
const error = await readErrorBody(response);
|
|
5263
5337
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
5264
5338
|
}
|
|
5265
5339
|
return response.json();
|
|
@@ -5394,9 +5468,7 @@ export function useHelmManifest(
|
|
|
5394
5468
|
`${getApiBase()}/helm/releases/${namespace}/${name}/manifest${params}`,
|
|
5395
5469
|
);
|
|
5396
5470
|
if (!response.ok) {
|
|
5397
|
-
const error = await response
|
|
5398
|
-
.json()
|
|
5399
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
5471
|
+
const error = await readErrorBody(response);
|
|
5400
5472
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
5401
5473
|
}
|
|
5402
5474
|
return response.text();
|
|
@@ -5640,9 +5712,7 @@ export function useHelmRollback() {
|
|
|
5640
5712
|
},
|
|
5641
5713
|
);
|
|
5642
5714
|
if (!response.ok) {
|
|
5643
|
-
const error = await response
|
|
5644
|
-
.json()
|
|
5645
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
5715
|
+
const error = await readErrorBody(response);
|
|
5646
5716
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
5647
5717
|
}
|
|
5648
5718
|
return response.json();
|
|
@@ -5679,9 +5749,7 @@ export function useHelmUninstall() {
|
|
|
5679
5749
|
},
|
|
5680
5750
|
);
|
|
5681
5751
|
if (!response.ok) {
|
|
5682
|
-
const error = await response
|
|
5683
|
-
.json()
|
|
5684
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
5752
|
+
const error = await readErrorBody(response);
|
|
5685
5753
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
5686
5754
|
}
|
|
5687
5755
|
return response.json();
|
|
@@ -5713,9 +5781,7 @@ function streamHelmProgress(
|
|
|
5713
5781
|
fetch(url, { credentials: getCredentialsMode(), ...options, headers })
|
|
5714
5782
|
.then(async (response) => {
|
|
5715
5783
|
if (!response.ok) {
|
|
5716
|
-
const error = await response
|
|
5717
|
-
.json()
|
|
5718
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
5784
|
+
const error = await readErrorBody(response);
|
|
5719
5785
|
reject(new Error(error.error || `HTTP ${response.status}`));
|
|
5720
5786
|
return;
|
|
5721
5787
|
}
|
|
@@ -5835,9 +5901,7 @@ export function useHelmPreviewValues() {
|
|
|
5835
5901
|
},
|
|
5836
5902
|
);
|
|
5837
5903
|
if (!response.ok) {
|
|
5838
|
-
const error = await response
|
|
5839
|
-
.json()
|
|
5840
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
5904
|
+
const error = await readErrorBody(response);
|
|
5841
5905
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
5842
5906
|
}
|
|
5843
5907
|
return response.json();
|
|
@@ -5868,9 +5932,7 @@ export function useHelmApplyValues() {
|
|
|
5868
5932
|
},
|
|
5869
5933
|
);
|
|
5870
5934
|
if (!response.ok) {
|
|
5871
|
-
const error = await response
|
|
5872
|
-
.json()
|
|
5873
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
5935
|
+
const error = await readErrorBody(response);
|
|
5874
5936
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
5875
5937
|
}
|
|
5876
5938
|
return response.json();
|
|
@@ -5915,9 +5977,7 @@ async function updateRepositoryFn(repoName: string): Promise<unknown> {
|
|
|
5915
5977
|
},
|
|
5916
5978
|
);
|
|
5917
5979
|
if (!response.ok) {
|
|
5918
|
-
const error = await response
|
|
5919
|
-
.json()
|
|
5920
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
5980
|
+
const error = await readErrorBody(response);
|
|
5921
5981
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
5922
5982
|
}
|
|
5923
5983
|
return response.json();
|
|
@@ -5975,9 +6035,7 @@ async function mutateOCISource(
|
|
|
5975
6035
|
body: JSON.stringify({ source }),
|
|
5976
6036
|
});
|
|
5977
6037
|
if (!response.ok) {
|
|
5978
|
-
const error = await response
|
|
5979
|
-
.json()
|
|
5980
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
6038
|
+
const error = await readErrorBody(response);
|
|
5981
6039
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
5982
6040
|
}
|
|
5983
6041
|
return response.json();
|
|
@@ -6066,9 +6124,7 @@ export function useInstallChart() {
|
|
|
6066
6124
|
body: JSON.stringify(req),
|
|
6067
6125
|
});
|
|
6068
6126
|
if (!response.ok) {
|
|
6069
|
-
const error = await response
|
|
6070
|
-
.json()
|
|
6071
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
6127
|
+
const error = await readErrorBody(response);
|
|
6072
6128
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
6073
6129
|
}
|
|
6074
6130
|
return response.json() as Promise<HelmRelease>;
|
|
@@ -6211,9 +6267,7 @@ function createGitOpsMutation<TVariables>(
|
|
|
6211
6267
|
},
|
|
6212
6268
|
);
|
|
6213
6269
|
if (!response.ok) {
|
|
6214
|
-
const error = await response
|
|
6215
|
-
.json()
|
|
6216
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
6270
|
+
const error = await readErrorBody(response);
|
|
6217
6271
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
6218
6272
|
}
|
|
6219
6273
|
return response.json() as Promise<GitOpsOperationResponse>;
|
|
@@ -6394,9 +6448,7 @@ export function useArgoResourceValidation() {
|
|
|
6394
6448
|
},
|
|
6395
6449
|
);
|
|
6396
6450
|
if (!response.ok) {
|
|
6397
|
-
const error = await response
|
|
6398
|
-
.json()
|
|
6399
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
6451
|
+
const error = await readErrorBody(response);
|
|
6400
6452
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
6401
6453
|
}
|
|
6402
6454
|
return response.json() as Promise<ArgoResourceValidationResult>;
|
|
@@ -6460,9 +6512,7 @@ export function useArgoRefresh() {
|
|
|
6460
6512
|
},
|
|
6461
6513
|
);
|
|
6462
6514
|
if (!response.ok) {
|
|
6463
|
-
const error = await response
|
|
6464
|
-
.json()
|
|
6465
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
6515
|
+
const error = await readErrorBody(response);
|
|
6466
6516
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
6467
6517
|
}
|
|
6468
6518
|
return response.json();
|
|
@@ -6546,9 +6596,7 @@ export function useSwitchContext() {
|
|
|
6546
6596
|
clearTimeout(timeoutId);
|
|
6547
6597
|
|
|
6548
6598
|
if (!response.ok) {
|
|
6549
|
-
const error = await response
|
|
6550
|
-
.json()
|
|
6551
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
6599
|
+
const error = await readErrorBody(response);
|
|
6552
6600
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
6553
6601
|
}
|
|
6554
6602
|
return response.json();
|
|
@@ -6675,9 +6723,7 @@ export function useSetActiveNamespace() {
|
|
|
6675
6723
|
durationMs: Math.round(performance.now() - startedAt),
|
|
6676
6724
|
});
|
|
6677
6725
|
if (!response.ok) {
|
|
6678
|
-
const error = await response
|
|
6679
|
-
.json()
|
|
6680
|
-
.catch(() => ({ error: "Unknown error" }));
|
|
6726
|
+
const error = await readErrorBody(response);
|
|
6681
6727
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
6682
6728
|
}
|
|
6683
6729
|
return response.json();
|