@skyhook-io/radar-app 1.15.0 → 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.
Files changed (89) hide show
  1. package/README.md +2 -0
  2. package/package.json +6 -5
  3. package/src/App.tsx +14 -11
  4. package/src/RadarApp.tsx +51 -21
  5. package/src/api/apiResources.test.ts +19 -1
  6. package/src/api/apiResources.ts +18 -3
  7. package/src/api/client.capacity.test.ts +15 -0
  8. package/src/api/client.trace.test.ts +9 -1
  9. package/src/api/client.ts +232 -201
  10. package/src/api/diagnose.test.ts +102 -1
  11. package/src/api/diagnose.ts +42 -11
  12. package/src/api/drain-plan.test.ts +0 -10
  13. package/src/api/httpErrors.test.ts +95 -0
  14. package/src/api/httpErrors.ts +101 -0
  15. package/src/api/policy.ts +33 -5
  16. package/src/api/radarFeatures.render.test.tsx +190 -0
  17. package/src/api/radarFeatures.test.ts +133 -0
  18. package/src/api/radarFeatures.ts +147 -0
  19. package/src/api/timelineSource.test.ts +2 -10
  20. package/src/api/timelineSource.ts +3 -2
  21. package/src/api/traffic.ts +4 -3
  22. package/src/components/CloudFunnelButton.tsx +9 -9
  23. package/src/components/NamespaceSwitcher.test.tsx +47 -0
  24. package/src/components/NamespaceSwitcher.tsx +17 -1
  25. package/src/components/SelfManagedStart.tsx +5 -6
  26. package/src/components/applications/ApplicationsView.tsx +20 -6
  27. package/src/components/audit/UpgradeReadinessView.test.ts +14 -6
  28. package/src/components/audit/UpgradeReadinessView.tsx +9 -13
  29. package/src/components/capacity/CapacityView.test.tsx +13 -4
  30. package/src/components/capacity/shared.tsx +14 -13
  31. package/src/components/cost/ApplicationCostTab.tsx +9 -1
  32. package/src/components/cost/CostConnectionAction.tsx +30 -0
  33. package/src/components/cost/CostTrendChart.tsx +2 -0
  34. package/src/components/cost/CostView.tsx +4 -24
  35. package/src/components/cost/WorkloadCostTab.tsx +7 -1
  36. package/src/components/cost/source.test.ts +30 -3
  37. package/src/components/cost/source.ts +37 -2
  38. package/src/components/curl/ServiceCurlButton.tsx +7 -3
  39. package/src/components/dock/NodeTerminalTab.tsx +2 -1
  40. package/src/components/dock/TerminalTab.tsx +2 -1
  41. package/src/components/dock/TrafficFlowListTab.tsx +2 -2
  42. package/src/components/gitops/GitOpsView.tsx +12 -0
  43. package/src/components/home/HomeView.tsx +1 -1
  44. package/src/components/home/MCPSetupDialog.tsx +37 -37
  45. package/src/components/resource/PVCUsageBar.render.test.tsx +16 -0
  46. package/src/components/resource/PVCUsageBar.tsx +4 -1
  47. package/src/components/resource/PrometheusChartsGrid.render.test.tsx +1 -0
  48. package/src/components/resource/PrometheusChartsGrid.tsx +4 -0
  49. package/src/components/resources/ImageFilesystemModal.tsx +2 -1
  50. package/src/components/resources/PodFilePreview.tsx +4 -1
  51. package/src/components/resources/PodFilesystemModal.tsx +3 -2
  52. package/src/components/resources/ResourcesView.tsx +2 -1
  53. package/src/components/rightsizing/RightsizingScanView.tsx +4 -1
  54. package/src/components/settings/ConnectionHeadersEditor.tsx +109 -0
  55. package/src/components/settings/FormSaveActions.tsx +76 -0
  56. package/src/components/settings/IntegrationConnectionForms.tsx +391 -0
  57. package/src/components/settings/LocalConfigurationDetails.tsx +389 -0
  58. package/src/components/settings/LocalConnectionSettings.test.tsx +189 -0
  59. package/src/components/settings/LocalConnectionSettings.tsx +1015 -0
  60. package/src/components/settings/LocalIntegrationStatus.tsx +50 -0
  61. package/src/components/settings/PrometheusConfigField.tsx +30 -0
  62. package/src/components/settings/PrometheusConnectionForm.tsx +295 -0
  63. package/src/components/settings/SettingsDialog.tsx +382 -521
  64. package/src/components/settings/currency-options.test.ts +1 -1
  65. package/src/components/settings/currency-options.ts +1 -1
  66. package/src/components/settings/settings-state.test.ts +17 -10
  67. package/src/components/settings/settings-state.ts +20 -12
  68. package/src/components/timeline/TimelineList.test.ts +56 -0
  69. package/src/components/timeline/TimelineList.tsx +7 -1
  70. package/src/components/traffic/TrafficFlowList.tsx +5 -21
  71. package/src/components/traffic/TrafficFlowListContext.tsx +6 -4
  72. package/src/components/traffic/TrafficGraph.tsx +79 -48
  73. package/src/components/traffic/TrafficView.tsx +61 -37
  74. package/src/components/traffic/TrafficWizard.tsx +4 -2
  75. package/src/components/traffic/trafficFilters.test.ts +154 -1
  76. package/src/components/traffic/trafficFilters.ts +141 -0
  77. package/src/components/ui/Omnibar.tsx +1 -1
  78. package/src/components/ui/UpdateNotification.tsx +57 -12
  79. package/src/components/workload/WorkloadView.tsx +36 -13
  80. package/src/components/workload/historyPaging.test.ts +72 -0
  81. package/src/components/workload/historyPaging.ts +147 -0
  82. package/src/components/workload/useHistoryPaging.test.tsx +81 -0
  83. package/src/context/ConnectionContext.tsx +2 -1
  84. package/src/context/RadarUpgrade.tsx +30 -0
  85. package/src/context/RadarUpgradeHost.tsx +23 -0
  86. package/src/hooks/usePreviousIntegrationSettings.render.test.tsx +62 -0
  87. package/src/hooks/usePreviousIntegrationSettings.test.ts +62 -0
  88. package/src/hooks/usePreviousIntegrationSettings.ts +89 -0
  89. 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
