@skyhook-io/radar-app 1.12.3 → 1.13.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/package.json +5 -5
- package/src/App.tsx +37 -8
- package/src/RadarApp.tsx +1 -1
- package/src/api/client.ts +38 -6
- package/src/api/diagnose.ts +45 -5
- package/src/components/cost/ApplicationCostTab.test.ts +45 -0
- package/src/components/cost/ApplicationCostTab.tsx +115 -64
- package/src/components/cost/CostTrendChart.test.ts +65 -0
- package/src/components/cost/CostTrendChart.tsx +107 -17
- package/src/components/cost/CostView.test.ts +36 -1
- package/src/components/cost/CostView.tsx +133 -51
- package/src/components/cost/CurrentAllocationUse.tsx +8 -4
- package/src/components/cost/WorkloadCostTab.test.ts +50 -0
- package/src/components/cost/WorkloadCostTab.tsx +109 -61
- package/src/components/cost/source.test.ts +33 -0
- package/src/components/cost/source.ts +100 -0
- package/src/components/diagnose/AISettings.tsx +9 -4
- package/src/components/diagnose/DiagnoseContext.tsx +216 -48
- package/src/components/diagnose/DiagnoseSurface.test.tsx +118 -5
- package/src/components/diagnose/DiagnoseSurface.tsx +190 -32
- package/src/components/diagnose/Home.tsx +93 -64
- package/src/components/diagnose/InvestigationView.tsx +190 -124
- package/src/components/diagnose/parts.test.tsx +12 -4
- package/src/components/diagnose/parts.tsx +29 -17
- package/src/components/execution/BatchExecutionView.render.test.tsx +35 -0
- package/src/components/execution/BatchExecutionView.tsx +2 -3
- package/src/components/execution/execution-definition.test.ts +19 -0
- package/src/components/execution/execution-definition.ts +2 -0
- package/src/components/gitops/GitOpsView.tsx +25 -3
- package/src/components/home/CostCard.tsx +3 -2
- package/src/components/nav/navigation.test.ts +26 -0
- package/src/components/nav/navigation.ts +10 -0
- package/src/components/rightsizing/RightsizingScanView.tsx +36 -12
- package/src/components/rightsizing/copy.test.ts +19 -0
- package/src/components/settings/SettingsDialog.tsx +666 -103
- package/src/components/settings/settings-state.test.ts +42 -0
- package/src/components/settings/settings-state.ts +39 -0
- package/src/index.css +11 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyhook-io/radar-app",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.1",
|
|
4
4
|
"description": "Radar's full web UI as a reusable React component. Used by Radar's own binary and by external consumers like Radar Cloud.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"diff": "^9.0.0",
|
|
36
36
|
"monaco-editor": "0.55.1",
|
|
37
37
|
"react-markdown": "^10.1.0",
|
|
38
|
-
"react-virtuoso": "^4.18.
|
|
38
|
+
"react-virtuoso": "^4.18.12",
|
|
39
39
|
"remark-gfm": "^4.0.1",
|
|
40
40
|
"shiki": "^4.4.2",
|
|
41
41
|
"yaml": "^2.9.0"
|
|
@@ -69,8 +69,8 @@
|
|
|
69
69
|
"eslint": "^10.8.0",
|
|
70
70
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
71
71
|
"eslint-plugin-react-refresh": "^0.5.3",
|
|
72
|
-
"globals": "^17.
|
|
73
|
-
"lucide-react": "^1.
|
|
72
|
+
"globals": "^17.11.0",
|
|
73
|
+
"lucide-react": "^1.37.0",
|
|
74
74
|
"postcss": "^8.5.26",
|
|
75
75
|
"prettier": "^3.9.5",
|
|
76
76
|
"react": "^19.2.8",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"tailwindcss": "^4.3.3",
|
|
81
81
|
"typescript": "^6.0.2",
|
|
82
82
|
"typescript-eslint": "^8.67.0",
|
|
83
|
-
"vite": "^8.2.
|
|
83
|
+
"vite": "^8.2.2"
|
|
84
84
|
},
|
|
85
85
|
"sideEffects": [
|
|
86
86
|
"*.css"
|
package/src/App.tsx
CHANGED
|
@@ -37,6 +37,7 @@ import { CloudFunnelButton } from './components/CloudFunnelButton'
|
|
|
37
37
|
import { useNavCustomization } from './context/NavCustomization'
|
|
38
38
|
import type { FleetTakeoverTarget } from './context/NavCustomization'
|
|
39
39
|
import { PrimaryNavRail } from './components/nav/PrimaryNavRail'
|
|
40
|
+
import { navigateFromPrimaryRail } from './components/nav/navigation'
|
|
40
41
|
import { useNavRailPinned } from './hooks/useNavRailPinned'
|
|
41
42
|
import { useMediaQuery } from './hooks/useMediaQuery'
|
|
42
43
|
import { ContextSwitchProvider, useContextSwitch } from './context/ContextSwitchContext'
|
|
@@ -322,7 +323,12 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
322
323
|
// The AI panel is an absolute slot in the body frame (the column under the header):
|
|
323
324
|
// it reserves a right gutter on the CONTENT only, so the navbar + nav rail stay
|
|
324
325
|
// static. contentGutter is the docked panel width (0 when closed/overlay/maximized).
|
|
325
|
-
const {
|
|
326
|
+
const {
|
|
327
|
+
open: diagnoseOpen,
|
|
328
|
+
close: closeDiagnose,
|
|
329
|
+
contentGutter,
|
|
330
|
+
maximized: diagnoseMaximized,
|
|
331
|
+
} = useDiagnoseLayout()
|
|
326
332
|
// Hand off to a host-owned URL. The host's `onHostNavigate` (Radar Cloud's
|
|
327
333
|
// cross-tree swap) navigates same-document so the chrome morphs instead of
|
|
328
334
|
// cold-booting; without it we fall back to a hard `window.location` nav.
|
|
@@ -468,12 +474,19 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
468
474
|
|
|
469
475
|
const path = view === 'home' ? '/' : `/${view}`
|
|
470
476
|
|
|
471
|
-
// Start fresh — keep only cross-view params
|
|
477
|
+
// Start fresh — keep only cross-view params, discard view-specific ones.
|
|
478
|
+
// Read the live location because Diagnose owns `ai-run` through the History
|
|
479
|
+
// API; React Router's searchParams snapshot does not update for that write.
|
|
472
480
|
const newParams = new URLSearchParams()
|
|
473
|
-
const
|
|
481
|
+
const currentParams = new URLSearchParams(window.location.search)
|
|
482
|
+
const globalNamespaces = currentParams.get('namespaces')
|
|
474
483
|
if (globalNamespaces) {
|
|
475
484
|
newParams.set('namespaces', globalNamespaces)
|
|
476
485
|
}
|
|
486
|
+
const diagnoseRun = currentParams.get('ai-run')
|
|
487
|
+
if (diagnoseRun) {
|
|
488
|
+
newParams.set('ai-run', diagnoseRun)
|
|
489
|
+
}
|
|
477
490
|
|
|
478
491
|
// Add any new params
|
|
479
492
|
if (params) {
|
|
@@ -483,7 +496,18 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
483
496
|
}
|
|
484
497
|
|
|
485
498
|
navigate({ pathname: path, search: newParams.toString() })
|
|
486
|
-
}, [navigate,
|
|
499
|
+
}, [navigate, takeover, goHost])
|
|
500
|
+
|
|
501
|
+
// The standalone rail expresses intent to leave the full-width investigation
|
|
502
|
+
// workspace. Close it before routing so the destination is immediately visible;
|
|
503
|
+
// docked investigations stay open across views as a persistent side panel.
|
|
504
|
+
const handlePrimaryNavigate = useCallback((view: ExtendedMainView) => {
|
|
505
|
+
navigateFromPrimaryRail(
|
|
506
|
+
diagnoseOpen && diagnoseMaximized,
|
|
507
|
+
closeDiagnose,
|
|
508
|
+
() => setMainView(view),
|
|
509
|
+
)
|
|
510
|
+
}, [diagnoseOpen, diagnoseMaximized, closeDiagnose, setMainView])
|
|
487
511
|
|
|
488
512
|
// Cloud (embedded) takes over the "fleet-shaped" per-cluster views with its
|
|
489
513
|
// own fleet pages scoped to this cluster — owned by the host's left rail — so
|
|
@@ -1135,9 +1159,13 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1135
1159
|
setSelectedResource(null)
|
|
1136
1160
|
setSelectedHelmRelease(null)
|
|
1137
1161
|
|
|
1138
|
-
// Reset
|
|
1139
|
-
//
|
|
1140
|
-
|
|
1162
|
+
// Reset resource-specific params while retaining the durable investigation
|
|
1163
|
+
// focus. Diagnose resolves runs by id and owns whether the focused run is
|
|
1164
|
+
// still readable after the context switch.
|
|
1165
|
+
const nextParams = new URLSearchParams()
|
|
1166
|
+
const diagnoseRun = new URLSearchParams(window.location.search).get('ai-run')
|
|
1167
|
+
if (diagnoseRun) nextParams.set('ai-run', diagnoseRun)
|
|
1168
|
+
navigate({ pathname: location.pathname, search: nextParams.toString() }, { replace: true })
|
|
1141
1169
|
|
|
1142
1170
|
// Auto-unpause so the new cluster's topology loads immediately
|
|
1143
1171
|
setTopologyPaused(false)
|
|
@@ -1658,7 +1686,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1658
1686
|
{showNavRail && (
|
|
1659
1687
|
<PrimaryNavRail
|
|
1660
1688
|
activeView={navActiveView}
|
|
1661
|
-
onNavigate={
|
|
1689
|
+
onNavigate={handlePrimaryNavigate}
|
|
1662
1690
|
pinned={navRailEffectivePinned}
|
|
1663
1691
|
onTogglePinned={toggleNavRailPinned}
|
|
1664
1692
|
showPinToggle={!railForcedSlim}
|
|
@@ -2507,6 +2535,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
2507
2535
|
open={showSettings}
|
|
2508
2536
|
initialSection={settingsSection}
|
|
2509
2537
|
onClose={() => setShowSettings(false)}
|
|
2538
|
+
onNavigateToResource={navigateToResourceList}
|
|
2510
2539
|
/>
|
|
2511
2540
|
|
|
2512
2541
|
{/* Debug overlay — dev mode, standalone only. Embedded hosts (Radar Hub)
|
package/src/RadarApp.tsx
CHANGED
|
@@ -228,7 +228,7 @@ export function RadarApp({
|
|
|
228
228
|
value={renderDiagnoseAction ?? defaultDiagnoseAction}
|
|
229
229
|
consentCopy={diagnoseConsent}
|
|
230
230
|
>
|
|
231
|
-
<DiagnoseProvider>
|
|
231
|
+
<DiagnoseProvider browserURLState={router !== "memory"}>
|
|
232
232
|
<App
|
|
233
233
|
manageDocumentTitle={manageDocumentTitle}
|
|
234
234
|
documentTitleSuffix={documentTitleSuffix}
|
package/src/api/client.ts
CHANGED
|
@@ -882,24 +882,36 @@ export interface OpenCostNamespaceCost {
|
|
|
882
882
|
|
|
883
883
|
export type CostUnavailableReason =
|
|
884
884
|
| "no_prometheus"
|
|
885
|
+
| "no_cost_source"
|
|
885
886
|
| "no_metrics"
|
|
886
887
|
| "query_error"
|
|
887
888
|
| "access_denied"
|
|
888
|
-
| "not_found"
|
|
889
|
+
| "not_found"
|
|
890
|
+
| "source_unavailable"
|
|
891
|
+
| "authentication_error"
|
|
892
|
+
| "configuration_mismatch"
|
|
893
|
+
| "deployment_configuration_error"
|
|
894
|
+
| "history_unsupported"
|
|
895
|
+
| "insufficient_history";
|
|
896
|
+
|
|
897
|
+
export type CostDataSource = "prometheus" | "kubecost";
|
|
889
898
|
|
|
890
899
|
export interface OpenCostSummary {
|
|
891
900
|
available: boolean;
|
|
892
901
|
reason?: CostUnavailableReason;
|
|
902
|
+
source?: CostDataSource;
|
|
903
|
+
dataThrough?: string;
|
|
893
904
|
currency?: string;
|
|
894
905
|
window?: string;
|
|
895
906
|
totalHourlyCost?: number;
|
|
896
907
|
totalStorageCost?: number;
|
|
897
908
|
totalIdleCost?: number;
|
|
898
909
|
clusterEfficiency?: number;
|
|
910
|
+
namespaceScope?: string[];
|
|
899
911
|
namespaces?: OpenCostNamespaceCost[];
|
|
900
912
|
}
|
|
901
913
|
|
|
902
|
-
const
|
|
914
|
+
const costDiscoveryFirstSeenAt = new Map<string, number>();
|
|
903
915
|
|
|
904
916
|
function costRefetchInterval(
|
|
905
917
|
defaultInterval: number | false = COST_REFRESH_INTERVAL_MS,
|
|
@@ -915,15 +927,18 @@ function costRefetchInterval(
|
|
|
915
927
|
}) => {
|
|
916
928
|
const data = query.state.data;
|
|
917
929
|
const queryID = `${contextName ?? "unknown"}:${query.queryHash ?? JSON.stringify(query.queryKey ?? "opencost")}`;
|
|
918
|
-
if (
|
|
930
|
+
if (
|
|
931
|
+
data?.available === false &&
|
|
932
|
+
(data.reason === "no_prometheus" || data.reason === "no_cost_source")
|
|
933
|
+
) {
|
|
919
934
|
const now = Date.now();
|
|
920
|
-
const firstSeenAt =
|
|
921
|
-
|
|
935
|
+
const firstSeenAt = costDiscoveryFirstSeenAt.get(queryID) ?? now;
|
|
936
|
+
costDiscoveryFirstSeenAt.set(queryID, firstSeenAt);
|
|
922
937
|
return now - firstSeenAt < COST_DISCOVERY_GRACE_MS
|
|
923
938
|
? COST_DISCOVERY_RETRY_INTERVAL_MS
|
|
924
939
|
: defaultInterval;
|
|
925
940
|
}
|
|
926
|
-
|
|
941
|
+
costDiscoveryFirstSeenAt.delete(queryID);
|
|
927
942
|
return defaultInterval;
|
|
928
943
|
};
|
|
929
944
|
}
|
|
@@ -963,6 +978,9 @@ export interface OpenCostWorkloadCost {
|
|
|
963
978
|
export interface OpenCostWorkloadResponse {
|
|
964
979
|
available: boolean;
|
|
965
980
|
reason?: CostUnavailableReason;
|
|
981
|
+
source?: CostDataSource;
|
|
982
|
+
window?: string;
|
|
983
|
+
dataThrough?: string;
|
|
966
984
|
currency?: string;
|
|
967
985
|
namespace: string;
|
|
968
986
|
workloads: OpenCostWorkloadCost[];
|
|
@@ -991,6 +1009,9 @@ export function useOpenCostWorkloads(
|
|
|
991
1009
|
export interface OpenCostWorkloadDetailResponse {
|
|
992
1010
|
available: boolean;
|
|
993
1011
|
reason?: CostUnavailableReason;
|
|
1012
|
+
source?: CostDataSource;
|
|
1013
|
+
window?: string;
|
|
1014
|
+
dataThrough?: string;
|
|
994
1015
|
currency?: string;
|
|
995
1016
|
namespace: string;
|
|
996
1017
|
kind: string;
|
|
@@ -1036,8 +1057,12 @@ export interface OpenCostTrendSeries {
|
|
|
1036
1057
|
export interface OpenCostTrendResponse {
|
|
1037
1058
|
available: boolean;
|
|
1038
1059
|
reason?: CostUnavailableReason;
|
|
1060
|
+
source?: CostDataSource;
|
|
1039
1061
|
currency?: string;
|
|
1040
1062
|
range: string;
|
|
1063
|
+
windowStart?: number;
|
|
1064
|
+
windowEnd?: number;
|
|
1065
|
+
dataThrough?: string;
|
|
1041
1066
|
series?: OpenCostTrendSeries[];
|
|
1042
1067
|
}
|
|
1043
1068
|
|
|
@@ -1058,6 +1083,7 @@ export function useOpenCostTrend(range_: CostTimeRange = "24h") {
|
|
|
1058
1083
|
export interface OpenCostWorkloadTrendResponse {
|
|
1059
1084
|
available: boolean;
|
|
1060
1085
|
reason?: CostUnavailableReason;
|
|
1086
|
+
source?: CostDataSource;
|
|
1061
1087
|
currency?: string;
|
|
1062
1088
|
namespace: string;
|
|
1063
1089
|
kind: string;
|
|
@@ -1131,6 +1157,9 @@ export interface OpenCostApplicationWorkloadCost extends OpenCostApplicationWork
|
|
|
1131
1157
|
export interface OpenCostApplicationCostResponse {
|
|
1132
1158
|
available: boolean;
|
|
1133
1159
|
reason?: CostUnavailableReason;
|
|
1160
|
+
source?: CostDataSource;
|
|
1161
|
+
window?: string;
|
|
1162
|
+
dataThrough?: string;
|
|
1134
1163
|
currency?: string;
|
|
1135
1164
|
partial?: boolean;
|
|
1136
1165
|
totals: OpenCostApplicationCostTotals;
|
|
@@ -1146,6 +1175,7 @@ export interface OpenCostApplicationCostTrendSeries extends OpenCostApplicationW
|
|
|
1146
1175
|
export interface OpenCostApplicationCostTrendResponse {
|
|
1147
1176
|
available: boolean;
|
|
1148
1177
|
reason?: CostUnavailableReason;
|
|
1178
|
+
source?: CostDataSource;
|
|
1149
1179
|
currency?: string;
|
|
1150
1180
|
range: string;
|
|
1151
1181
|
partial?: boolean;
|
|
@@ -1238,6 +1268,8 @@ export interface OpenCostNodeCost {
|
|
|
1238
1268
|
export interface OpenCostNodeResponse {
|
|
1239
1269
|
available: boolean;
|
|
1240
1270
|
reason?: CostUnavailableReason;
|
|
1271
|
+
source?: CostDataSource;
|
|
1272
|
+
dataThrough?: string;
|
|
1241
1273
|
currency?: string;
|
|
1242
1274
|
nodes?: OpenCostNodeCost[];
|
|
1243
1275
|
}
|
package/src/api/diagnose.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Client for the local AI-diagnose engine (OSS BYO-agent). The agent CLI runs
|
|
2
2
|
// on the user's own machine/subscription against Radar's MCP; this just starts
|
|
3
3
|
// the investigation and consumes its SSE event stream.
|
|
4
|
-
import { getApiBase, getCredentialsMode } from "./config";
|
|
4
|
+
import { getApiBase, getAuthHeaders, getCredentialsMode } from "./config";
|
|
5
5
|
|
|
6
6
|
export interface AgentInfo {
|
|
7
7
|
name: string;
|
|
@@ -81,6 +81,7 @@ export interface DiagnoseStreamEvent {
|
|
|
81
81
|
error?: string;
|
|
82
82
|
question?: string; // on "turn"
|
|
83
83
|
apply?: boolean; // on "turn"
|
|
84
|
+
actor?: string; // human author on shared hosted transcripts
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
// A run is a durable, server-owned investigation. Its lifetime is independent of
|
|
@@ -101,11 +102,17 @@ export interface RunSummary {
|
|
|
101
102
|
effort?: string;
|
|
102
103
|
managedBy?: string; // GitOps/Helm owner of the target ("Argo CD"/"Flux"/"Helm"), for the Apply warning
|
|
103
104
|
health?: ResourceHealthSignal;
|
|
104
|
-
status: "running" | "done" | "error" | "stopped" | "stale";
|
|
105
|
+
status: "running" | "stopping" | "done" | "error" | "stopped" | "stale";
|
|
105
106
|
sessionId?: string;
|
|
106
107
|
preview?: string;
|
|
107
108
|
createdAt: string;
|
|
108
109
|
updatedAt: string;
|
|
110
|
+
visibility?: "private" | "organization";
|
|
111
|
+
ownedByMe?: boolean;
|
|
112
|
+
canManageVisibility?: boolean;
|
|
113
|
+
canContinue?: boolean;
|
|
114
|
+
trigger?: "interactive" | "background";
|
|
115
|
+
radarUrl?: string;
|
|
109
116
|
}
|
|
110
117
|
|
|
111
118
|
export async function fetchAgents(
|
|
@@ -113,6 +120,7 @@ export async function fetchAgents(
|
|
|
113
120
|
): Promise<AgentsResponse> {
|
|
114
121
|
const res = await fetch(`${getApiBase()}/agents`, {
|
|
115
122
|
credentials: getCredentialsMode(),
|
|
123
|
+
headers: getAuthHeaders(),
|
|
116
124
|
signal,
|
|
117
125
|
});
|
|
118
126
|
if (!res.ok) throw new Error(`agents: ${res.status}`);
|
|
@@ -167,7 +175,7 @@ export async function createRun(
|
|
|
167
175
|
const res = await fetch(RUNS(), {
|
|
168
176
|
method: "POST",
|
|
169
177
|
credentials: getCredentialsMode(),
|
|
170
|
-
headers: { "Content-Type": "application/json" },
|
|
178
|
+
headers: { "Content-Type": "application/json", ...getAuthHeaders() },
|
|
171
179
|
body: JSON.stringify({ ...target, ...opts }),
|
|
172
180
|
});
|
|
173
181
|
if (!res.ok) throw new DiagnoseError(res.status, await errorText(res));
|
|
@@ -186,6 +194,7 @@ export interface RunsResponse {
|
|
|
186
194
|
export async function listRuns(signal?: AbortSignal): Promise<RunsResponse> {
|
|
187
195
|
const res = await fetch(RUNS(), {
|
|
188
196
|
credentials: getCredentialsMode(),
|
|
197
|
+
headers: getAuthHeaders(),
|
|
189
198
|
signal,
|
|
190
199
|
});
|
|
191
200
|
if (!res.ok) throw new DiagnoseError(res.status, await errorText(res));
|
|
@@ -193,12 +202,41 @@ export async function listRuns(signal?: AbortSignal): Promise<RunsResponse> {
|
|
|
193
202
|
return { runs: d.runs ?? [], historyDegraded: !!d.historyDegraded };
|
|
194
203
|
}
|
|
195
204
|
|
|
205
|
+
// getRun resolves a stable run id directly. Deep links use this rather than
|
|
206
|
+
// relying on a bounded history page to happen to contain the target.
|
|
207
|
+
export async function getRun(
|
|
208
|
+
id: string,
|
|
209
|
+
signal?: AbortSignal,
|
|
210
|
+
): Promise<RunSummary> {
|
|
211
|
+
const res = await fetch(`${RUNS()}/${encodeURIComponent(id)}`, {
|
|
212
|
+
credentials: getCredentialsMode(),
|
|
213
|
+
headers: getAuthHeaders(),
|
|
214
|
+
signal,
|
|
215
|
+
});
|
|
216
|
+
if (!res.ok) throw new DiagnoseError(res.status, await errorText(res));
|
|
217
|
+
return res.json();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function updateRunVisibility(
|
|
221
|
+
id: string,
|
|
222
|
+
visibility: "private" | "organization",
|
|
223
|
+
): Promise<RunSummary> {
|
|
224
|
+
const res = await fetch(`${RUNS()}/${encodeURIComponent(id)}`, {
|
|
225
|
+
method: "PATCH",
|
|
226
|
+
credentials: getCredentialsMode(),
|
|
227
|
+
headers: { "Content-Type": "application/json", ...getAuthHeaders() },
|
|
228
|
+
body: JSON.stringify({ visibility }),
|
|
229
|
+
});
|
|
230
|
+
if (!res.ok) throw new DiagnoseError(res.status, await errorText(res));
|
|
231
|
+
return res.json();
|
|
232
|
+
}
|
|
233
|
+
|
|
196
234
|
// recordConsent acknowledges the current disclosure for an execution profile, server-side.
|
|
197
235
|
export async function recordConsent(surface: string): Promise<void> {
|
|
198
236
|
const res = await fetch(`${getApiBase()}/diagnose/consent`, {
|
|
199
237
|
method: "POST",
|
|
200
238
|
credentials: getCredentialsMode(),
|
|
201
|
-
headers: { "Content-Type": "application/json" },
|
|
239
|
+
headers: { "Content-Type": "application/json", ...getAuthHeaders() },
|
|
202
240
|
body: JSON.stringify({ surface }),
|
|
203
241
|
});
|
|
204
242
|
if (!res.ok) throw new DiagnoseError(res.status, await errorText(res));
|
|
@@ -210,6 +248,7 @@ export async function clearHistory(): Promise<void> {
|
|
|
210
248
|
const res = await fetch(`${getApiBase()}/diagnose/history/clear`, {
|
|
211
249
|
method: "POST",
|
|
212
250
|
credentials: getCredentialsMode(),
|
|
251
|
+
headers: getAuthHeaders(),
|
|
213
252
|
});
|
|
214
253
|
if (!res.ok) throw new DiagnoseError(res.status, await errorText(res));
|
|
215
254
|
}
|
|
@@ -222,7 +261,7 @@ export async function addTurn(
|
|
|
222
261
|
const res = await fetch(`${RUNS()}/${id}/turns`, {
|
|
223
262
|
method: "POST",
|
|
224
263
|
credentials: getCredentialsMode(),
|
|
225
|
-
headers: { "Content-Type": "application/json" },
|
|
264
|
+
headers: { "Content-Type": "application/json", ...getAuthHeaders() },
|
|
226
265
|
body: JSON.stringify(body),
|
|
227
266
|
});
|
|
228
267
|
if (!res.ok) throw new DiagnoseError(res.status, await errorText(res));
|
|
@@ -233,6 +272,7 @@ export async function stopRun(id: string): Promise<void> {
|
|
|
233
272
|
await fetch(`${RUNS()}/${id}/stop`, {
|
|
234
273
|
method: "POST",
|
|
235
274
|
credentials: getCredentialsMode(),
|
|
275
|
+
headers: getAuthHeaders(),
|
|
236
276
|
}).catch(() => {});
|
|
237
277
|
}
|
|
238
278
|
|
|
@@ -53,6 +53,48 @@ describe('getApplicationCostState', () => {
|
|
|
53
53
|
).toBe('partial_missing_history')
|
|
54
54
|
})
|
|
55
55
|
|
|
56
|
+
it('keeps Kubecost application totals visible when history is unsupported', () => {
|
|
57
|
+
const current: OpenCostApplicationCostResponse = {
|
|
58
|
+
available: true,
|
|
59
|
+
source: 'kubecost',
|
|
60
|
+
currency: 'USD',
|
|
61
|
+
totals: {
|
|
62
|
+
hourlyCost: 0.4, cpuCost: 0.25, memoryCost: 0.15, replicas: 3,
|
|
63
|
+
cpuUsageAvailable: true, memoryUsageAvailable: true,
|
|
64
|
+
cpuAllocationUse: 40, memoryAllocationUse: 60,
|
|
65
|
+
},
|
|
66
|
+
coverage: { total: 1, included: 1 },
|
|
67
|
+
workloads: [],
|
|
68
|
+
}
|
|
69
|
+
const trend: OpenCostApplicationCostTrendResponse = {
|
|
70
|
+
available: false,
|
|
71
|
+
source: 'kubecost',
|
|
72
|
+
reason: 'history_unsupported',
|
|
73
|
+
currency: 'USD',
|
|
74
|
+
range: '24h',
|
|
75
|
+
coverage: { total: 1, included: 0 },
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
expect(getApplicationCostState(current, trend, {})).toBe('partial_missing_history')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('does not let unsupported history mask current loading or errors', () => {
|
|
82
|
+
const trend: OpenCostApplicationCostTrendResponse = {
|
|
83
|
+
available: false,
|
|
84
|
+
source: 'kubecost',
|
|
85
|
+
reason: 'history_unsupported',
|
|
86
|
+
currency: 'USD',
|
|
87
|
+
range: '24h',
|
|
88
|
+
coverage: { total: 1, included: 0 },
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
expect(getApplicationCostState(undefined, trend, { currentLoading: true })).toBe('loading')
|
|
92
|
+
expect(getApplicationCostState(undefined, trend, { currentError: true })).toBe('load_error')
|
|
93
|
+
expect(
|
|
94
|
+
getApplicationCostState(undefined, trend, { currentError: new ApiError('denied', 403) }),
|
|
95
|
+
).toBe('access_denied')
|
|
96
|
+
})
|
|
97
|
+
|
|
56
98
|
it('uses historical data when current app metrics are absent but history exists', () => {
|
|
57
99
|
const current: OpenCostApplicationCostResponse = {
|
|
58
100
|
available: false,
|
|
@@ -206,5 +248,8 @@ describe('getApplicationCostState', () => {
|
|
|
206
248
|
}),
|
|
207
249
|
).toBe('access_denied')
|
|
208
250
|
expect(getApplicationCostState(current, undefined, { trendError: true })).toBe('access_denied')
|
|
251
|
+
|
|
252
|
+
current.reason = 'configuration_mismatch'
|
|
253
|
+
expect(getApplicationCostState(current, undefined, {})).toBe('configuration_mismatch')
|
|
209
254
|
})
|
|
210
255
|
})
|