@skyhook-io/radar-app 1.14.6 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/package.json +6 -5
- package/src/App.tsx +65 -17
- package/src/RadarApp.tsx +51 -21
- package/src/api/apiResources.test.ts +19 -1
- package/src/api/apiResources.ts +18 -3
- package/src/api/client.admission.test.ts +34 -0
- package/src/api/client.capacity.test.ts +15 -0
- package/src/api/client.trace.test.ts +9 -1
- package/src/api/client.ts +269 -204
- package/src/api/diagnose.test.ts +102 -1
- package/src/api/diagnose.ts +42 -11
- package/src/api/drain-plan.test.ts +0 -10
- package/src/api/httpErrors.test.ts +95 -0
- package/src/api/httpErrors.ts +101 -0
- package/src/api/policy.ts +33 -5
- package/src/api/radarFeatures.render.test.tsx +190 -0
- package/src/api/radarFeatures.test.ts +133 -0
- package/src/api/radarFeatures.ts +147 -0
- package/src/api/timelineSource.test.ts +2 -10
- package/src/api/timelineSource.ts +3 -2
- package/src/api/traffic.ts +4 -3
- package/src/api/usage-data.fixtures.ts +33 -0
- package/src/api/usage-data.ts +165 -0
- package/src/components/CloudFunnelButton.test.tsx +2 -2
- package/src/components/CloudFunnelButton.tsx +99 -55
- package/src/components/NamespaceSwitcher.test.tsx +47 -0
- package/src/components/NamespaceSwitcher.tsx +17 -1
- package/src/components/SelfManagedStart.tsx +95 -0
- package/src/components/applications/ApplicationsView.tsx +20 -6
- package/src/components/audit/UpgradeReadinessView.test.ts +14 -6
- package/src/components/audit/UpgradeReadinessView.tsx +9 -13
- package/src/components/capacity/CapacityView.test.tsx +13 -4
- package/src/components/capacity/shared.tsx +14 -13
- package/src/components/cloudConnectHandoff.test.ts +1 -1
- package/src/components/cloudConnectHandoff.ts +1 -1
- package/src/components/cost/ApplicationCostTab.tsx +9 -1
- package/src/components/cost/CostConnectionAction.tsx +30 -0
- package/src/components/cost/CostTrendChart.tsx +2 -0
- package/src/components/cost/CostView.tsx +4 -24
- package/src/components/cost/WorkloadCostTab.tsx +7 -1
- package/src/components/cost/source.test.ts +30 -3
- package/src/components/cost/source.ts +37 -2
- package/src/components/curl/ServiceCurlButton.tsx +7 -3
- package/src/components/diagnose/ActivityTurn.tsx +3 -2
- package/src/components/diagnose/ApplyDialog.tsx +4 -2
- package/src/components/dialog-names.test.ts +69 -0
- package/src/components/dock/NodeTerminalTab.tsx +2 -1
- package/src/components/dock/TerminalTab.tsx +2 -1
- package/src/components/dock/TrafficFlowListTab.tsx +2 -2
- package/src/components/execution/BatchExecutionView.tsx +1 -1
- package/src/components/execution/JobSetAdmission.test.tsx +160 -37
- package/src/components/execution/JobSetAdmission.tsx +16 -7
- package/src/components/gitops/GitOpsView.tsx +12 -0
- package/src/components/helm/TrackChartSourceDialog.tsx +5 -3
- package/src/components/home/HomeView.tsx +7 -3
- package/src/components/home/MCPSetupDialog.tsx +37 -37
- package/src/components/home/RadarVersionLine.test.tsx +14 -0
- package/src/components/home/RadarVersionLine.tsx +28 -3
- package/src/components/nav/PrimaryNavRail.test.tsx +10 -1
- package/src/components/nav/PrimaryNavRail.tsx +25 -3
- package/src/components/resource/PVCUsageBar.render.test.tsx +16 -0
- package/src/components/resource/PVCUsageBar.tsx +4 -1
- package/src/components/resource/PrometheusChartsGrid.render.test.tsx +1 -0
- package/src/components/resource/PrometheusChartsGrid.tsx +4 -0
- package/src/components/resources/ImageFilesystemModal.tsx +2 -1
- package/src/components/resources/PodFilePreview.tsx +4 -1
- package/src/components/resources/PodFilesystemModal.tsx +3 -2
- package/src/components/resources/ResourcesView.tsx +2 -1
- package/src/components/resources/renderers/JobAdmissionRenderers.test.tsx +18 -0
- package/src/components/resources/renderers/JobAdmissionRenderers.tsx +18 -0
- package/src/components/resources/renderers/RayJobRenderer.test.tsx +40 -0
- package/src/components/resources/renderers/RayJobRenderer.tsx +45 -0
- package/src/components/rightsizing/RightsizingScanView.tsx +4 -1
- package/src/components/settings/ConnectionHeadersEditor.tsx +109 -0
- package/src/components/settings/FormSaveActions.tsx +76 -0
- package/src/components/settings/IntegrationConnectionForms.tsx +391 -0
- package/src/components/settings/LocalConfigurationDetails.tsx +389 -0
- package/src/components/settings/LocalConnectionSettings.test.tsx +189 -0
- package/src/components/settings/LocalConnectionSettings.tsx +1015 -0
- package/src/components/settings/LocalIntegrationStatus.tsx +50 -0
- package/src/components/settings/PrivacySection.test.tsx +84 -0
- package/src/components/settings/PrivacySection.tsx +140 -0
- package/src/components/settings/PrometheusConfigField.tsx +30 -0
- package/src/components/settings/PrometheusConnectionForm.tsx +295 -0
- package/src/components/settings/SettingsDialog.tsx +393 -571
- package/src/components/settings/controls.tsx +55 -0
- package/src/components/settings/currency-options.test.ts +1 -1
- package/src/components/settings/currency-options.ts +1 -1
- package/src/components/settings/settings-state.test.ts +17 -10
- package/src/components/settings/settings-state.ts +21 -12
- package/src/components/timeline/TimelineList.test.ts +56 -0
- package/src/components/timeline/TimelineList.tsx +7 -1
- package/src/components/traffic/TrafficFlowList.tsx +5 -21
- package/src/components/traffic/TrafficFlowListContext.tsx +6 -4
- package/src/components/traffic/TrafficGraph.tsx +79 -48
- package/src/components/traffic/TrafficView.tsx +61 -37
- package/src/components/traffic/TrafficWizard.tsx +4 -2
- package/src/components/traffic/trafficFilters.test.ts +154 -1
- package/src/components/traffic/trafficFilters.ts +141 -0
- package/src/components/ui/ErrorBoundary.tsx +5 -0
- package/src/components/ui/Omnibar.tsx +17 -1
- package/src/components/ui/UpdateNotification.tsx +70 -21
- package/src/components/ui/command-items.ts +14 -0
- package/src/components/usage-data/UsageDataAsk.test.tsx +41 -0
- package/src/components/usage-data/UsageDataAsk.tsx +106 -0
- package/src/components/usage-data/UsageDataPrompt.test.ts +53 -0
- package/src/components/usage-data/UsageDataPrompt.tsx +148 -0
- package/src/components/whats-new/WhatsNew.test.ts +110 -0
- package/src/components/whats-new/WhatsNew.tsx +423 -0
- package/src/components/whats-new/WhatsNewDialog.test.tsx +229 -0
- package/src/components/whats-new/check-whats-new.test.ts +65 -0
- package/src/components/whats-new/releaseNotes.ts +121 -0
- package/src/components/workload/WorkloadView.tsx +47 -14
- package/src/components/workload/historyPaging.test.ts +72 -0
- package/src/components/workload/historyPaging.ts +147 -0
- package/src/components/workload/useHistoryPaging.test.tsx +81 -0
- package/src/context/ConnectionContext.tsx +2 -1
- package/src/context/RadarUpgrade.tsx +30 -0
- package/src/context/RadarUpgradeHost.tsx +23 -0
- package/src/hooks/usePreviousIntegrationSettings.render.test.tsx +62 -0
- package/src/hooks/usePreviousIntegrationSettings.test.ts +62 -0
- package/src/hooks/usePreviousIntegrationSettings.ts +89 -0
- package/src/index.ts +1 -0
- package/src/k8s-ui-exports.test.ts +73 -0
- package/src/utils/navigation.test.ts +17 -1
- package/src/utils/navigation.ts +16 -0
- package/src/utils/version.ts +11 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { ArrowLeft, ArrowUpRight } from 'lucide-react'
|
|
2
|
+
import { SELF_HOSTED_DOCS_URL, signupUrlFor } from './cloudConnectHandoff'
|
|
3
|
+
|
|
4
|
+
// What the Cloud dialog shows after "Run Radar Cloud yourself…". Running your
|
|
5
|
+
// own control plane starts with a Radar Cloud account, because the Hub signs
|
|
6
|
+
// the Self-Managed license, so this view names that as step 1 of 3 before the
|
|
7
|
+
// click instead of letting a sign-up page surprise someone who came to host
|
|
8
|
+
// it themselves. The link carries intent=self-hosting: the Hub's sign-in page
|
|
9
|
+
// repeats the steps and, after sign-up or sign-in, continues on its
|
|
10
|
+
// Self-hosting page, where the trial, the license and the install command are.
|
|
11
|
+
export function SelfManagedStart({ appUrl, onBack }: { appUrl: string; onBack: () => void }) {
|
|
12
|
+
const signupUrl = `${signupUrlFor(appUrl, 'self-managed-signup')}&intent=self-hosting`
|
|
13
|
+
const steps = [
|
|
14
|
+
{
|
|
15
|
+
title: 'Create your Radar Cloud account',
|
|
16
|
+
body: 'Your license is issued from it. An existing account works too.',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
title: 'Generate your install command',
|
|
20
|
+
body: 'Self-Managed is part of Enterprise, with a 14-day free trial.',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
title: 'Install the control plane, then connect each cluster',
|
|
24
|
+
body: 'One Helm command installs it. Then one command per cluster to connect it.',
|
|
25
|
+
},
|
|
26
|
+
]
|
|
27
|
+
return (
|
|
28
|
+
<>
|
|
29
|
+
<div className="min-h-0 overflow-y-auto">
|
|
30
|
+
<div className="px-8 pb-5">
|
|
31
|
+
<h3 className="text-[20px] font-semibold leading-tight tracking-tight text-theme-text-primary mb-1.5">
|
|
32
|
+
Run Radar Cloud yourself
|
|
33
|
+
</h3>
|
|
34
|
+
<p className="text-[13px] leading-relaxed text-theme-text-secondary mb-5">
|
|
35
|
+
Run the Radar Cloud control plane in your own infrastructure: the same fleet view, alerts, history
|
|
36
|
+
and team access, and your cluster data never leaves your network.
|
|
37
|
+
</p>
|
|
38
|
+
<ol className="space-y-3.5">
|
|
39
|
+
{steps.map((step, i) => (
|
|
40
|
+
<li key={step.title} className="flex gap-3">
|
|
41
|
+
<span
|
|
42
|
+
className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[10.5px] font-semibold ${
|
|
43
|
+
i === 0 ? 'bg-emerald-500 text-emerald-950' : 'border border-theme-border text-theme-text-tertiary'
|
|
44
|
+
}`}
|
|
45
|
+
>
|
|
46
|
+
{i + 1}
|
|
47
|
+
</span>
|
|
48
|
+
<div className="min-w-0">
|
|
49
|
+
<p className={`text-[13px] font-medium ${i === 0 ? 'text-theme-text-primary' : 'text-theme-text-secondary'}`}>
|
|
50
|
+
{step.title}
|
|
51
|
+
</p>
|
|
52
|
+
<p className="text-[12px] leading-relaxed text-theme-text-tertiary">{step.body}</p>
|
|
53
|
+
</div>
|
|
54
|
+
</li>
|
|
55
|
+
))}
|
|
56
|
+
</ol>
|
|
57
|
+
{/* Split by stage so a trial doesn't look like a production install:
|
|
58
|
+
the trial bundles Postgres and a built-in admin. */}
|
|
59
|
+
<div className="mt-5 space-y-1 text-[12px] leading-relaxed text-theme-text-tertiary">
|
|
60
|
+
<p>
|
|
61
|
+
<b className="font-medium text-theme-text-secondary">For a trial</b>, install it in one cluster and open
|
|
62
|
+
it with port-forward, no public address needed. To connect more clusters, give it an address they can
|
|
63
|
+
reach.
|
|
64
|
+
</p>
|
|
65
|
+
<p>
|
|
66
|
+
<b className="font-medium text-theme-text-secondary">For production</b>, connect your own Postgres and
|
|
67
|
+
your IdP to manage users.
|
|
68
|
+
</p>
|
|
69
|
+
<a href={SELF_HOSTED_DOCS_URL} target="_blank" rel="noopener noreferrer" className="inline-block underline underline-offset-2 hover:text-theme-text-primary">
|
|
70
|
+
Read the Self-Managed guide →
|
|
71
|
+
</a>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
<div className="shrink-0 px-8 py-4 bg-theme-base border-t border-theme-border flex flex-wrap items-center gap-x-3 gap-y-2.5">
|
|
76
|
+
<button
|
|
77
|
+
onClick={onBack}
|
|
78
|
+
className="inline-flex items-center gap-1.5 whitespace-nowrap text-[12.5px] text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
79
|
+
>
|
|
80
|
+
<ArrowLeft className="w-3.5 h-3.5" />
|
|
81
|
+
Back to Radar Cloud
|
|
82
|
+
</button>
|
|
83
|
+
<a
|
|
84
|
+
href={signupUrl}
|
|
85
|
+
target="_blank"
|
|
86
|
+
rel="noopener noreferrer"
|
|
87
|
+
className="ml-auto inline-flex items-center gap-1.5 whitespace-nowrap px-5 py-2 rounded-[10px] bg-emerald-500 hover:bg-emerald-400 text-emerald-950 text-[13.5px] font-bold shadow-[0_0_22px_rgba(16,185,129,0.35)] hover:shadow-[0_0_30px_rgba(16,185,129,0.5)] transition-all"
|
|
88
|
+
>
|
|
89
|
+
Create your Radar Cloud account
|
|
90
|
+
<ArrowUpRight className="w-4 h-4" />
|
|
91
|
+
</a>
|
|
92
|
+
</div>
|
|
93
|
+
</>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
@@ -4,7 +4,11 @@ import {
|
|
|
4
4
|
ApplicationsList,
|
|
5
5
|
ApplicationDetail,
|
|
6
6
|
CenteredEmpty,
|
|
7
|
+
getRadarUpgradeRequirement,
|
|
7
8
|
PageHeader,
|
|
9
|
+
RadarUpgradeAction,
|
|
10
|
+
radarUpgradeDetail,
|
|
11
|
+
radarUpgradeHeadline,
|
|
8
12
|
FreshnessControl,
|
|
9
13
|
IssueRow,
|
|
10
14
|
ISSUE_SEVERITY_RANK,
|
|
@@ -166,6 +170,7 @@ export function ApplicationsView({
|
|
|
166
170
|
);
|
|
167
171
|
}
|
|
168
172
|
if (query.error) {
|
|
173
|
+
const upgrade = getRadarUpgradeRequirement(query.error);
|
|
169
174
|
return (
|
|
170
175
|
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
171
176
|
<div className="shrink-0 border-b border-theme-border px-4 py-4">
|
|
@@ -175,12 +180,21 @@ export function ApplicationsView({
|
|
|
175
180
|
description="Deployable software in this cluster — your services, workers, and jobs, grouped by app/release evidence."
|
|
176
181
|
/>
|
|
177
182
|
</div>
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
183
|
+
{upgrade ? (
|
|
184
|
+
<CenteredEmpty
|
|
185
|
+
icon={Boxes}
|
|
186
|
+
headline={radarUpgradeHeadline("Applications")}
|
|
187
|
+
body={radarUpgradeDetail(upgrade)}
|
|
188
|
+
action={<RadarUpgradeAction requirement={upgrade} />}
|
|
189
|
+
/>
|
|
190
|
+
) : (
|
|
191
|
+
<CenteredEmpty
|
|
192
|
+
tone="filtered"
|
|
193
|
+
icon={Boxes}
|
|
194
|
+
headline="Failed to load applications"
|
|
195
|
+
body={(query.error as Error).message}
|
|
196
|
+
/>
|
|
197
|
+
)}
|
|
184
198
|
</div>
|
|
185
199
|
);
|
|
186
200
|
}
|
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
3
|
import { ApiError } from '../../api/client'
|
|
4
|
-
import {
|
|
4
|
+
import { RadarFeatureUnsupportedError } from '../../api/radarFeatures'
|
|
5
|
+
import { UPGRADE_IMPACT_DOCS_URL, UpgradeReadinessError, groupFindings, incompleteUpgradeCheckCount, issueSpecificReferences, summaryMeta, upgradeEvaluationSummary, upgradeEvidenceCoverageLabel, upgradeUnavailableKindsMessage } from './UpgradeReadinessView'
|
|
5
6
|
|
|
6
7
|
describe('UpgradeReadinessError', () => {
|
|
7
|
-
it('maps an
|
|
8
|
-
const html = renderToStaticMarkup(UpgradeReadinessError({ error: new
|
|
8
|
+
it('maps an unsupported-feature error to the v1.9 upgrade message', () => {
|
|
9
|
+
const html = renderToStaticMarkup(UpgradeReadinessError({ error: new RadarFeatureUnsupportedError('upgradeReadiness', {}) }))
|
|
9
10
|
|
|
10
|
-
expect(html).toContain('Upgrade impact needs a newer Radar')
|
|
11
|
-
expect(html).toContain(
|
|
12
|
-
expect(html).toContain('Upgrade the in-cluster Radar to enable it')
|
|
11
|
+
expect(html).toContain('Upgrade impact needs a newer Radar on this cluster')
|
|
12
|
+
expect(html).toContain('Available from Radar v1.9.')
|
|
13
13
|
expect(html).not.toContain('Unable to analyze upgrade impact')
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
+
it('keeps a non-JSON 404 that is not the router signature on the generic error state', () => {
|
|
17
|
+
// e.g. Radar Hub's plain-text "not found" for a cluster it can't resolve.
|
|
18
|
+
const html = renderToStaticMarkup(UpgradeReadinessError({ error: new ApiError('HTTP 404 (Not Found)', 404) }))
|
|
19
|
+
|
|
20
|
+
expect(html).toContain('Unable to analyze upgrade impact')
|
|
21
|
+
expect(html).not.toContain('needs a newer Radar')
|
|
22
|
+
})
|
|
23
|
+
|
|
16
24
|
it('keeps a real resource 404 on the generic error state', () => {
|
|
17
25
|
const html = renderToStaticMarkup(UpgradeReadinessError({ error: new ApiError('Cluster not found', 404) }))
|
|
18
26
|
|
|
@@ -19,8 +19,12 @@ import {
|
|
|
19
19
|
CollapseChevron,
|
|
20
20
|
EmptyState,
|
|
21
21
|
FreshnessControl,
|
|
22
|
+
getRadarUpgradeRequirement,
|
|
22
23
|
PageHeader,
|
|
23
24
|
PaneLoader,
|
|
25
|
+
RadarUpgradeAction,
|
|
26
|
+
radarUpgradeDetail,
|
|
27
|
+
radarUpgradeHeadline,
|
|
24
28
|
SelectMenu,
|
|
25
29
|
} from '@skyhook-io/k8s-ui'
|
|
26
30
|
import {
|
|
@@ -43,7 +47,6 @@ interface UpgradeReadinessViewProps {
|
|
|
43
47
|
|
|
44
48
|
const FINDING_CAP = 8
|
|
45
49
|
export const UPGRADE_IMPACT_DOCS_URL = 'https://radarhq.io/docs/features/upgrade-impact'
|
|
46
|
-
export const UPGRADE_IMPACT_MIN_RADAR_VERSION = 'v1.9.0'
|
|
47
50
|
|
|
48
51
|
const statusMeta: Record<UpgradeReadinessCheckStatus, {
|
|
49
52
|
label: string
|
|
@@ -250,22 +253,15 @@ export function UpgradeReadinessView({ namespaces, onNavigateToResource }: Upgra
|
|
|
250
253
|
}
|
|
251
254
|
|
|
252
255
|
export function UpgradeReadinessError({ error, onResetTarget }: { error: unknown; onResetTarget?: () => void }) {
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
&& error.message === 'Unknown error'
|
|
256
|
-
if (missingEndpoint) {
|
|
256
|
+
const upgrade = getRadarUpgradeRequirement(error)
|
|
257
|
+
if (upgrade) {
|
|
257
258
|
return (
|
|
258
259
|
<EmptyState
|
|
259
260
|
tone="neutral"
|
|
260
261
|
icon={ShieldAlert}
|
|
261
|
-
headline=
|
|
262
|
-
body={
|
|
263
|
-
|
|
264
|
-
This cluster’s Radar predates Upgrade impact (added in Radar {UPGRADE_IMPACT_MIN_RADAR_VERSION}).
|
|
265
|
-
<br />
|
|
266
|
-
Upgrade the in-cluster Radar to enable it.
|
|
267
|
-
</>
|
|
268
|
-
}
|
|
262
|
+
headline={radarUpgradeHeadline('Upgrade impact')}
|
|
263
|
+
body={radarUpgradeDetail(upgrade)}
|
|
264
|
+
action={<RadarUpgradeAction requirement={upgrade} />}
|
|
269
265
|
/>
|
|
270
266
|
)
|
|
271
267
|
}
|
|
@@ -26,6 +26,7 @@ import { CapacityView } from "./CapacityView";
|
|
|
26
26
|
import { updateDemandSearchParam } from "./CapacityDemand";
|
|
27
27
|
import { integrationBlock } from "./shared";
|
|
28
28
|
import { ApiError } from "../../api/client";
|
|
29
|
+
import { RadarFeatureUnsupportedError } from "../../api/radarFeatures";
|
|
29
30
|
|
|
30
31
|
vi.mock("../../context/ConnectionContext", () => ({
|
|
31
32
|
useConnection: () => ({ connection: { state: "connected" } }),
|
|
@@ -2289,15 +2290,23 @@ describe("CapacityView activity", () => {
|
|
|
2289
2290
|
});
|
|
2290
2291
|
|
|
2291
2292
|
describe("integrationBlock version skew", () => {
|
|
2292
|
-
it("maps
|
|
2293
|
+
it("maps an unsupported-feature error to the upgrade message, never 'Unknown error'", () => {
|
|
2293
2294
|
const html = renderToString(
|
|
2294
|
-
<>{integrationBlock(undefined, new
|
|
2295
|
+
<>{integrationBlock(undefined, new RadarFeatureUnsupportedError("capacity", { currentVersion: "v1.7.2" }), false, "Loading")}</>,
|
|
2295
2296
|
);
|
|
2296
|
-
expect(html).toContain("Capacity needs a newer Radar");
|
|
2297
|
-
expect(html).toContain("v1.9.
|
|
2297
|
+
expect(html).toContain("Capacity needs a newer Radar on this cluster");
|
|
2298
|
+
expect(html).toContain("Available from Radar v1.9. This cluster runs v1.7.2.");
|
|
2298
2299
|
expect(html).not.toContain("Capacity unavailable");
|
|
2299
2300
|
});
|
|
2300
2301
|
|
|
2302
|
+
it("does not read a handler's JSON 404 as a missing route", () => {
|
|
2303
|
+
const html = renderToString(
|
|
2304
|
+
<>{integrationBlock(undefined, new ApiError("nodepool not found", 404, { error: "nodepool not found" }), false, "Loading")}</>,
|
|
2305
|
+
);
|
|
2306
|
+
expect(html).toContain("Capacity unavailable");
|
|
2307
|
+
expect(html).not.toContain("needs a newer Radar");
|
|
2308
|
+
});
|
|
2309
|
+
|
|
2301
2310
|
it("keeps ordinary failures on the generic error state", () => {
|
|
2302
2311
|
const html = renderToString(
|
|
2303
2312
|
<>{integrationBlock(undefined, new ApiError("boom", 500), false, "Loading")}</>,
|
|
@@ -15,6 +15,10 @@ import {
|
|
|
15
15
|
import {
|
|
16
16
|
FreshnessControl,
|
|
17
17
|
formatDuration,
|
|
18
|
+
getRadarUpgradeRequirement,
|
|
19
|
+
RadarUpgradeAction,
|
|
20
|
+
radarUpgradeDetail,
|
|
21
|
+
radarUpgradeHeadline,
|
|
18
22
|
memberRef,
|
|
19
23
|
PaneLoader,
|
|
20
24
|
ResourceBar,
|
|
@@ -51,7 +55,6 @@ import {
|
|
|
51
55
|
import {
|
|
52
56
|
isCapacityCursorInvalidError,
|
|
53
57
|
isForbiddenError,
|
|
54
|
-
isNotFoundError,
|
|
55
58
|
useCapacityPools,
|
|
56
59
|
} from "../../api/client";
|
|
57
60
|
import type { SelectedResource } from "../../types";
|
|
@@ -1520,21 +1523,19 @@ export function integrationBlock(
|
|
|
1520
1523
|
if (!response && error) {
|
|
1521
1524
|
if (isForbiddenError(error))
|
|
1522
1525
|
return <DeniedCapacityState detail={errorMessage(error)} />;
|
|
1523
|
-
//
|
|
1524
|
-
//
|
|
1525
|
-
|
|
1526
|
-
if (
|
|
1526
|
+
// Only reachable behind a version-skewed host (Radar Cloud): the OSS
|
|
1527
|
+
// binary always serves the routes its own frontend calls.
|
|
1528
|
+
const upgrade = getRadarUpgradeRequirement(error);
|
|
1529
|
+
if (upgrade)
|
|
1527
1530
|
return (
|
|
1528
1531
|
<EmptyState
|
|
1529
1532
|
icon={Gauge}
|
|
1530
|
-
title="Capacity
|
|
1531
|
-
detail={
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
Upgrade the in-cluster Radar to enable it.
|
|
1537
|
-
</>
|
|
1533
|
+
title={radarUpgradeHeadline("Capacity")}
|
|
1534
|
+
detail={radarUpgradeDetail(upgrade)}
|
|
1535
|
+
action={
|
|
1536
|
+
<div className="mt-3 text-sm">
|
|
1537
|
+
<RadarUpgradeAction requirement={upgrade} />
|
|
1538
|
+
</div>
|
|
1538
1539
|
}
|
|
1539
1540
|
/>
|
|
1540
1541
|
);
|
|
@@ -171,7 +171,7 @@ describe('composeAdminNote — an ask, then the link, then the evidence', () =>
|
|
|
171
171
|
expect(note).toContain('Please confirm nothing is already installed before a fresh install.')
|
|
172
172
|
// The object of the request and the answer to "no SaaS", before the evidence.
|
|
173
173
|
expect(note.indexOf('What it is: Radar, the open-source Kubernetes tool')).toBeLessThan(note.indexOf('Details:'))
|
|
174
|
-
expect(note).toContain(`can
|
|
174
|
+
expect(note).toContain(`can run in-house as Radar Cloud Self-Managed: ${SELF_HOSTED_DOCS_URL}`)
|
|
175
175
|
// Never the card's second person.
|
|
176
176
|
expect(note).not.toMatch(/\byour\b/i)
|
|
177
177
|
})
|
|
@@ -144,7 +144,7 @@ export interface AdminNoteContext {
|
|
|
144
144
|
export const SELF_HOSTED_DOCS_URL = 'https://radarhq.io/docs/cloud/self-hosted/'
|
|
145
145
|
const WHAT_IT_IS = [
|
|
146
146
|
'What it is: Radar, the open-source Kubernetes tool, installed in the cluster as a Helm chart. It opens an outbound connection to a hosted control plane (nothing listens inbound); Radar Cloud users reach this Radar only through that connection, with the permissions of the ServiceAccount it runs as.',
|
|
147
|
-
`If a hosted service is not an option, the same control plane can
|
|
147
|
+
`If a hosted service is not an option, the same control plane can run in-house as Radar Cloud Self-Managed: ${SELF_HOSTED_DOCS_URL}`,
|
|
148
148
|
].join(' ')
|
|
149
149
|
|
|
150
150
|
export function composeAdminNote(blocked: CloudInstallBlocked, exit: BlockedExit, where: AdminNoteContext = {}): string {
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
isCostDiscoveryPending,
|
|
34
34
|
} from './source'
|
|
35
35
|
import { useNavCustomization } from '../../context/NavCustomization'
|
|
36
|
+
import { CostConnectionAction } from './CostConnectionAction'
|
|
36
37
|
|
|
37
38
|
type ApplicationCostState =
|
|
38
39
|
| 'loading'
|
|
@@ -130,6 +131,8 @@ export function ApplicationCostTab({
|
|
|
130
131
|
state === 'authentication_error' ||
|
|
131
132
|
state === 'configuration_mismatch' ||
|
|
132
133
|
state === 'deployment_configuration_error' ||
|
|
134
|
+
state === 'metrics_settings_error' ||
|
|
135
|
+
state === 'cost_settings_error' ||
|
|
133
136
|
state === 'history_unsupported' ||
|
|
134
137
|
state === 'load_error'
|
|
135
138
|
) {
|
|
@@ -398,7 +401,9 @@ export function getApplicationCostState(
|
|
|
398
401
|
reason === 'source_unavailable' ||
|
|
399
402
|
reason === 'authentication_error' ||
|
|
400
403
|
reason === 'configuration_mismatch' ||
|
|
401
|
-
reason === 'deployment_configuration_error'
|
|
404
|
+
reason === 'deployment_configuration_error' ||
|
|
405
|
+
reason === 'metrics_settings_error' ||
|
|
406
|
+
reason === 'cost_settings_error'
|
|
402
407
|
)
|
|
403
408
|
return reason
|
|
404
409
|
if (queryError) return 'load_error'
|
|
@@ -512,6 +517,8 @@ function reasonLabel(reason?: CostUnavailableReason) {
|
|
|
512
517
|
if (reason === 'not_found') return 'Workload not found'
|
|
513
518
|
if (reason === 'configuration_mismatch') return 'Kubecost settings are not valid for this cluster'
|
|
514
519
|
if (reason === 'deployment_configuration_error') return 'Radar cost deployment is misconfigured'
|
|
520
|
+
if (reason === 'metrics_settings_error') return 'Saved metrics settings need review'
|
|
521
|
+
if (reason === 'cost_settings_error') return 'Saved cost settings need review'
|
|
515
522
|
return 'No workload cost metrics'
|
|
516
523
|
}
|
|
517
524
|
|
|
@@ -577,6 +584,7 @@ function ApplicationCostUnavailable({
|
|
|
577
584
|
<div className="flex max-w-md flex-col items-center gap-3 text-center text-theme-text-secondary">
|
|
578
585
|
<Coins className="h-8 w-8 text-theme-text-tertiary/50" />
|
|
579
586
|
<div className="text-sm">{text}</div>
|
|
587
|
+
<CostConnectionAction reason={state} />
|
|
580
588
|
</div>
|
|
581
589
|
</div>
|
|
582
590
|
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CostUnavailableReason } from '../../api/client'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { useNavCustomization } from '../../context/NavCustomization'
|
|
4
|
+
import { useCapabilitiesContext } from '../../contexts/CapabilitiesContext'
|
|
5
|
+
import { usePreviousIntegrationSettings } from '../../hooks/usePreviousIntegrationSettings'
|
|
6
|
+
import { costConfigurationAction, isCostConfigurable } from './source'
|
|
7
|
+
|
|
8
|
+
export function CostConnectionAction({ reason, children }: { reason?: CostUnavailableReason | 'load_error'; children?: ReactNode }) {
|
|
9
|
+
const { embedded } = useNavCustomization()
|
|
10
|
+
const { configManagement } = useCapabilitiesContext()
|
|
11
|
+
const available = !embedded && isCostConfigurable(reason)
|
|
12
|
+
const offers = usePreviousIntegrationSettings(available)
|
|
13
|
+
if (!available) return <>{children}</>
|
|
14
|
+
const action = costConfigurationAction(reason as CostUnavailableReason, offers)
|
|
15
|
+
return (
|
|
16
|
+
<div className="flex flex-col items-center gap-2 text-center">
|
|
17
|
+
{action.note && <p className="text-xs text-theme-text-secondary">{action.note}</p>}
|
|
18
|
+
<div className="flex flex-wrap items-center justify-center gap-2">
|
|
19
|
+
<button
|
|
20
|
+
type="button"
|
|
21
|
+
onClick={() => window.dispatchEvent(new CustomEvent('radar:open-settings', { detail: { section: action.section } }))}
|
|
22
|
+
className="btn-brand px-3 py-1.5 text-xs font-medium"
|
|
23
|
+
>
|
|
24
|
+
{configManagement === 'operator' || configManagement === 'cloud' ? 'View connection settings' : action.label}
|
|
25
|
+
</button>
|
|
26
|
+
{children}
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -513,6 +513,8 @@ export function kubecostTrendUnavailableMessage(
|
|
|
513
513
|
return 'Radar could not reach Kubecost. Check Settings → Cost.'
|
|
514
514
|
case 'configuration_mismatch':
|
|
515
515
|
return 'Kubecost returned data for a different cluster. Check the cluster ID in Settings → Cost.'
|
|
516
|
+
case 'cost_settings_error':
|
|
517
|
+
return 'This cluster’s saved cost settings need review. Check Settings → Cost.'
|
|
516
518
|
default:
|
|
517
519
|
return 'Kubecost history could not be loaded.'
|
|
518
520
|
}
|
|
@@ -41,8 +41,8 @@ import { clusterCloudConsoleLink, nodeCloudConsoleLink } from './cloud-console'
|
|
|
41
41
|
import { RightsizingScanView } from '../rightsizing/RightsizingScanView'
|
|
42
42
|
import { CostViewTabs } from './CostViewTabs'
|
|
43
43
|
import { useNavCustomization } from '../../context/NavCustomization'
|
|
44
|
+
import { CostConnectionAction } from './CostConnectionAction'
|
|
44
45
|
import {
|
|
45
|
-
costConfigurationAction,
|
|
46
46
|
costFreshnessLabel,
|
|
47
47
|
costIntegrationUnavailableMessage,
|
|
48
48
|
costRateLabels,
|
|
@@ -57,13 +57,6 @@ interface CostViewProps {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
const SYSTEM_COST_NAMESPACES = new Set(['kube-system', 'kube-public', 'kube-node-lease'])
|
|
60
|
-
const CONFIGURABLE_COST_REASONS = new Set<CostUnavailableReason>([
|
|
61
|
-
'no_prometheus',
|
|
62
|
-
'no_cost_source',
|
|
63
|
-
'source_unavailable',
|
|
64
|
-
'authentication_error',
|
|
65
|
-
'configuration_mismatch',
|
|
66
|
-
])
|
|
67
60
|
|
|
68
61
|
export function CostView(props: CostViewProps) {
|
|
69
62
|
const { pathname } = useLocation()
|
|
@@ -137,8 +130,6 @@ function CostOverview({ onBack, onOpenResource }: CostViewProps) {
|
|
|
137
130
|
settingsAvailable,
|
|
138
131
|
namespaceScoped: namespaceScopeCount > 0,
|
|
139
132
|
})
|
|
140
|
-
const canConfigure = settingsAvailable && reason != null && CONFIGURABLE_COST_REASONS.has(reason)
|
|
141
|
-
const configureAction = costConfigurationAction(reason)
|
|
142
133
|
|
|
143
134
|
return (
|
|
144
135
|
<CostOverviewState>
|
|
@@ -146,18 +137,7 @@ function CostOverview({ onBack, onOpenResource }: CostViewProps) {
|
|
|
146
137
|
<div className="flex flex-col items-center gap-3 text-theme-text-secondary">
|
|
147
138
|
<Coins className="w-8 h-8 text-theme-text-tertiary/40" />
|
|
148
139
|
<p className="text-sm">{message}</p>
|
|
149
|
-
<
|
|
150
|
-
{canConfigure && (
|
|
151
|
-
<button
|
|
152
|
-
type="button"
|
|
153
|
-
onClick={() => window.dispatchEvent(
|
|
154
|
-
new CustomEvent('radar:open-settings', { detail: { section: configureAction.section } }),
|
|
155
|
-
)}
|
|
156
|
-
className="btn-brand px-3 py-1.5 text-xs font-medium"
|
|
157
|
-
>
|
|
158
|
-
{configureAction.label}
|
|
159
|
-
</button>
|
|
160
|
-
)}
|
|
140
|
+
<CostConnectionAction reason={reason}>
|
|
161
141
|
{(reason === 'no_prometheus' || reason === 'no_cost_source' || reason === 'source_unavailable') && (
|
|
162
142
|
<button
|
|
163
143
|
onClick={() => {
|
|
@@ -170,7 +150,7 @@ function CostOverview({ onBack, onOpenResource }: CostViewProps) {
|
|
|
170
150
|
{isFetching ? 'Checking…' : 'Check again'}
|
|
171
151
|
</button>
|
|
172
152
|
)}
|
|
173
|
-
</
|
|
153
|
+
</CostConnectionAction>
|
|
174
154
|
<button
|
|
175
155
|
onClick={onBack}
|
|
176
156
|
className="text-xs text-accent-text hover:text-theme-text-primary transition-colors"
|
|
@@ -893,7 +873,7 @@ function CostHelpDialog({ currency, source, window, onClose }: { currency: strin
|
|
|
893
873
|
Which currency is shown?
|
|
894
874
|
</h3>
|
|
895
875
|
<p>
|
|
896
|
-
Radar labels these values <strong>{currency}</strong> and does not convert them.
|
|
876
|
+
Radar labels these values <strong>{currency}</strong> and does not convert them. Automatic
|
|
897
877
|
reads <code>currencyCode</code> or <code>DISPLAY_CURRENCY</code> from a
|
|
898
878
|
cluster-discovered OpenCost installation or any active Kubecost installation, then
|
|
899
879
|
falls back to USD. A manually configured Prometheus URL disables OpenCost currency
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
isCostDiscoveryPending,
|
|
29
29
|
} from './source'
|
|
30
30
|
import { useNavCustomization } from '../../context/NavCustomization'
|
|
31
|
+
import { CostConnectionAction } from './CostConnectionAction'
|
|
31
32
|
|
|
32
33
|
type WorkloadCostState =
|
|
33
34
|
| 'loading'
|
|
@@ -98,6 +99,8 @@ export function WorkloadCostTab({ kind, namespace, name }: WorkloadCostTabProps)
|
|
|
98
99
|
state === 'authentication_error' ||
|
|
99
100
|
state === 'configuration_mismatch' ||
|
|
100
101
|
state === 'deployment_configuration_error' ||
|
|
102
|
+
state === 'metrics_settings_error' ||
|
|
103
|
+
state === 'cost_settings_error' ||
|
|
101
104
|
state === 'history_unsupported' ||
|
|
102
105
|
state === 'load_error'
|
|
103
106
|
) {
|
|
@@ -306,7 +309,9 @@ export function getWorkloadCostState(
|
|
|
306
309
|
reason === 'source_unavailable' ||
|
|
307
310
|
reason === 'authentication_error' ||
|
|
308
311
|
reason === 'configuration_mismatch' ||
|
|
309
|
-
reason === 'deployment_configuration_error'
|
|
312
|
+
reason === 'deployment_configuration_error' ||
|
|
313
|
+
reason === 'metrics_settings_error' ||
|
|
314
|
+
reason === 'cost_settings_error'
|
|
310
315
|
)
|
|
311
316
|
return reason
|
|
312
317
|
if (queryError) return 'load_error'
|
|
@@ -375,6 +380,7 @@ function WorkloadCostUnavailable({
|
|
|
375
380
|
<div className="flex max-w-md flex-col items-center gap-3 text-center text-theme-text-secondary">
|
|
376
381
|
<Coins className="h-8 w-8 text-theme-text-tertiary/50" />
|
|
377
382
|
<div className="text-sm">{message}</div>
|
|
383
|
+
<CostConnectionAction reason={state} />
|
|
378
384
|
</div>
|
|
379
385
|
</div>
|
|
380
386
|
)
|
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { costDataThroughLabel, costFreshnessLabel, costIntegrationUnavailableMessage, costRateLabels, costSourceLabel, isCostDiscoveryPending } from './source'
|
|
2
|
+
import { COST_SOURCE_OPTIONS, costSourcePreferenceLabel, costConfigurationAction, isCostConfigurable, costDataThroughLabel, costFreshnessLabel, costIntegrationUnavailableMessage, costRateLabels, costSourceLabel, isCostDiscoveryPending } from './source'
|
|
3
3
|
|
|
4
4
|
describe('cost source presentation', () => {
|
|
5
|
+
it('routes missing sources to one applicable previous connection', () => {
|
|
6
|
+
const both = { metrics: 'previous', cost: 'previous', explicitCostBackend: true } as const
|
|
7
|
+
expect(costConfigurationAction('no_prometheus', both).section).toBe('prometheus')
|
|
8
|
+
expect(costConfigurationAction('no_cost_source', both).section).toBe('cost')
|
|
9
|
+
expect(costConfigurationAction('no_prometheus', { ...both, metrics: undefined }).section).toBe('cost')
|
|
10
|
+
expect(costConfigurationAction('no_cost_source', { ...both, explicitCostBackend: false }).section).toBe('prometheus')
|
|
11
|
+
expect(costConfigurationAction('no_prometheus', { ...both, metrics: undefined, explicitCostBackend: false }).label).toBe('Configure metrics')
|
|
12
|
+
expect(costConfigurationAction('authentication_error', { ...both, cost: undefined }).label).toBe('Configure cost source')
|
|
13
|
+
expect(costConfigurationAction('configuration_mismatch', both).section).toBe('cost')
|
|
14
|
+
})
|
|
15
|
+
it('keeps the neutral action and explains reuse in the note', () => {
|
|
16
|
+
const copy = { metrics: 'copy', explicitCostBackend: false } as const
|
|
17
|
+
expect(costConfigurationAction('no_prometheus', copy)).toMatchObject({ section: 'prometheus', label: 'Configure metrics' })
|
|
18
|
+
expect(costConfigurationAction('no_prometheus', copy).note).toContain('if that backend also serves this cluster')
|
|
19
|
+
expect(costConfigurationAction('no_prometheus', { ...copy, metrics: 'previous' })).toMatchObject({ label: 'Configure metrics' })
|
|
20
|
+
expect(costConfigurationAction('no_prometheus', { ...copy, metrics: 'previous' }).note).toContain('now saved per cluster')
|
|
21
|
+
})
|
|
22
|
+
it.each(['access_denied', 'not_found', 'history_unsupported', 'no_workload_data', 'no_metrics', 'query_error', 'load_error', undefined])('does not offer connection edits for %s', reason => {
|
|
23
|
+
expect(isCostConfigurable(reason)).toBe(false)
|
|
24
|
+
})
|
|
5
25
|
it('distinguishes Prometheus windows from Kubecost ETL freshness', () => {
|
|
6
|
-
expect(costSourceLabel('prometheus')).toBe('OpenCost
|
|
7
|
-
expect(costSourceLabel('kubecost')).toBe('Kubecost
|
|
26
|
+
expect(costSourceLabel('prometheus')).toBe('OpenCost metrics')
|
|
27
|
+
expect(costSourceLabel('kubecost')).toBe('Kubecost')
|
|
8
28
|
expect(costFreshnessLabel('prometheus', '1h')).toBe('last 1h average')
|
|
9
29
|
expect(costFreshnessLabel('kubecost', '1h', '2026-08-26T13:58:00Z')).toContain('1-hour allocation average')
|
|
10
30
|
expect(costFreshnessLabel('kubecost', '1d', '2026-08-26T13:58:00Z')).toContain('1-day allocation average')
|
|
@@ -12,6 +32,13 @@ describe('cost source presentation', () => {
|
|
|
12
32
|
expect(costDataThroughLabel('invalid')).toBe('')
|
|
13
33
|
})
|
|
14
34
|
|
|
35
|
+
it('names cost-source preferences the same way in every install type', () => {
|
|
36
|
+
expect(COST_SOURCE_OPTIONS.map(option => option.label)).toEqual(['Automatic', 'OpenCost metrics', 'Kubecost'])
|
|
37
|
+
expect(costSourcePreferenceLabel(undefined)).toBe('Automatic')
|
|
38
|
+
expect(costSourcePreferenceLabel('')).toBe('Automatic')
|
|
39
|
+
expect(costSourcePreferenceLabel('prometheus')).toBe('OpenCost metrics')
|
|
40
|
+
})
|
|
41
|
+
|
|
15
42
|
it('labels fallback allocation windows without calling a daily average current', () => {
|
|
16
43
|
expect(costRateLabels('1h').hourly).toBe('Hourly (1-hour average)')
|
|
17
44
|
expect(costRateLabels('1d').rate).toBe('1-day average hourly rate')
|
|
@@ -1,17 +1,48 @@
|
|
|
1
1
|
import type { CostDataSource, CostUnavailableReason } from '../../api/client'
|
|
2
|
+
import { previousSettingsNote, type PreviousIntegrationSettings } from '../../hooks/usePreviousIntegrationSettings'
|
|
3
|
+
|
|
4
|
+
export function isCostConfigurable(reason?: string): boolean {
|
|
5
|
+
return reason === 'no_prometheus' || reason === 'no_cost_source' || reason === 'source_unavailable'
|
|
6
|
+
|| reason === 'authentication_error' || reason === 'configuration_mismatch'
|
|
7
|
+
|| reason === 'metrics_settings_error' || reason === 'cost_settings_error'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type CostSourcePreference = 'auto' | CostDataSource
|
|
11
|
+
|
|
12
|
+
export const COST_SOURCE_OPTIONS: { value: CostSourcePreference; label: string; description: string }[] = [
|
|
13
|
+
{ value: 'auto', label: 'Automatic', description: 'OpenCost metrics when available, otherwise Kubecost' },
|
|
14
|
+
{ value: 'prometheus', label: 'OpenCost metrics', description: 'From the metrics connection' },
|
|
15
|
+
{ value: 'kubecost', label: 'Kubecost', description: 'From a Kubecost Aggregator' },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
export function costSourcePreferenceLabel(preference?: string): string {
|
|
19
|
+
return COST_SOURCE_OPTIONS.find(option => option.value === (preference || 'auto'))?.label ?? preference ?? 'Automatic'
|
|
20
|
+
}
|
|
2
21
|
|
|
3
22
|
export function costSourceLabel(source?: CostDataSource): string {
|
|
4
|
-
return source === 'kubecost' ? '
|
|
23
|
+
return costSourcePreferenceLabel(source === 'kubecost' ? 'kubecost' : 'prometheus')
|
|
5
24
|
}
|
|
6
25
|
|
|
7
26
|
export function isCostDiscoveryPending(reason?: string): boolean {
|
|
8
27
|
return reason === 'no_prometheus' || reason === 'no_cost_source'
|
|
9
28
|
}
|
|
10
29
|
|
|
11
|
-
export function costConfigurationAction(reason?: CostUnavailableReason): {
|
|
30
|
+
export function costConfigurationAction(reason?: CostUnavailableReason, offers?: PreviousIntegrationSettings): {
|
|
12
31
|
section: 'prometheus' | 'cost'
|
|
13
32
|
label: string
|
|
33
|
+
note?: string
|
|
14
34
|
} {
|
|
35
|
+
if (reason === 'metrics_settings_error') return { section: 'prometheus', label: 'Review metrics settings' }
|
|
36
|
+
if (reason === 'cost_settings_error') return { section: 'cost', label: 'Review cost settings' }
|
|
37
|
+
const previousKind = reason === 'no_prometheus'
|
|
38
|
+
? offers?.metrics ? 'metrics' : offers?.explicitCostBackend ? 'cost' : undefined
|
|
39
|
+
: reason === 'no_cost_source'
|
|
40
|
+
? offers?.explicitCostBackend ? 'cost' : offers?.metrics ? 'metrics' : undefined
|
|
41
|
+
: isCostConfigurable(reason) && offers?.cost ? 'cost' : undefined
|
|
42
|
+
const offer = previousKind && offers?.[previousKind]
|
|
43
|
+
if (previousKind && offer) return previousKind === 'metrics'
|
|
44
|
+
? { section: 'prometheus', label: 'Configure metrics', note: previousSettingsNote('metrics', offer) }
|
|
45
|
+
: { section: 'cost', label: 'Configure cost source', note: previousSettingsNote('cost', offer) }
|
|
15
46
|
return reason === 'no_prometheus'
|
|
16
47
|
? { section: 'prometheus', label: 'Configure metrics' }
|
|
17
48
|
: { section: 'cost', label: 'Configure cost source' }
|
|
@@ -84,6 +115,10 @@ export function costIntegrationUnavailableMessage(
|
|
|
84
115
|
return settingsAvailable
|
|
85
116
|
? 'Kubecost Aggregator is unavailable. Check the URL, network path, and cluster ID in Settings → Cost.'
|
|
86
117
|
: 'Kubecost Aggregator is unavailable. Update this cluster’s cost-source configuration in the host application or Radar deployment.'
|
|
118
|
+
case 'metrics_settings_error':
|
|
119
|
+
return 'This cluster’s saved metrics settings need review before cost data can load. Review them in Settings → Metrics.'
|
|
120
|
+
case 'cost_settings_error':
|
|
121
|
+
return 'This cluster’s saved cost settings need review before cost data can load. Review them in Settings → Cost.'
|
|
87
122
|
case 'deployment_configuration_error':
|
|
88
123
|
return 'Cost collection is misconfigured by this Radar deployment. Update its environment variables or Helm cost values, then restart Radar.'
|
|
89
124
|
case 'authentication_error':
|