- constructor(message: string, status: number, data?: Record<string, unknown>) {
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 errorData = await response
340
- .json()
341
- .catch(() => ({ error: "Unknown error" }));
357
+ const { body, unknownRoute } = await readErrorResponse(response);
342
358
  throw new ApiError(
343
- errorData.error || `HTTP ${response.status}`,
359
+ body.error || httpStatusMessage(response.status, response.statusText),
344
360
  response.status,
345
- errorData,
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) returns
836
- // HTML - an unguarded .json() surfaced "Unexpected token <" instead of the
837
- // status. Mirror fetchJSON's tolerant parse.
838
- const body = await response.json().catch(() => undefined)
839
- if (!response.ok || body?.error) {
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
- fetchJSON<CapacityOverviewResponse>(
1406
- `/capacity${capacityPageQuery(options)}`,
1407
- signal,
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
- fetchJSON<CapacityPoolListResponse>(
1425
- `/capacity/pools${capacityPageQuery(options)}`,
1426
- signal,
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
- fetchJSON<CapacityPoolDetailResponse>(
1448
- `/capacity/pools/${encodeURIComponent(name ?? "")}${capacityPageQuery(options)}`,
1449
- signal,
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
- fetchJSON<CapacityMemberListResponse>(
1477
- `/capacity/pools/${encodeURIComponent(name ?? "")}/members${pageQuery}${separator}type=${encodeURIComponent(type)}`,
1478
- signal,
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
- fetchJSON<CapacityDemandResponse>(
1532
- `/capacity/demand${query ? `?${query}` : ""}`,
1533
- signal,
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
- fetchJSON<CapacityActivityResponse>(
1577
- `/capacity/activity${query ? `?${query}` : ""}`,
1578
- signal,
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,36 @@ 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
+ gatedKey: support === 'unsupported' ? ['radar-feature-unsupported'] : [],
1786
+ guard: <T,>(request: () => Promise<T>): Promise<T> =>
1787
+ guardRadarFeature(feature, support, { currentVersion, latestVersion }, request),
1788
+ }
1789
+ }
1790
+
1713
1791
  // ============================================================================
1714
1792
  // Desktop Update API hooks
1715
1793
  // ============================================================================
@@ -1731,9 +1809,7 @@ export function useStartDesktopUpdate() {
1731
1809
  method: "POST",
1732
1810
  });
1733
1811
  if (!response.ok) {
1734
- const error = await response
1735
- .json()
1736
- .catch(() => ({ error: "Unknown error" }));
1812
+ const error = await readErrorBody(response);
1737
1813
  throw new Error(error.error || `HTTP ${response.status}`);
1738
1814
  }
1739
1815
  return response.json();
@@ -1761,9 +1837,7 @@ export function useApplyDesktopUpdate() {
1761
1837
  method: "POST",
1762
1838
  });
1763
1839
  if (!response.ok) {
1764
- const error = await response
1765
- .json()
1766
- .catch(() => ({ error: "Unknown error" }));
1840
+ const error = await readErrorBody(response);
1767
1841
  throw new Error(error.error || `HTTP ${response.status}`);
1768
1842
  }
1769
1843
  return response.json();
@@ -2320,15 +2394,24 @@ export function useApplications(
2320
2394
  const queryString = params.toString();
2321
2395
 
2322
2396
  const enabled = options?.enabled !== false;
2397
+ const { guard, gatedKey } = useRadarFeature("applications");
2323
2398
  return useQuery<{ applications: AppRow[] }>({
2324
- queryKey: ["applications", namespaces],
2399
+ queryKey: ["applications", namespaces, ...gatedKey],
2325
2400
  queryFn: () =>
2326
- fetchJSON(`/applications${queryString ? `?${queryString}` : ""}`),
2401
+ guard(() =>
2402
+ fetchJSON<{ applications: AppRow[] }>(
2403
+ `/applications${queryString ? `?${queryString}` : ""}`,
2404
+ ),
2405
+ ),
2327
2406
  staleTime: 30_000,
2407
+ retry: shouldRetryRadarQuery,
2328
2408
  // Only poll while a consumer needs the index; gated off it must not keep the
2329
- // background refetch alive.
2409
+ // background refetch alive. A Radar that predates the endpoint never will.
2330
2410
  enabled,
2331
- refetchInterval: enabled ? APPLICATIONS_REFRESH_INTERVAL_MS : false,
2411
+ refetchInterval: (query) =>
2412
+ enabled && !(query.state.error instanceof RadarFeatureUnsupportedError)
2413
+ ? APPLICATIONS_REFRESH_INTERVAL_MS
2414
+ : false,
2332
2415
  });
2333
2416
  }
2334
2417
 
@@ -2520,7 +2603,7 @@ export function useResource<T>(
2520
2603
  retry: (failureCount, error) => {
2521
2604
  if (isStillLoadingError(error)) return true;
2522
2605
  if (isKindSyncFailed(error)) return false;
2523
- return failureCount < 1; // matches the QueryClient default (retry: 1)
2606
+ return failureCount < 1; // one retry, 4xx included, unlike the QueryClient default
2524
2607
  },
2525
2608
  retryDelay: (failureCount, error) =>
2526
2609
  isStillLoadingError(error) ? 2000 : Math.min(1000 * 2 ** failureCount, 30000),
@@ -2552,7 +2635,7 @@ export function useResourceWithRelationships<T>(
2552
2635
  retry: (failureCount, error) => {
2553
2636
  if (isStillLoadingError(error)) return true;
2554
2637
  if (isKindSyncFailed(error)) return false;
2555
- return failureCount < 1; // matches the QueryClient default (retry: 1)
2638
+ return failureCount < 1; // one retry, 4xx included, unlike the QueryClient default
2556
2639
  },
2557
2640
  retryDelay: (failureCount, error) =>
2558
2641
  isStillLoadingError(error) ? 2000 : Math.min(1000 * 2 ** failureCount, 30000),
@@ -2584,7 +2667,7 @@ export function useResources<T>(
2584
2667
  retry: (failureCount, error) => {
2585
2668
  if (isStillLoadingError(error)) return true;
2586
2669
  if (isKindSyncFailed(error)) return false;
2587
- return failureCount < 1; // matches the QueryClient default (retry: 1)
2670
+ return failureCount < 1; // one retry, 4xx included, unlike the QueryClient default
2588
2671
  },
2589
2672
  retryDelay: (failureCount, error) =>
2590
2673
  isStillLoadingError(error) ? 2000 : Math.min(1000 * 2 ** failureCount, 30000),
@@ -2652,9 +2735,7 @@ async function fetchChangesPage(
2652
2735
  signal ? { signal } : undefined,
2653
2736
  );
2654
2737
  if (!response.ok) {
2655
- const errorData = await response
2656
- .json()
2657
- .catch(() => ({ error: "Unknown error" }));
2738
+ const errorData = await readErrorBody(response);
2658
2739
  throw new ApiError(
2659
2740
  errorData.error || `HTTP ${response.status}`,
2660
2741
  response.status,
@@ -2846,6 +2927,39 @@ export function useChanges(options: UseChangesOptions = {}) {
2846
2927
  });
2847
2928
  }
2848
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
+
2849
2963
  export interface ResourceEventsResult {
2850
2964
  k8sEvents: TimelineEvent[];
2851
2965
  updates: TimelineEvent[];
@@ -2995,11 +3109,13 @@ export function usePodMetrics(
2995
3109
  }
2996
3110
 
2997
3111
  export function usePodEnvironment(namespace: string, podName: string, enabled = true) {
3112
+ const { guard, gatedKey } = useRadarFeature('podEnvironment')
2998
3113
  return useQuery<PodEnvironmentResponse>({
2999
- queryKey: ['pod-environment', namespace, podName],
3000
- queryFn: () => fetchJSON(
3114
+ queryKey: ['pod-environment', namespace, podName, ...gatedKey],
3115
+ queryFn: () => guard(() => fetchJSON<PodEnvironmentResponse>(
3001
3116
  `/pods/${encodeURIComponent(namespace)}/${encodeURIComponent(podName)}/environment`,
3002
- ),
3117
+ )),
3118
+ retry: shouldRetryRadarQuery,
3003
3119
  enabled: enabled && Boolean(namespace && podName),
3004
3120
  staleTime: 10000,
3005
3121
  })
@@ -3423,9 +3539,10 @@ export function prometheusStatusRefetchInterval(
3423
3539
  }
3424
3540
 
3425
3541
  // Check Prometheus availability
3426
- export function usePrometheusStatus() {
3542
+ export function usePrometheusStatus(enabled = true) {
3427
3543
  return useQuery<PrometheusStatus>({
3428
3544
  queryKey: ["prometheus-status"],
3545
+ enabled,
3429
3546
  queryFn: () => fetchJSON("/prometheus/status"),
3430
3547
  staleTime: 30000,
3431
3548
  refetchInterval: (query) =>
@@ -3464,9 +3581,7 @@ export function usePrometheusConnect() {
3464
3581
  method: "POST",
3465
3582
  });
3466
3583
  if (!resp.ok) {
3467
- const body = await resp
3468
- .json()
3469
- .catch(() => ({ error: "Unknown error" }));
3584
+ const body = await readErrorBody(resp);
3470
3585
  throw new Error(body.error || `HTTP ${resp.status}`);
3471
3586
  }
3472
3587
  return resp.json() as Promise<PrometheusStatus>;
@@ -3961,9 +4076,7 @@ export function useUpdateResource() {
3961
4076
  body: yaml,
3962
4077
  });
3963
4078
  if (!response.ok) {
3964
- const error = await response
3965
- .json()
3966
- .catch(() => ({ error: "Unknown error" }));
4079
+ const error = await readErrorBody(response);
3967
4080
  throw new Error(error.error || `HTTP ${response.status}`);
3968
4081
  }
3969
4082
  return response.json();
@@ -4075,9 +4188,7 @@ export function useDeleteResource() {
4075
4188
  method: "DELETE",
4076
4189
  });
4077
4190
  if (!response.ok) {
4078
- const error = await response
4079
- .json()
4080
- .catch(() => ({ error: "Unknown error" }));
4191
+ const error = await readErrorBody(response);
4081
4192
  throw new Error(error.error || `HTTP ${response.status}`);
4082
4193
  }
4083
4194
  // DELETE returns 204 No Content, no body to parse
@@ -4122,9 +4233,7 @@ export function useBulkDeleteResources() {
4122
4233
  if (force) url.searchParams.set("force", "true");
4123
4234
  const response = await apiFetch(url.toString(), { method: "DELETE" });
4124
4235
  if (!response.ok) {
4125
- const error = await response
4126
- .json()
4127
- .catch(() => ({ error: "Unknown error" }));
4236
+ const error = await readErrorBody(response);
4128
4237
  throw new Error(
4129
4238
  error.error || `Failed to delete ${namespace}/${name}`,
4130
4239
  );
@@ -4206,9 +4315,7 @@ export function useBulkRestartWorkloads() {
4206
4315
  },
4207
4316
  );
4208
4317
  if (!response.ok) {
4209
- const error = await response
4210
- .json()
4211
- .catch(() => ({ error: "Unknown error" }));
4318
+ const error = await readErrorBody(response);
4212
4319
  throw new Error(
4213
4320
  `${namespace}/${name}: ${error.error || `HTTP ${response.status}`}`,
4214
4321
  );
@@ -4278,9 +4385,7 @@ export function useBulkScaleWorkloads() {
4278
4385
  },
4279
4386
  );
4280
4387
  if (!response.ok) {
4281
- const error = await response
4282
- .json()
4283
- .catch(() => ({ error: "Unknown error" }));
4388
+ const error = await readErrorBody(response);
4284
4389
  throw new Error(
4285
4390
  `${namespace}/${name}: ${error.error || `HTTP ${response.status}`}`,
4286
4391
  );
@@ -4464,7 +4569,7 @@ export function usePreviewResources() {
4464
4569
  body: JSON.stringify(request),
4465
4570
  })
4466
4571
  if (!response.ok) {
4467
- const error = await response.json().catch(() => ({ error: 'Preview failed' }))
4572
+ const error = await readErrorBody(response)
4468
4573
  throw new Error(error.error || `HTTP ${response.status}`)
4469
4574
  }
4470
4575
  return response.json() as Promise<YamlPreviewResponse>
@@ -4514,9 +4619,7 @@ export function useApplyResource() {
4514
4619
  body: yaml,
4515
4620
  });
4516
4621
  if (!response.ok) {
4517
- const error = (await response
4518
- .json()
4519
- .catch(() => ({ error: 'Unknown error' }))) as ApplyResourceErrorResponse
4622
+ const error = (await readErrorBody(response)) as ApplyResourceErrorResponse
4520
4623
  throw new ApplyResourceError(error, response.status)
4521
4624
  }
4522
4625
  return response.json() as Promise<ApplyResourceResult[]>;
@@ -4572,9 +4675,7 @@ export function useTriggerCronJob() {
4572
4675
  },
4573
4676
  );
4574
4677
  if (!response.ok) {
4575
- const error = await response
4576
- .json()
4577
- .catch(() => ({ error: "Unknown error" }));
4678
+ const error = await readErrorBody(response);
4578
4679
  throw new Error(error.error || `HTTP ${response.status}`);
4579
4680
  }
4580
4681
  return response.json();
@@ -4612,9 +4713,7 @@ export function useSuspendCronJob() {
4612
4713
  },
4613
4714
  );
4614
4715
  if (!response.ok) {
4615
- const error = await response
4616
- .json()
4617
- .catch(() => ({ error: "Unknown error" }));
4716
+ const error = await readErrorBody(response);
4618
4717
  throw new Error(error.error || `HTTP ${response.status}`);
4619
4718
  }
4620
4719
  return response.json();
@@ -4652,9 +4751,7 @@ export function useResumeCronJob() {
4652
4751
  },
4653
4752
  );
4654
4753
  if (!response.ok) {
4655
- const error = await response
4656
- .json()
4657
- .catch(() => ({ error: "Unknown error" }));
4754
+ const error = await readErrorBody(response);
4658
4755
  throw new Error(error.error || `HTTP ${response.status}`);
4659
4756
  }
4660
4757
  return response.json();
@@ -4698,9 +4795,7 @@ export function useRestartWorkload() {
4698
4795
  },
4699
4796
  );
4700
4797
  if (!response.ok) {
4701
- const error = await response
4702
- .json()
4703
- .catch(() => ({ error: "Unknown error" }));
4798
+ const error = await readErrorBody(response);
4704
4799
  throw new Error(error.error || `HTTP ${response.status}`);
4705
4800
  }
4706
4801
  return response.json();
@@ -4827,9 +4922,7 @@ export function useScaleWorkload() {
4827
4922
  },
4828
4923
  );
4829
4924
  if (!response.ok) {
4830
- const error = await response
4831
- .json()
4832
- .catch(() => ({ error: "Unknown error" }));
4925
+ const error = await readErrorBody(response);
4833
4926
  throw new Error(error.error || `HTTP ${response.status}`);
4834
4927
  }
4835
4928
  return response.json();
@@ -4910,9 +5003,7 @@ export function useRollbackWorkload() {
4910
5003
  },
4911
5004
  );
4912
5005
  if (!response.ok) {
4913
- const error = await response
4914
- .json()
4915
- .catch(() => ({ error: "Unknown error" }));
5006
+ const error = await readErrorBody(response);
4916
5007
  throw new Error(error.error || `HTTP ${response.status}`);
4917
5008
  }
4918
5009
  return response.json();
@@ -5052,9 +5143,7 @@ export function useRolloutAction(options?: { reportErrors?: boolean }) {
5052
5143
  { method: "POST" },
5053
5144
  );
5054
5145
  if (!response.ok) {
5055
- const error = await response
5056
- .json()
5057
- .catch(() => ({ error: "Unknown error" }));
5146
+ const error = await readErrorBody(response);
5058
5147
  // The code, not the status, says whether retrying can help: a lost cluster
5059
5148
  // connection answers 503 on this route too.
5060
5149
  throw new ApiError(
@@ -5112,9 +5201,7 @@ export function useCordonNode() {
5112
5201
  method: "POST",
5113
5202
  });
5114
5203
  if (!response.ok) {
5115
- const error = await response
5116
- .json()
5117
- .catch(() => ({ error: "Unknown error" }));
5204
+ const error = await readErrorBody(response);
5118
5205
  throw new Error(error.error || `HTTP ${response.status}`);
5119
5206
  }
5120
5207
  return response.json();
@@ -5145,9 +5232,7 @@ export function useUncordonNode() {
5145
5232
  },
5146
5233
  );
5147
5234
  if (!response.ok) {
5148
- const error = await response
5149
- .json()
5150
- .catch(() => ({ error: "Unknown error" }));
5235
+ const error = await readErrorBody(response);
5151
5236
  throw new Error(error.error || `HTTP ${response.status}`);
5152
5237
  }
5153
5238
  return response.json();
@@ -5189,51 +5274,27 @@ export function drainPlanBody(options: DrainPlanRequestOptions): string {
5189
5274
  });
5190
5275
  }
5191
5276
 
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
5277
  // Read-only drain plan: what a drain with these options would do to each pod on the node.
5217
5278
  // Modelled as a mutation because it is a POST with a body and is fetched on demand
5218
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.
5219
5283
  export function useDrainPlan() {
5284
+ const { guard } = useRadarFeature("drainPlan");
5220
5285
  return useMutation<
5221
5286
  DrainPlan,
5222
5287
  Error,
5223
5288
  { name: string; options: DrainPlanRequestOptions }
5224
5289
  >({
5225
- mutationFn: async ({ name, options }) => {
5226
- const response = await apiFetch(apiUrl(drainPlanPath(name)), {
5227
- method: "POST",
5228
- headers: { "Content-Type": "application/json" },
5229
- body: drainPlanBody(options),
5230
- });
5231
- if (!response.ok) {
5232
- const body = await response.json().catch(() => null);
5233
- throw drainPlanFetchError(response.status, body);
5234
- }
5235
- return response.json();
5236
- },
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
+ ),
5237
5298
  // Deliberately no meta toast keys: the plan is fetched only from the drain dialog, which
5238
5299
  // renders a failure inline and keeps Drain disabled while it shows; a global toast would
5239
5300
  // report the same failure twice.
@@ -5257,9 +5318,7 @@ export function useDrainNode() {
5257
5318
  body: options ? JSON.stringify(options) : undefined,
5258
5319
  });
5259
5320
  if (!response.ok) {
5260
- const error = await response
5261
- .json()
5262
- .catch(() => ({ error: "Unknown error" }));
5321
+ const error = await readErrorBody(response);
5263
5322
  throw new Error(error.error || `HTTP ${response.status}`);
5264
5323
  }
5265
5324
  return response.json();
@@ -5394,9 +5453,7 @@ export function useHelmManifest(
5394
5453
  `${getApiBase()}/helm/releases/${namespace}/${name}/manifest${params}`,
5395
5454
  );
5396
5455
  if (!response.ok) {
5397
- const error = await response
5398
- .json()
5399
- .catch(() => ({ error: "Unknown error" }));
5456
+ const error = await readErrorBody(response);
5400
5457
  throw new Error(error.error || `HTTP ${response.status}`);
5401
5458
  }
5402
5459
  return response.text();
@@ -5640,9 +5697,7 @@ export function useHelmRollback() {
5640
5697
  },
5641
5698
  );
5642
5699
  if (!response.ok) {
5643
- const error = await response
5644
- .json()
5645
- .catch(() => ({ error: "Unknown error" }));
5700
+ const error = await readErrorBody(response);
5646
5701
  throw new Error(error.error || `HTTP ${response.status}`);
5647
5702
  }
5648
5703
  return response.json();
@@ -5679,9 +5734,7 @@ export function useHelmUninstall() {
5679
5734
  },
5680
5735
  );
5681
5736
  if (!response.ok) {
5682
- const error = await response
5683
- .json()
5684
- .catch(() => ({ error: "Unknown error" }));
5737
+ const error = await readErrorBody(response);
5685
5738
  throw new Error(error.error || `HTTP ${response.status}`);
5686
5739
  }
5687
5740
  return response.json();
@@ -5713,9 +5766,7 @@ function streamHelmProgress(
5713
5766
  fetch(url, { credentials: getCredentialsMode(), ...options, headers })
5714
5767
  .then(async (response) => {
5715
5768
  if (!response.ok) {
5716
- const error = await response
5717
- .json()
5718
- .catch(() => ({ error: "Unknown error" }));
5769
+ const error = await readErrorBody(response);
5719
5770
  reject(new Error(error.error || `HTTP ${response.status}`));
5720
5771
  return;
5721
5772
  }
@@ -5835,9 +5886,7 @@ export function useHelmPreviewValues() {
5835
5886
  },
5836
5887
  );
5837
5888
  if (!response.ok) {
5838
- const error = await response
5839
- .json()
5840
- .catch(() => ({ error: "Unknown error" }));
5889
+ const error = await readErrorBody(response);
5841
5890
  throw new Error(error.error || `HTTP ${response.status}`);
5842
5891
  }
5843
5892
  return response.json();
@@ -5868,9 +5917,7 @@ export function useHelmApplyValues() {
5868
5917
  },
5869
5918
  );
5870
5919
  if (!response.ok) {
5871
- const error = await response
5872
- .json()
5873
- .catch(() => ({ error: "Unknown error" }));
5920
+ const error = await readErrorBody(response);
5874
5921
  throw new Error(error.error || `HTTP ${response.status}`);
5875
5922
  }
5876
5923
  return response.json();
@@ -5915,9 +5962,7 @@ async function updateRepositoryFn(repoName: string): Promise<unknown> {
5915
5962
  },
5916
5963
  );
5917
5964
  if (!response.ok) {
5918
- const error = await response
5919
- .json()
5920
- .catch(() => ({ error: "Unknown error" }));
5965
+ const error = await readErrorBody(response);
5921
5966
  throw new Error(error.error || `HTTP ${response.status}`);
5922
5967
  }
5923
5968
  return response.json();
@@ -5975,9 +6020,7 @@ async function mutateOCISource(
5975
6020
  body: JSON.stringify({ source }),
5976
6021
  });
