@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.
- package/README.md +2 -0
- package/package.json +6 -5
- package/src/App.tsx +14 -11
- package/src/RadarApp.tsx +51 -21
- package/src/api/apiResources.test.ts +19 -1
- package/src/api/apiResources.ts +18 -3
- package/src/api/client.capacity.test.ts +15 -0
- package/src/api/client.trace.test.ts +9 -1
- package/src/api/client.ts +232 -201
- package/src/api/diagnose.test.ts +102 -1
- package/src/api/diagnose.ts +42 -11
- package/src/api/drain-plan.test.ts +0 -10
- package/src/api/httpErrors.test.ts +95 -0
- package/src/api/httpErrors.ts +101 -0
- package/src/api/policy.ts +33 -5
- package/src/api/radarFeatures.render.test.tsx +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/components/CloudFunnelButton.tsx +9 -9
- package/src/components/NamespaceSwitcher.test.tsx +47 -0
- package/src/components/NamespaceSwitcher.tsx +17 -1
- package/src/components/SelfManagedStart.tsx +5 -6
- package/src/components/applications/ApplicationsView.tsx +20 -6
- package/src/components/audit/UpgradeReadinessView.test.ts +14 -6
- package/src/components/audit/UpgradeReadinessView.tsx +9 -13
- package/src/components/capacity/CapacityView.test.tsx +13 -4
- package/src/components/capacity/shared.tsx +14 -13
- package/src/components/cost/ApplicationCostTab.tsx +9 -1
- package/src/components/cost/CostConnectionAction.tsx +30 -0
- package/src/components/cost/CostTrendChart.tsx +2 -0
- package/src/components/cost/CostView.tsx +4 -24
- package/src/components/cost/WorkloadCostTab.tsx +7 -1
- package/src/components/cost/source.test.ts +30 -3
- package/src/components/cost/source.ts +37 -2
- package/src/components/curl/ServiceCurlButton.tsx +7 -3
- package/src/components/dock/NodeTerminalTab.tsx +2 -1
- package/src/components/dock/TerminalTab.tsx +2 -1
- package/src/components/dock/TrafficFlowListTab.tsx +2 -2
- package/src/components/gitops/GitOpsView.tsx +12 -0
- package/src/components/home/HomeView.tsx +1 -1
- package/src/components/home/MCPSetupDialog.tsx +37 -37
- package/src/components/resource/PVCUsageBar.render.test.tsx +16 -0
- package/src/components/resource/PVCUsageBar.tsx +4 -1
- package/src/components/resource/PrometheusChartsGrid.render.test.tsx +1 -0
- package/src/components/resource/PrometheusChartsGrid.tsx +4 -0
- package/src/components/resources/ImageFilesystemModal.tsx +2 -1
- package/src/components/resources/PodFilePreview.tsx +4 -1
- package/src/components/resources/PodFilesystemModal.tsx +3 -2
- package/src/components/resources/ResourcesView.tsx +2 -1
- package/src/components/rightsizing/RightsizingScanView.tsx +4 -1
- package/src/components/settings/ConnectionHeadersEditor.tsx +109 -0
- package/src/components/settings/FormSaveActions.tsx +76 -0
- package/src/components/settings/IntegrationConnectionForms.tsx +391 -0
- package/src/components/settings/LocalConfigurationDetails.tsx +389 -0
- package/src/components/settings/LocalConnectionSettings.test.tsx +189 -0
- package/src/components/settings/LocalConnectionSettings.tsx +1015 -0
- package/src/components/settings/LocalIntegrationStatus.tsx +50 -0
- package/src/components/settings/PrometheusConfigField.tsx +30 -0
- package/src/components/settings/PrometheusConnectionForm.tsx +295 -0
- package/src/components/settings/SettingsDialog.tsx +382 -521
- package/src/components/settings/currency-options.test.ts +1 -1
- package/src/components/settings/currency-options.ts +1 -1
- package/src/components/settings/settings-state.test.ts +17 -10
- package/src/components/settings/settings-state.ts +20 -12
- package/src/components/timeline/TimelineList.test.ts +56 -0
- package/src/components/timeline/TimelineList.tsx +7 -1
- package/src/components/traffic/TrafficFlowList.tsx +5 -21
- package/src/components/traffic/TrafficFlowListContext.tsx +6 -4
- package/src/components/traffic/TrafficGraph.tsx +79 -48
- package/src/components/traffic/TrafficView.tsx +61 -37
- package/src/components/traffic/TrafficWizard.tsx +4 -2
- package/src/components/traffic/trafficFilters.test.ts +154 -1
- package/src/components/traffic/trafficFilters.ts +141 -0
- package/src/components/ui/Omnibar.tsx +1 -1
- package/src/components/ui/UpdateNotification.tsx +57 -12
- package/src/components/workload/WorkloadView.tsx +36 -13
- 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
|
@@ -3,7 +3,7 @@ import { TrafficFlowList } from '../traffic/TrafficFlowList'
|
|
|
3
3
|
import { List } from 'lucide-react'
|
|
4
4
|
|
|
5
5
|
export function TrafficFlowListTab() {
|
|
6
|
-
const { flows } = useTrafficFlowList()
|
|
6
|
+
const { flows, responsesCallerOriented } = useTrafficFlowList()
|
|
7
7
|
|
|
8
8
|
if (flows.length === 0) {
|
|
9
9
|
return (
|
|
@@ -14,5 +14,5 @@ export function TrafficFlowListTab() {
|
|
|
14
14
|
)
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
return <TrafficFlowList flows={flows} />
|
|
17
|
+
return <TrafficFlowList flows={flows} responsesCallerOriented={responsesCallerOriented} />
|
|
18
18
|
}
|
|
@@ -2,6 +2,8 @@ import { useEffect, useMemo, useRef, useState } from 'react'
|
|
|
2
2
|
import { useLocation, useNavigate } from 'react-router-dom'
|
|
3
3
|
import { useQuery } from '@tanstack/react-query'
|
|
4
4
|
import yaml from 'yaml'
|
|
5
|
+
import { hasHealthSettingsHint } from '@skyhook-io/k8s-ui/components/gitops/GitOpsHealthSourceNotice'
|
|
6
|
+
import { previousSettingsNote, usePreviousIntegrationSettings } from '../../hooks/usePreviousIntegrationSettings'
|
|
5
7
|
import {
|
|
6
8
|
GitOpsActivityInsightView,
|
|
7
9
|
GitOpsChangesView,
|
|
@@ -412,6 +414,14 @@ function GitOpsDetailView({ namespaces, onOpenResource, onOpenSettings }: GitOps
|
|
|
412
414
|
const terminatingChipTooltip = terminatingDescriptions.chipTooltip
|
|
413
415
|
const terminatingActionTooltip = terminatingDescriptions.actionDisabledTooltip
|
|
414
416
|
const [appView, setAppView] = useState<GitOpsDetailTab>('topology')
|
|
417
|
+
const insight = insightsQ.data
|
|
418
|
+
const showDiffSettingsHint = appView === 'changes' && insight?.summary.tool === 'argocd' && insight.summary.kind === 'Application'
|
|
419
|
+
&& !!insight.changes?.length && !insight.capabilities?.argoDiffAvailable
|
|
420
|
+
const previousSettings = usePreviousIntegrationSettings(!!onOpenSettings && (
|
|
421
|
+
hasHealthSettingsHint(insight?.summary, insight?.changes)
|
|
422
|
+
|| showDiffSettingsHint
|
|
423
|
+
))
|
|
424
|
+
const settingsNote = previousSettings.argocd ? previousSettingsNote('argocd', previousSettings.argocd) : undefined
|
|
415
425
|
// When the user clicks an actionable issue alert ("OutOfSync — NodePool
|
|
416
426
|
// default is out of sync · View →"), we navigate to Changes and focus
|
|
417
427
|
// that resource. The ref is stringified to a stable key so GitOpsChangesView
|
|
@@ -670,6 +680,7 @@ function GitOpsDetailView({ namespaces, onOpenResource, onOpenSettings }: GitOps
|
|
|
670
680
|
healthDocsUrl={GITOPS_HEALTH_DOCS_URL}
|
|
671
681
|
remoteDestinationHint={<RemoteDestinationCloudHint />}
|
|
672
682
|
onOpenSettings={onOpenSettings}
|
|
683
|
+
settingsNote={showDiffSettingsHint ? undefined : settingsNote}
|
|
673
684
|
renderRevisionMeta={
|
|
674
685
|
isArgoApp && insightsQ.data?.capabilities?.revisionMetadataAvailable
|
|
675
686
|
? (revision) => (
|
|
@@ -809,6 +820,7 @@ function GitOpsDetailView({ namespaces, onOpenResource, onOpenSettings }: GitOps
|
|
|
809
820
|
<ArgoResourceDiffLoader appNamespace={namespace} appName={name} resourceRef={ref} />
|
|
810
821
|
) : undefined}
|
|
811
822
|
onOpenSettings={onOpenSettings}
|
|
823
|
+
settingsNote={settingsNote}
|
|
812
824
|
/>
|
|
813
825
|
)
|
|
814
826
|
}
|
|
@@ -351,7 +351,7 @@ function ProblemsPanel({
|
|
|
351
351
|
<div className="flex items-center gap-2">
|
|
352
352
|
<button
|
|
353
353
|
type="button"
|
|
354
|
-
className="rounded-md px-2 py-1 text-xs font-medium text-accent-text transition-colors hover:bg-accent-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-radar-accent)]/40"
|
|
354
|
+
className="rounded-md px-2 py-1 text-xs font-medium text-accent-text transition-colors hover:bg-accent-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-radar-accent,var(--accent))]/40"
|
|
355
355
|
onClick={onNavigateToIssues}
|
|
356
356
|
>
|
|
357
357
|
View all
|
|
@@ -116,71 +116,67 @@ export function MCPSetupDialog({ open, onClose, mcpUrl }: MCPSetupDialogProps) {
|
|
|
116
116
|
|
|
117
117
|
const currentPort = Number(window.location.port) || 80
|
|
118
118
|
|
|
119
|
-
const
|
|
119
|
+
const mcpServersUrlConfig = (key: 'url' | 'serverUrl') => JSON.stringify({
|
|
120
120
|
mcpServers: {
|
|
121
121
|
radar: {
|
|
122
|
-
|
|
123
|
-
url: mcpUrl,
|
|
122
|
+
[key]: mcpUrl,
|
|
124
123
|
}
|
|
125
124
|
}
|
|
126
125
|
}, null, 2)
|
|
127
126
|
|
|
128
|
-
const
|
|
129
|
-
|
|
127
|
+
const vsCodeConfig = JSON.stringify({
|
|
128
|
+
servers: {
|
|
130
129
|
radar: {
|
|
130
|
+
type: "http",
|
|
131
131
|
url: mcpUrl,
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
}, null, 2)
|
|
135
135
|
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
radar: {
|
|
139
|
-
serverUrl: mcpUrl,
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}, null, 2)
|
|
143
|
-
|
|
144
|
-
const vsCodeConfig = JSON.stringify({
|
|
145
|
-
servers: {
|
|
136
|
+
const opencodeConfig = JSON.stringify({
|
|
137
|
+
mcp: {
|
|
146
138
|
radar: {
|
|
147
|
-
type: "
|
|
139
|
+
type: "remote",
|
|
148
140
|
url: mcpUrl,
|
|
141
|
+
enabled: true,
|
|
149
142
|
}
|
|
150
143
|
}
|
|
151
144
|
}, null, 2)
|
|
152
145
|
|
|
153
|
-
const
|
|
146
|
+
const clineConfig = JSON.stringify({
|
|
154
147
|
mcpServers: {
|
|
155
148
|
radar: {
|
|
156
|
-
|
|
149
|
+
type: "streamableHttp",
|
|
150
|
+
url: mcpUrl,
|
|
157
151
|
}
|
|
158
152
|
}
|
|
159
153
|
}, null, 2)
|
|
160
154
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
155
|
+
// mcp-remote rejects plain http:// unless the host is localhost/127.0.0.1 or --allow-http is passed.
|
|
156
|
+
const parsedMcpUrl = new URL(mcpUrl)
|
|
157
|
+
const mcpRemoteNeedsAllowHttp = parsedMcpUrl.protocol === 'http:' && parsedMcpUrl.hostname !== 'localhost' && parsedMcpUrl.hostname !== '127.0.0.1'
|
|
158
|
+
const mcpRemoteArgs = ['-y', 'mcp-remote', mcpUrl, ...(mcpRemoteNeedsAllowHttp ? ['--allow-http'] : [])]
|
|
159
|
+
const claudeDesktopConfig = JSON.stringify({
|
|
164
160
|
mcpServers: {
|
|
165
161
|
radar: {
|
|
166
|
-
|
|
162
|
+
command: "npx",
|
|
163
|
+
args: mcpRemoteArgs,
|
|
167
164
|
}
|
|
168
165
|
}
|
|
169
166
|
}, null, 2)
|
|
170
167
|
|
|
171
|
-
const
|
|
172
|
-
|
|
168
|
+
const zedConfig = JSON.stringify({
|
|
169
|
+
context_servers: {
|
|
173
170
|
radar: {
|
|
174
171
|
url: mcpUrl,
|
|
175
172
|
}
|
|
176
173
|
}
|
|
177
174
|
}, null, 2)
|
|
178
175
|
|
|
179
|
-
const
|
|
180
|
-
|
|
176
|
+
const geminiConfig = JSON.stringify({
|
|
177
|
+
mcpServers: {
|
|
181
178
|
radar: {
|
|
182
|
-
|
|
183
|
-
url: mcpUrl,
|
|
179
|
+
httpUrl: mcpUrl,
|
|
184
180
|
}
|
|
185
181
|
}
|
|
186
182
|
}, null, 2)
|
|
@@ -285,16 +281,20 @@ export function MCPSetupDialog({ open, onClose, mcpUrl }: MCPSetupDialogProps) {
|
|
|
285
281
|
<h4 className="text-sm font-semibold text-theme-text-primary">Connect your AI tool</h4>
|
|
286
282
|
|
|
287
283
|
{[
|
|
288
|
-
{ icon: Terminal, name: 'Claude Code', path: '', config: `claude mcp add
|
|
289
|
-
{ icon:
|
|
290
|
-
{ icon: Code2, name: 'Cursor', path: '~/.cursor/mcp.json', config:
|
|
291
|
-
{ icon: Code2, name: '
|
|
292
|
-
{ icon:
|
|
293
|
-
{ icon: Code2, name: 'Cline', path: 'Cline MCP settings (via UI)', config: clineConfig },
|
|
294
|
-
{ icon: Code2, name: 'JetBrains AI', path: 'Settings → Tools → AI Assistant → MCP', config: jetbrainsConfig },
|
|
284
|
+
{ icon: Terminal, name: 'Claude Code', path: '', config: `claude mcp add --transport http radar ${mcpUrl}` },
|
|
285
|
+
{ icon: Terminal, name: 'Codex', path: '', config: `codex mcp add radar --url ${mcpUrl}` },
|
|
286
|
+
{ icon: Code2, name: 'Cursor', path: '~/.cursor/mcp.json', config: mcpServersUrlConfig('url') },
|
|
287
|
+
{ icon: Code2, name: 'VS Code (GitHub Copilot)', path: '.vscode/mcp.json', config: vsCodeConfig },
|
|
288
|
+
{ icon: Terminal, name: 'GitHub Copilot CLI', path: '', config: `copilot mcp add --transport http radar ${mcpUrl}` },
|
|
295
289
|
{ icon: Terminal, name: 'OpenCode', path: '~/.config/opencode/opencode.json', config: opencodeConfig },
|
|
296
|
-
{ icon:
|
|
297
|
-
{ icon:
|
|
290
|
+
{ icon: Code2, name: 'Google Antigravity', path: '~/.gemini/config/mcp_config.json', config: mcpServersUrlConfig('serverUrl') },
|
|
291
|
+
{ icon: Code2, name: 'Kiro', path: '~/.kiro/settings/mcp.json', config: mcpServersUrlConfig('url') },
|
|
292
|
+
{ icon: Code2, name: 'JetBrains AI Assistant / Junie', path: 'AI Assistant settings or ~/.junie/mcp/mcp.json', config: mcpServersUrlConfig('url') },
|
|
293
|
+
{ icon: Code2, name: 'Cline', path: 'Cline MCP settings (via UI)', config: clineConfig },
|
|
294
|
+
{ icon: Code2, name: 'Devin Desktop (formerly Windsurf)', path: '~/.config/devin/mcp_config.json', config: mcpServersUrlConfig('url') },
|
|
295
|
+
{ icon: MessageSquare, name: 'Claude Desktop', path: '~/Library/Application Support/Claude/claude_desktop_config.json', config: claudeDesktopConfig },
|
|
296
|
+
{ icon: Code2, name: 'Zed', path: 'Zed settings.json', config: zedConfig },
|
|
297
|
+
{ icon: Terminal, name: 'Gemini CLI (Code Assist Standard/Enterprise)', path: '~/.gemini/settings.json', config: geminiConfig },
|
|
298
298
|
].map((agent) => (
|
|
299
299
|
<Disclosure
|
|
300
300
|
key={agent.name}
|
|
@@ -5,6 +5,7 @@ import { ApiError, type PrometheusPVCUsage } from '../../api/client'
|
|
|
5
5
|
|
|
6
6
|
let canConfigure = true
|
|
7
7
|
let roleLoading = false
|
|
8
|
+
let previousMetrics = false
|
|
8
9
|
let statusResult: { data?: { connected: boolean; discovering?: boolean }; error?: Error }
|
|
9
10
|
let usageResult: { data?: PrometheusPVCUsage; error?: Error }
|
|
10
11
|
vi.mock('../../api/client', async (importActual) => ({
|
|
@@ -15,13 +16,28 @@ vi.mock('../../api/client', async (importActual) => ({
|
|
|
15
16
|
useCloudRole: () => ({ canAtLeast: () => canConfigure, isLoading: roleLoading }),
|
|
16
17
|
}))
|
|
17
18
|
const { PVCUsageBar } = await import('./PVCUsageBar')
|
|
19
|
+
vi.mock('../../hooks/usePreviousIntegrationSettings', () => ({
|
|
20
|
+
usePreviousIntegrationSettings: (relevant: boolean) => ({ metrics: relevant && previousMetrics }),
|
|
21
|
+
previousSettingsNote: () => 'You can copy your previous settings in Settings → Metrics.',
|
|
22
|
+
}))
|
|
18
23
|
const measured: PrometheusPVCUsage = { namespace: 'demo', name: 'disk', used: 0, capacity: 1024, ratio: 0, hasData: true, status: 'available' }
|
|
19
24
|
const render = () => renderToString(<PVCUsageBar namespace="demo" name="disk" />)
|
|
20
25
|
|
|
21
26
|
describe('PVC usage availability', () => {
|
|
27
|
+
it('offers previous settings only for a missing connection, not missing series or denied access', () => {
|
|
28
|
+
previousMetrics = true
|
|
29
|
+
statusResult = { data: { connected: false } }
|
|
30
|
+
expect(render()).toContain('copy your previous settings')
|
|
31
|
+
statusResult = { data: { connected: true } }
|
|
32
|
+
usageResult = { data: { ...measured, hasData: false, status: 'no_series' } }
|
|
33
|
+
expect(render()).not.toContain('copy your previous settings')
|
|
34
|
+
statusResult = { error: new ApiError('denied', 403) }
|
|
35
|
+
expect(render()).not.toContain('copy your previous settings')
|
|
36
|
+
})
|
|
22
37
|
beforeEach(() => {
|
|
23
38
|
canConfigure = true
|
|
24
39
|
roleLoading = false
|
|
40
|
+
previousMetrics = false
|
|
25
41
|
statusResult = { data: { connected: true } }
|
|
26
42
|
usageResult = {}
|
|
27
43
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { formatMemoryBytes } from '@skyhook-io/k8s-ui/utils/format'
|
|
2
2
|
import { isForbiddenError, useAutoPromConnect, useCloudRole, usePrometheusPVCUsage, usePrometheusStatus } from '../../api/client'
|
|
3
3
|
import { useNavCustomization } from '../../context/NavCustomization'
|
|
4
|
+
import { previousSettingsNote, usePreviousIntegrationSettings } from '../../hooks/usePreviousIntegrationSettings'
|
|
4
5
|
|
|
5
6
|
export function PVCUsageBar({ namespace, name }: { namespace: string; name: string }) {
|
|
6
7
|
// PVC detail can be the first Prometheus-backed surface a user opens; without
|
|
@@ -40,6 +41,8 @@ export function PVCUsageBar({ namespace, name }: { namespace: string; name: stri
|
|
|
40
41
|
|
|
41
42
|
const denied = isForbiddenError(error) || isForbiddenError(statusError)
|
|
42
43
|
const waiting = !denied && !statusError && (!status || status.discovering || (isConnected && !usage && !error))
|
|
44
|
+
const offers = usePreviousIntegrationSettings(canConfigure && !waiting && !denied && !statusError && !isConnected)
|
|
45
|
+
const previousNote = offers.metrics ? previousSettingsNote('metrics', offers.metrics) : undefined
|
|
43
46
|
const guidance = denied
|
|
44
47
|
? 'Ask your operator to review your metrics access.'
|
|
45
48
|
: !canConfigure && !roleLoading
|
|
@@ -53,6 +56,7 @@ export function PVCUsageBar({ namespace, name }: { namespace: string; name: stri
|
|
|
53
56
|
<section aria-label="PVC usage" className="rounded-lg border border-theme-border bg-theme-surface/30 p-3">
|
|
54
57
|
<div className="text-xs font-medium text-theme-text-secondary uppercase tracking-wide mb-1">Usage</div>
|
|
55
58
|
<p className="text-sm text-theme-text-tertiary">{unavailable}</p>
|
|
59
|
+
{previousNote && <p className="mt-2 text-xs text-theme-text-secondary">{previousNote}</p>}
|
|
56
60
|
{!waiting && (
|
|
57
61
|
canConfigure && !denied ? (
|
|
58
62
|
<button
|
|
@@ -102,4 +106,3 @@ export function PVCUsageBar({ namespace, name }: { namespace: string; name: stri
|
|
|
102
106
|
</section>
|
|
103
107
|
)
|
|
104
108
|
}
|
|
105
|
-
|
|
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
2
2
|
import { renderToString } from 'react-dom/server'
|
|
3
3
|
import { MemoryRouter } from 'react-router-dom'
|
|
4
4
|
import { NavCustomizationProvider } from '../../context/NavCustomization'
|
|
5
|
+
vi.mock('../../hooks/usePreviousIntegrationSettings', () => ({ usePreviousIntegrationSettings: () => ({ metrics: false }) }))
|
|
5
6
|
import type { WorkloadMetrics } from '../../api/workloadMetrics'
|
|
6
7
|
|
|
7
8
|
let connected = false
|
|
@@ -35,6 +35,7 @@ import { Tooltip } from "../ui/Tooltip";
|
|
|
35
35
|
import { WorkloadMetricsSection } from "./WorkloadMetricsSection";
|
|
36
36
|
import { RightsizingStrip } from "./RightsizingStrip";
|
|
37
37
|
import { useNavCustomization } from "../../context/NavCustomization";
|
|
38
|
+
import { previousSettingsNote, usePreviousIntegrationSettings } from '../../hooks/usePreviousIntegrationSettings';
|
|
38
39
|
|
|
39
40
|
// Used when MetricsTabContent is in expanded (full-screen) mode. Drawer mode
|
|
40
41
|
// uses the single-chart tabbed `PrometheusCharts` instead — drawer width
|
|
@@ -73,6 +74,8 @@ export function PrometheusChartsGrid({
|
|
|
73
74
|
const showRestartLane = isSupported && kind !== "Node";
|
|
74
75
|
|
|
75
76
|
const settingsAvailable = !useNavCustomization().embedded;
|
|
77
|
+
const offers = usePreviousIntegrationSettings(isSupported && !statusLoading && !!status && !isConnected && !status.discovering);
|
|
78
|
+
const previousNote = offers.metrics ? previousSettingsNote('metrics', offers.metrics) : undefined;
|
|
76
79
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
77
80
|
const timeRange = TIME_RANGES.find((range) => range.value === searchParams.get("metricsRange"))?.value ?? "1h";
|
|
78
81
|
|
|
@@ -142,6 +145,7 @@ export function PrometheusChartsGrid({
|
|
|
142
145
|
onClick={() => window.dispatchEvent(new CustomEvent('radar:open-settings', { detail: { section: 'prometheus' } }))}
|
|
143
146
|
>Configure metrics</button>}
|
|
144
147
|
</div>
|
|
148
|
+
{previousNote && <p className="mt-3 text-xs text-theme-text-secondary">{previousNote}</p>}
|
|
145
149
|
{!settingsAvailable && <p className="mt-3 text-xs text-theme-text-tertiary">Ask your operator to configure the metrics connection for this cluster.</p>}
|
|
146
150
|
{isWorkload && <a className="mt-3 inline-block text-xs text-accent hover:underline" href="https://github.com/skyhook-io/radar/blob/main/docs/workload-metrics.md#what-each-chart-needs" target="_blank" rel="noopener noreferrer">What each chart needs</a>}
|
|
147
151
|
</div>
|
|
@@ -10,6 +10,7 @@ import { formatBytes } from '../../utils/format'
|
|
|
10
10
|
import { downloadBlob, filterTree } from './file-browser-utils'
|
|
11
11
|
import { Tooltip } from '../ui/Tooltip'
|
|
12
12
|
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
|
|
13
|
+
import { readErrorBody } from '../../api/httpErrors'
|
|
13
14
|
import { Input } from '@skyhook-io/k8s-ui'
|
|
14
15
|
|
|
15
16
|
const radarLoadingIconUrl = assetUrl(radarLoadingIcon)
|
|
@@ -32,7 +33,7 @@ async function fetchImageFilesystem(
|
|
|
32
33
|
headers: getAuthHeaders(),
|
|
33
34
|
})
|
|
34
35
|
if (!response.ok) {
|
|
35
|
-
const error = await response
|
|
36
|
+
const error = await readErrorBody(response)
|
|
36
37
|
throw new Error(error.error || `HTTP ${response.status}`)
|
|
37
38
|
}
|
|
38
39
|
return response.json()
|
|
@@ -5,6 +5,7 @@ import { PaneLoader, ensureMonacoRuntime } from '@skyhook-io/k8s-ui'
|
|
|
5
5
|
import { formatBytes } from '../../utils/format'
|
|
6
6
|
import { Disclosure } from '@skyhook-io/k8s-ui/components/ui/Disclosure'
|
|
7
7
|
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
|
|
8
|
+
import { nonJsonErrorMessage } from '../../api/httpErrors'
|
|
8
9
|
|
|
9
10
|
// A curated inline viewer for text files inside a pod container, rendered in
|
|
10
11
|
// place of the file listing by PodFilesystemModal — never as its own dialog.
|
|
@@ -81,7 +82,9 @@ async function fetchPodFilePreview(
|
|
|
81
82
|
return {
|
|
82
83
|
ok: false,
|
|
83
84
|
code: 'network_error',
|
|
84
|
-
message:
|
|
85
|
+
message: response.ok
|
|
86
|
+
? `Unexpected non-JSON response (HTTP ${response.status}).`
|
|
87
|
+
: nonJsonErrorMessage(response, raw),
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
90
|
|
|
@@ -7,6 +7,7 @@ import type { FileNode } from '../../types'
|
|
|
7
7
|
import { formatBytes } from '../../utils/format'
|
|
8
8
|
import { downloadBlob, filterTree } from './file-browser-utils'
|
|
9
9
|
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
|
|
10
|
+
import { readErrorBody } from '../../api/httpErrors'
|
|
10
11
|
import { isDesktopApp } from '../../utils/desktop-download'
|
|
11
12
|
import { openFile, openFolder } from '../../utils/desktop-open-folder'
|
|
12
13
|
import { useToast } from '../ui/Toast'
|
|
@@ -33,7 +34,7 @@ async function fetchPodFiles(
|
|
|
33
34
|
headers: getAuthHeaders(),
|
|
34
35
|
})
|
|
35
36
|
if (!response.ok) {
|
|
36
|
-
const error = await response
|
|
37
|
+
const error = await readErrorBody(response)
|
|
37
38
|
throw new Error(error.error || `HTTP ${response.status}`)
|
|
38
39
|
}
|
|
39
40
|
return response.json()
|
|
@@ -105,7 +106,7 @@ async function downloadPodFile(
|
|
|
105
106
|
headers: getAuthHeaders(),
|
|
106
107
|
})
|
|
107
108
|
if (!response.ok) {
|
|
108
|
-
const err = await response
|
|
109
|
+
const err = await readErrorBody(response)
|
|
109
110
|
throw new Error(err.error || `HTTP ${response.status}`)
|
|
110
111
|
}
|
|
111
112
|
|
|
@@ -5,6 +5,7 @@ import { ApiError, debugNamespaceLog, fetchJSON, isForbiddenError, isKindSyncFai
|
|
|
5
5
|
import { isBadgeWorthy } from '../../utils/auditBadges'
|
|
6
6
|
import type { AuditBadgeMessage } from '@skyhook-io/k8s-ui'
|
|
7
7
|
import { apiUrl, getAuthHeaders, getCredentialsMode, stripBasename } from '../../api/config'
|
|
8
|
+
import { readErrorBody } from '../../api/httpErrors'
|
|
8
9
|
import { useAPIResources } from '../../api/apiResources'
|
|
9
10
|
import { useConnection } from '../../context/ConnectionContext'
|
|
10
11
|
import { initNavigationMap, getSecretStoreProviderType } from '@skyhook-io/k8s-ui'
|
|
@@ -296,7 +297,7 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
296
297
|
durationMs: Math.round(performance.now() - startedAt),
|
|
297
298
|
})
|
|
298
299
|
if (!res.ok) {
|
|
299
|
-
const errorData = await
|
|
300
|
+
const errorData = await readErrorBody(res)
|
|
300
301
|
throw new ApiError(errorData.error || `Failed to fetch ${selectedKind.name}`, res.status, errorData)
|
|
301
302
|
}
|
|
302
303
|
const body = await res.json()
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
type RightsizingActionTone,
|
|
37
37
|
} from './presentation'
|
|
38
38
|
import { useNavCustomization } from '../../context/NavCustomization'
|
|
39
|
+
import { previousSettingsNote, usePreviousIntegrationSettings } from '../../hooks/usePreviousIntegrationSettings'
|
|
39
40
|
|
|
40
41
|
export const RIGHTSIZING_SCAN_DESCRIPTION =
|
|
41
42
|
'Find CPU and memory requests to increase, reduce, or review. Radar never changes them.'
|
|
@@ -117,6 +118,8 @@ export function RightsizingScanView({ namespaces }: RightsizingScanViewProps) {
|
|
|
117
118
|
isLoading: statusLoading,
|
|
118
119
|
refetch: retryPrometheus,
|
|
119
120
|
} = usePrometheusStatus()
|
|
121
|
+
const offers = usePreviousIntegrationSettings(!!promStatus && !promStatus.connected && !promStatus.discovering && !statusLoading)
|
|
122
|
+
const previousNote = offers.metrics ? previousSettingsNote('metrics', offers.metrics) : undefined
|
|
120
123
|
const scan = useRightsizingScan(namespaces, clusterInfo?.context)
|
|
121
124
|
const result = scan.data
|
|
122
125
|
const [openRow, setOpenRow] = useState<string | null>(null)
|
|
@@ -257,7 +260,7 @@ export function RightsizingScanView({ namespaces }: RightsizingScanViewProps) {
|
|
|
257
260
|
<CenteredState
|
|
258
261
|
title={RIGHTSIZING_METRICS_REQUIRED_TITLE}
|
|
259
262
|
body={settingsAvailable
|
|
260
|
-
? RIGHTSIZING_METRICS_REQUIRED_BODY
|
|
263
|
+
? `${RIGHTSIZING_METRICS_REQUIRED_BODY}${previousNote ? `\n${previousNote}` : ''}`
|
|
261
264
|
: RIGHTSIZING_EMBEDDED_METRICS_REQUIRED_BODY}
|
|
262
265
|
action={
|
|
263
266
|
<div className="flex flex-wrap items-center justify-center gap-3">
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Input } from '@skyhook-io/k8s-ui'
|
|
2
|
+
|
|
3
|
+
export interface HeaderOperation {
|
|
4
|
+
key: string
|
|
5
|
+
action: 'keep' | 'set' | 'clear'
|
|
6
|
+
value?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function ConnectionHeadersEditor({
|
|
10
|
+
keys,
|
|
11
|
+
environmentKeys,
|
|
12
|
+
value,
|
|
13
|
+
onChange
|
|
14
|
+
}: {
|
|
15
|
+
keys: string[]
|
|
16
|
+
environmentKeys: string[]
|
|
17
|
+
value: HeaderOperation[]
|
|
18
|
+
onChange: (value: HeaderOperation[]) => void
|
|
19
|
+
}) {
|
|
20
|
+
const rows = value.length
|
|
21
|
+
? value
|
|
22
|
+
: keys.map((key): HeaderOperation => ({ key, action: 'keep' }))
|
|
23
|
+
const update = (index: number, patch: Partial<HeaderOperation>) =>
|
|
24
|
+
onChange(rows.map((row, i) => (i === index ? { ...row, ...patch } : row)))
|
|
25
|
+
return (
|
|
26
|
+
<section className="mt-4 space-y-2">
|
|
27
|
+
<h4 className="text-sm font-medium text-theme-text-primary">
|
|
28
|
+
Authentication headers
|
|
29
|
+
</h4>
|
|
30
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
31
|
+
Optional. Saved values stay unchanged unless you replace or remove them.
|
|
32
|
+
</p>
|
|
33
|
+
{rows.map((row, index) => {
|
|
34
|
+
const saved = index < keys.length
|
|
35
|
+
const environment = saved && environmentKeys.some(
|
|
36
|
+
(key) => key.toLowerCase() === row.key.toLowerCase()
|
|
37
|
+
)
|
|
38
|
+
return (
|
|
39
|
+
<div key={index} className="space-y-1">
|
|
40
|
+
<div className="flex items-center gap-2">
|
|
41
|
+
<Input
|
|
42
|
+
aria-label={`Header ${index + 1} name`}
|
|
43
|
+
value={row.key}
|
|
44
|
+
disabled={saved}
|
|
45
|
+
onChange={(e) => update(index, { key: e.target.value })}
|
|
46
|
+
placeholder="Header name"
|
|
47
|
+
className="min-w-0 flex-1 px-3 py-1.5 text-sm bg-theme-elevated border border-theme-border rounded-md text-theme-text-primary placeholder:text-theme-text-tertiary focus:outline-none focus:border-skyhook-500"
|
|
48
|
+
/>
|
|
49
|
+
<Input
|
|
50
|
+
aria-label={`${row.key || 'New header'} value`}
|
|
51
|
+
type="password"
|
|
52
|
+
autoComplete="new-password"
|
|
53
|
+
disabled={environment || row.action === 'clear'}
|
|
54
|
+
value={row.value ?? ''}
|
|
55
|
+
placeholder={
|
|
56
|
+
row.action === 'clear'
|
|
57
|
+
? 'Will be removed'
|
|
58
|
+
: environment
|
|
59
|
+
? 'From environment'
|
|
60
|
+
: saved
|
|
61
|
+
? 'Saved value'
|
|
62
|
+
: 'Value'
|
|
63
|
+
}
|
|
64
|
+
onChange={(e) =>
|
|
65
|
+
update(index, {
|
|
66
|
+
action: saved && !e.target.value ? 'keep' : 'set',
|
|
67
|
+
value: e.target.value || undefined
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
className="min-w-0 flex-1 px-3 py-1.5 text-sm bg-theme-elevated border border-theme-border rounded-md text-theme-text-primary placeholder:text-theme-text-tertiary focus:outline-none focus:border-skyhook-500"
|
|
71
|
+
/>
|
|
72
|
+
{!environment && (
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
className="text-xs text-accent-text hover:underline shrink-0"
|
|
76
|
+
aria-label={`${row.action === 'clear' ? 'Undo removal of' : 'Remove'} ${row.key || 'new header'}`}
|
|
77
|
+
onClick={() =>
|
|
78
|
+
saved
|
|
79
|
+
? update(index, {
|
|
80
|
+
action: row.action === 'clear' ? 'keep' : 'clear',
|
|
81
|
+
value: undefined
|
|
82
|
+
})
|
|
83
|
+
: onChange(rows.filter((_, i) => i !== index))
|
|
84
|
+
}
|
|
85
|
+
>
|
|
86
|
+
{row.action === 'clear' ? 'Undo' : 'Remove'}
|
|
87
|
+
</button>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
{environment && (
|
|
91
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
92
|
+
Environment reference · edit this header in clusters.json.
|
|
93
|
+
</p>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
})}
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
className="text-xs text-accent-text hover:underline"
|
|
101
|
+
onClick={() =>
|
|
102
|
+
onChange([...rows, { key: '', action: 'set', value: '' }])
|
|
103
|
+
}
|
|
104
|
+
>
|
|
105
|
+
Add header
|
|
106
|
+
</button>
|
|
107
|
+
</section>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Collapse } from "@skyhook-io/k8s-ui";
|
|
2
|
+
import { Check } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
export interface FormFeedback {
|
|
5
|
+
message: string;
|
|
6
|
+
tone: "success" | "warning" | "info";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function FormSaveActions({
|
|
10
|
+
dirty,
|
|
11
|
+
busy,
|
|
12
|
+
onSave,
|
|
13
|
+
onDiscard,
|
|
14
|
+
feedback,
|
|
15
|
+
error,
|
|
16
|
+
}: {
|
|
17
|
+
dirty: boolean;
|
|
18
|
+
busy: boolean;
|
|
19
|
+
onSave: () => void;
|
|
20
|
+
onDiscard: () => void;
|
|
21
|
+
feedback?: FormFeedback;
|
|
22
|
+
error?: string;
|
|
23
|
+
}) {
|
|
24
|
+
const success = !dirty && !busy && !error && feedback?.tone === "success";
|
|
25
|
+
const detail =
|
|
26
|
+
!busy && (error || (feedback?.tone !== "success" && feedback?.message));
|
|
27
|
+
return (
|
|
28
|
+
<div className="mt-4 border-t border-theme-border pt-4">
|
|
29
|
+
<div className="flex min-h-8 flex-wrap items-center gap-3">
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
disabled={!dirty || busy}
|
|
33
|
+
onClick={onSave}
|
|
34
|
+
className="btn-brand min-w-28 px-3 py-2 text-xs"
|
|
35
|
+
>
|
|
36
|
+
{busy ? "Saving…" : "Save changes"}
|
|
37
|
+
</button>
|
|
38
|
+
{success && (
|
|
39
|
+
<span
|
|
40
|
+
role="status"
|
|
41
|
+
className="flex items-center gap-1.5 text-xs text-theme-text-secondary"
|
|
42
|
+
>
|
|
43
|
+
<Check
|
|
44
|
+
aria-hidden="true"
|
|
45
|
+
className="h-3.5 w-3.5 text-[var(--color-success-dark)] dark:text-[var(--color-success-light)]"
|
|
46
|
+
/>
|
|
47
|
+
{feedback.message}
|
|
48
|
+
</span>
|
|
49
|
+
)}
|
|
50
|
+
{dirty && (
|
|
51
|
+
<>
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
disabled={busy}
|
|
55
|
+
onClick={onDiscard}
|
|
56
|
+
className="text-xs text-theme-text-secondary hover:underline"
|
|
57
|
+
>
|
|
58
|
+
Discard
|
|
59
|
+
</button>
|
|
60
|
+
<span className="text-xs text-theme-text-tertiary">
|
|
61
|
+
Unsaved changes
|
|
62
|
+
</span>
|
|
63
|
+
</>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
<Collapse open={!!detail}>
|
|
67
|
+
<p
|
|
68
|
+
role={error ? "alert" : "status"}
|
|
69
|
+
className={`pt-2 text-xs ${error ? "text-semantic-error" : feedback?.tone === "warning" ? "text-warning-text" : "text-theme-text-secondary"}`}
|
|
70
|
+
>
|
|
71
|
+
{detail}
|
|
72
|
+
</p>
|
|
73
|
+
</Collapse>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|