5977
6022
  if (!response.ok) {
5978
- const error = await response
5979
- .json()
5980
- .catch(() => ({ error: "Unknown error" }));
6023
+ const error = await readErrorBody(response);
5981
6024
  throw new Error(error.error || `HTTP ${response.status}`);
5982
6025
  }
5983
6026
  return response.json();
@@ -6066,9 +6109,7 @@ export function useInstallChart() {
6066
6109
  body: JSON.stringify(req),
6067
6110
  });
6068
6111
  if (!response.ok) {
6069
- const error = await response
6070
- .json()
6071
- .catch(() => ({ error: "Unknown error" }));
6112
+ const error = await readErrorBody(response);
6072
6113
  throw new Error(error.error || `HTTP ${response.status}`);
6073
6114
  }
6074
6115
  return response.json() as Promise<HelmRelease>;
@@ -6211,9 +6252,7 @@ function createGitOpsMutation<TVariables>(
6211
6252
  },
6212
6253
  );
6213
6254
  if (!response.ok) {
6214
- const error = await response
6215
- .json()
6216
- .catch(() => ({ error: "Unknown error" }));
6255
+ const error = await readErrorBody(response);
6217
6256
  throw new Error(error.error || `HTTP ${response.status}`);
6218
6257
  }
6219
6258
  return response.json() as Promise<GitOpsOperationResponse>;
@@ -6394,9 +6433,7 @@ export function useArgoResourceValidation() {
6394
6433
  },
6395
6434
  );
6396
6435
  if (!response.ok) {
6397
- const error = await response
6398
- .json()
6399
- .catch(() => ({ error: "Unknown error" }));
6436
+ const error = await readErrorBody(response);
6400
6437
  throw new Error(error.error || `HTTP ${response.status}`);
6401
6438
  }
6402
6439
  return response.json() as Promise<ArgoResourceValidationResult>;
@@ -6460,9 +6497,7 @@ export function useArgoRefresh() {
6460
6497
  },
6461
6498
  );
6462
6499
  if (!response.ok) {
6463
- const error = await response
6464
- .json()
6465
- .catch(() => ({ error: "Unknown error" }));
6500
+ const error = await readErrorBody(response);
6466
6501
  throw new Error(error.error || `HTTP ${response.status}`);
6467
6502
  }
6468
6503
  return response.json();
@@ -6546,9 +6581,7 @@ export function useSwitchContext() {
6546
6581
  clearTimeout(timeoutId);
6547
6582
 
6548
6583
  if (!response.ok) {
6549
- const error = await response
6550
- .json()
6551
- .catch(() => ({ error: "Unknown error" }));
6584
+ const error = await readErrorBody(response);
6552
6585
  throw new Error(error.error || `HTTP ${response.status}`);
6553
6586
  }
6554
6587
  return response.json();
@@ -6675,9 +6708,7 @@ export function useSetActiveNamespace() {
6675
6708
  durationMs: Math.round(performance.now() - startedAt),
6676
6709
  });
6677
6710
  if (!response.ok) {
6678
- const error = await response
6679
- .json()
6680
- .catch(() => ({ error: "Unknown error" }));
6711
+ const error = await readErrorBody(response);
6681
6712
  throw new Error(error.error || `HTTP ${response.status}`);
6682
6713
  }
6683
6714
  return response.json();