@skyhook-io/radar-app 1.11.0 → 1.12.3
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 +7 -7
- package/src/App.tsx +37 -17
- package/src/api/client.images.test.ts +63 -0
- package/src/api/client.ts +244 -39
- package/src/api/client.yaml.test.ts +3 -3
- package/src/api/version-check.test.ts +78 -0
- package/src/components/CloudConnectFlow.tsx +46 -26
- package/src/components/CloudFunnelButton.tsx +166 -129
- package/src/components/ConnectionErrorView.test.tsx +74 -1
- package/src/components/ConnectionErrorView.tsx +22 -20
- package/src/components/ContextSwitcher.tsx +10 -13
- package/src/components/applications/ApplicationsView.tsx +10 -9
- package/src/components/audit/AuditView.tsx +6 -3
- package/src/components/audit/UpgradeReadinessView.test.ts +26 -2
- package/src/components/audit/UpgradeReadinessView.tsx +22 -12
- package/src/components/capacity/ClusterSchedulingCard.tsx +8 -9
- package/src/components/capacity/schedulingBar.test.ts +10 -0
- package/src/components/cost/ApplicationCostTab.test.ts +6 -0
- package/src/components/cost/ApplicationCostTab.tsx +28 -16
- package/src/components/cost/CostTrendChart.tsx +20 -10
- package/src/components/cost/CostView.tsx +79 -28
- package/src/components/cost/CurrentAllocationUse.tsx +6 -4
- package/src/components/cost/WorkloadCostTab.test.ts +10 -0
- package/src/components/cost/WorkloadCostTab.tsx +24 -12
- package/src/components/cost/format.test.ts +27 -8
- package/src/components/cost/format.ts +78 -27
- package/src/components/diagnose/DiagnoseSurface.tsx +14 -10
- package/src/components/gitops/GitOpsView.tsx +8 -3
- package/src/components/helm/HelmReleaseDrawer.tsx +4 -3
- package/src/components/helm/OwnedResources.tsx +10 -2
- package/src/components/home/ClusterHealthCard.test.ts +31 -0
- package/src/components/home/ClusterHealthCard.tsx +63 -1
- package/src/components/home/CostCard.tsx +12 -7
- package/src/components/home/HomeView.tsx +36 -11
- package/src/components/home/MCPSetupDialog.tsx +5 -4
- package/src/components/home/RadarVersionLine.test.tsx +145 -0
- package/src/components/home/RadarVersionLine.tsx +137 -0
- package/src/components/home/mcpToolCatalog.ts +12 -0
- package/src/components/nav/PrimaryNavRail.tsx +2 -2
- package/src/components/resources/PodFilesystemModal.tsx +54 -2
- package/src/components/resources/ResourcesView.tsx +31 -8
- package/src/components/resources/renderers/WorkloadRenderer.tsx +13 -5
- package/src/components/rightsizing/RightsizingScanView.tsx +2 -2
- package/src/components/settings/SettingsDialog.tsx +181 -40
- package/src/components/settings/currency-options.test.ts +49 -0
- package/src/components/settings/currency-options.ts +38 -0
- package/src/components/ui/DiagnosticsOverlay.test.ts +40 -0
- package/src/components/ui/DiagnosticsOverlay.tsx +18 -6
- package/src/components/ui/ErrorBoundary.test.tsx +55 -0
- package/src/components/ui/ErrorBoundary.tsx +17 -2
- package/src/components/ui/UpdateNotification.test.tsx +49 -0
- package/src/components/ui/UpdateNotification.tsx +6 -2
- package/src/components/ui/command-items.ts +4 -14
- package/src/components/workload/WorkloadView.test.ts +60 -0
- package/src/components/workload/WorkloadView.tsx +272 -30
- package/src/contexts/CapabilitiesContext.test.tsx +29 -0
- package/src/contexts/CapabilitiesContext.tsx +7 -3
- package/src/main.tsx +4 -114
- package/src/utils/context-name.test.ts +63 -0
- package/src/utils/context-name.ts +22 -0
- package/src/utils/navigation.test.ts +45 -0
- package/src/utils/navigation.ts +5 -5
- package/src/utils/topology-selection.ts +3 -2
- package/src/utils/version.test.ts +37 -0
- package/src/utils/version.ts +56 -0
- package/src/utils/wails-clipboard.test.ts +109 -0
- package/src/utils/wails-clipboard.ts +127 -0
|
@@ -16,10 +16,12 @@ import {
|
|
|
16
16
|
ResourcesView as BaseResourcesView,
|
|
17
17
|
CORE_RESOURCES,
|
|
18
18
|
intersectWorkloadWrites,
|
|
19
|
+
hasCuratedColumns,
|
|
20
|
+
sanitizePrinterTable,
|
|
19
21
|
} from '@skyhook-io/k8s-ui'
|
|
20
|
-
import type { Capabilities, ResourceQueryResult, WorkloadWritePermissions } from '@skyhook-io/k8s-ui'
|
|
22
|
+
import type { Capabilities, PrinterTable, ResourceQueryResult, WorkloadWritePermissions } from '@skyhook-io/k8s-ui'
|
|
21
23
|
import type { SelectedResource } from '../../types'
|
|
22
|
-
import {
|
|
24
|
+
import { apiVersionToGroup, kindToPluralWithGroup, type NavigateToResource } from '../../utils/navigation'
|
|
23
25
|
import { CreateResourceDialog } from '../shared/CreateResourceDialog'
|
|
24
26
|
import { getSkeletonYaml } from '../../utils/skeleton-yaml'
|
|
25
27
|
|
|
@@ -229,12 +231,19 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
229
231
|
// Fetch full data only for the selected kind
|
|
230
232
|
const selectedKindQuery = useQuery({
|
|
231
233
|
queryKey: ['resources', selectedKind?.name, isSelectedCrd ? selectedKind?.group : '', namespaces],
|
|
232
|
-
queryFn: async () => {
|
|
233
|
-
if (!selectedKind) return []
|
|
234
|
+
queryFn: async (): Promise<{ items: any[]; printerTable: PrinterTable | null }> => {
|
|
235
|
+
if (!selectedKind) return { items: [], printerTable: null }
|
|
234
236
|
const params = new URLSearchParams()
|
|
235
237
|
if (namespaces.length > 0) params.set('namespaces', namespacesParam)
|
|
236
238
|
if (isSelectedCrd && selectedKind.group) params.set('group', selectedKind.group)
|
|
237
239
|
if (selectedKindSummaryServed) params.set('include', 'summary')
|
|
240
|
+
// Only CRDs can declare printer columns, and a curated kind discards the
|
|
241
|
+
// result — so table mode is requested from exactly the kinds that can use
|
|
242
|
+
// it. Resolving a table costs the server a CRD read per request; doing
|
|
243
|
+
// that for a kind whose columns are hand-curated is pure waste.
|
|
244
|
+
const wantsTable = isSelectedCrd && !!selectedKind.group &&
|
|
245
|
+
!hasCuratedColumns(selectedKind.name, selectedKind.group)
|
|
246
|
+
if (wantsTable) params.set('table', '1')
|
|
238
247
|
const startedAt = performance.now()
|
|
239
248
|
debugNamespaceLog('resources:selected-kind-fetch-start', {
|
|
240
249
|
kind: selectedKind.name,
|
|
@@ -258,7 +267,19 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
258
267
|
const errorData = await res.json().catch(() => ({ error: `HTTP ${res.status}` }))
|
|
259
268
|
throw new ApiError(errorData.error || `Failed to fetch ${selectedKind.name}`, res.status, errorData)
|
|
260
269
|
}
|
|
261
|
-
|
|
270
|
+
const body = await res.json()
|
|
271
|
+
// Both branches are current shapes, not a guess at a legacy one: a Radar
|
|
272
|
+
// backend that predates `table` ignores the parameter and answers with
|
|
273
|
+
// the bare array. @skyhook-io/radar-app is versioned independently of the
|
|
274
|
+
// backend it points at, so a consumer can pair a new frontend with an
|
|
275
|
+
// older Radar — and reading that array as a missing envelope would render
|
|
276
|
+
// every CRD list empty. Items and columns still come from one response,
|
|
277
|
+
// so a row can never render against another fetch's cells.
|
|
278
|
+
if (!wantsTable || Array.isArray(body)) return { items: body as any[], printerTable: null }
|
|
279
|
+
return {
|
|
280
|
+
items: Array.isArray(body?.items) ? body.items as any[] : [],
|
|
281
|
+
printerTable: sanitizePrinterTable(body),
|
|
282
|
+
}
|
|
262
283
|
},
|
|
263
284
|
enabled: !!selectedKind && !selectedKindQueryBlocked,
|
|
264
285
|
staleTime: 30000,
|
|
@@ -275,13 +296,13 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
275
296
|
return {
|
|
276
297
|
resourceName: selectedKind.name,
|
|
277
298
|
group: selectedKind.group,
|
|
278
|
-
data: selectedKindQueryBlocked ? [] : selectedKindQuery.data
|
|
299
|
+
data: selectedKindQueryBlocked ? [] : selectedKindQuery.data?.items,
|
|
279
300
|
isLoading: waitingForGuardCount || selectedKindQuery.isLoading,
|
|
280
301
|
error: selectedKindQueryBlocked ? undefined : selectedKindQuery.error,
|
|
281
302
|
refetch: selectedKindQuery.refetch,
|
|
282
303
|
dataUpdatedAt: selectedKindQuery.dataUpdatedAt,
|
|
283
304
|
}
|
|
284
|
-
}, [selectedKind, selectedKindQueryBlocked, waitingForGuardCount, selectedKindQuery.data, selectedKindQuery.isLoading, selectedKindQuery.error, selectedKindQuery.refetch, selectedKindQuery.dataUpdatedAt])
|
|
305
|
+
}, [selectedKind, selectedKindQueryBlocked, waitingForGuardCount, selectedKindQuery.data?.items, selectedKindQuery.isLoading, selectedKindQuery.error, selectedKindQuery.refetch, selectedKindQuery.dataUpdatedAt])
|
|
285
306
|
|
|
286
307
|
// Metrics
|
|
287
308
|
const { data: topPodMetrics } = useTopPodMetrics({ enabled: topPodMetricsEnabled, namespaces })
|
|
@@ -352,6 +373,7 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
352
373
|
resourceReasons={countsData?.reasons}
|
|
353
374
|
resourceUnavailable={countsData?.unavailable}
|
|
354
375
|
selectedKindQuery={selectedKindQueryResult}
|
|
376
|
+
printerTable={selectedKindQueryBlocked ? null : selectedKindQuery.data?.printerTable ?? null}
|
|
355
377
|
connectionState={connection.state}
|
|
356
378
|
largeListGuard={largeListGuard}
|
|
357
379
|
onSelectedKindChange={setSelectedKind}
|
|
@@ -394,7 +416,8 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
394
416
|
initialYaml={createDialogYaml}
|
|
395
417
|
title={createDialogTitle}
|
|
396
418
|
onCreated={(result) => {
|
|
397
|
-
|
|
419
|
+
const group = apiVersionToGroup(result.apiVersion)
|
|
420
|
+
onResourceClick?.({ kind: kindToPluralWithGroup(result.kind, group), namespace: result.namespace, name: result.name, group })
|
|
398
421
|
}}
|
|
399
422
|
/>
|
|
400
423
|
</>
|
|
@@ -4,8 +4,8 @@ import { useScaleWorkload, fetchJSON } from '../../../api/client'
|
|
|
4
4
|
import { useRBACSubject } from '../../../api/rbac'
|
|
5
5
|
import { usePolicyResource } from '../../../api/policy'
|
|
6
6
|
import { useQueries, useQueryClient } from '@tanstack/react-query'
|
|
7
|
-
import { kindToPlural } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
8
|
-
import type { Relationships, ResourceRef, ResourceWithRelationships } from '../../../types'
|
|
7
|
+
import { kindToPlural, kindToPluralWithGroup } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
8
|
+
import type { Relationships, ResourceRef, ResourceWithRelationships, WorkloadPodInfo } from '../../../types'
|
|
9
9
|
import type { ScalerDiagnosis } from '@skyhook-io/k8s-ui/components/resources/renderers/WorkloadRenderer'
|
|
10
10
|
|
|
11
11
|
// Map plural lowercase kind to singular PascalCase for ownerReferences matching
|
|
@@ -26,9 +26,10 @@ interface WorkloadRendererProps {
|
|
|
26
26
|
onNavigate?: (ref: ResourceRef) => void
|
|
27
27
|
relationships?: Relationships
|
|
28
28
|
scaleBlockedBy?: ResourceRef[]
|
|
29
|
+
workloadPods?: WorkloadPodInfo[]
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
export function WorkloadRenderer({ kind, data, onNavigate, scaleBlockedBy }: WorkloadRendererProps) {
|
|
32
|
+
export function WorkloadRenderer({ kind, data, onNavigate, scaleBlockedBy, workloadPods }: WorkloadRendererProps) {
|
|
32
33
|
const navigate = useNavigate()
|
|
33
34
|
const queryClient = useQueryClient()
|
|
34
35
|
const scaleMutation = useScaleWorkload()
|
|
@@ -54,13 +55,19 @@ export function WorkloadRenderer({ kind, data, onNavigate, scaleBlockedBy }: Wor
|
|
|
54
55
|
})
|
|
55
56
|
const hpaQueries = useQueries({
|
|
56
57
|
queries: hpaRefs.map(ref => ({
|
|
57
|
-
queryKey: [
|
|
58
|
+
queryKey: [
|
|
59
|
+
'resource',
|
|
60
|
+
kindToPluralWithGroup(ref.kind, ref.group ?? ''),
|
|
61
|
+
ref.namespace,
|
|
62
|
+
ref.name,
|
|
63
|
+
ref.group,
|
|
64
|
+
],
|
|
58
65
|
queryFn: () => {
|
|
59
66
|
const ns = ref.namespace || '_'
|
|
60
67
|
const params = new URLSearchParams()
|
|
61
68
|
if (ref.group) params.set('group', ref.group)
|
|
62
69
|
const query = params.toString()
|
|
63
|
-
return fetchJSON<ResourceWithRelationships<any>>(`/resources/${
|
|
70
|
+
return fetchJSON<ResourceWithRelationships<any>>(`/resources/${kindToPluralWithGroup(ref.kind, ref.group ?? '')}/${ns}/${ref.name}${query ? `?${query}` : ''}`)
|
|
64
71
|
},
|
|
65
72
|
enabled: Boolean(ref.kind && ref.name),
|
|
66
73
|
staleTime: 10000,
|
|
@@ -90,6 +97,7 @@ export function WorkloadRenderer({ kind, data, onNavigate, scaleBlockedBy }: Wor
|
|
|
90
97
|
policyLoading={policyLoading}
|
|
91
98
|
policyError={policyError as Error | null}
|
|
92
99
|
scaleBlockedBy={scaleBlockedBy}
|
|
100
|
+
workloadPods={workloadPods}
|
|
93
101
|
scalerDiagnostics={scalerDiagnostics}
|
|
94
102
|
onScale={async (replicas) => {
|
|
95
103
|
await scaleMutation.mutateAsync({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useLayoutEffect, useMemo, useState } from 'react'
|
|
2
2
|
import { useNavigate, useSearchParams } from 'react-router-dom'
|
|
3
|
-
import { AlertTriangle,
|
|
3
|
+
import { AlertTriangle, Coins, ExternalLink, Gauge, Loader2, RefreshCw } from 'lucide-react'
|
|
4
4
|
import {
|
|
5
5
|
Collapse,
|
|
6
6
|
CollapseChevron,
|
|
@@ -194,7 +194,7 @@ export function RightsizingScanView({ namespaces }: RightsizingScanViewProps) {
|
|
|
194
194
|
<div className="flex-1 min-h-0 overflow-y-auto">
|
|
195
195
|
<div className="mx-auto flex w-full max-w-[1920px] flex-col gap-4 px-6 py-6">
|
|
196
196
|
<PageHeader
|
|
197
|
-
icon={
|
|
197
|
+
icon={Coins}
|
|
198
198
|
title="Cost Insights"
|
|
199
199
|
description="Understand current allocation and find CPU and memory requests worth tuning."
|
|
200
200
|
/>
|
|
@@ -3,22 +3,25 @@ import { createPortal } from 'react-dom'
|
|
|
3
3
|
import {
|
|
4
4
|
Settings, X, RotateCcw, RotateCw, Loader2, Copy, Check, Pin, Shield, Lock, Plug,
|
|
5
5
|
Plus, Terminal, Boxes, Activity, GitBranch, Sparkles, SlidersHorizontal, Zap,
|
|
6
|
-
LayoutDashboard, ChevronRight, ExternalLink, Download, AlertTriangle,
|
|
6
|
+
LayoutDashboard, ChevronRight, ExternalLink, Download, AlertTriangle, Coins,
|
|
7
7
|
type LucideIcon,
|
|
8
8
|
} from 'lucide-react'
|
|
9
9
|
import { clsx } from 'clsx'
|
|
10
|
+
import { useQueryClient } from '@tanstack/react-query'
|
|
10
11
|
import { useAnimatedUnmount } from '../../hooks/useAnimatedUnmount'
|
|
11
12
|
import { TRANSITION_BACKDROP, TRANSITION_PANEL } from '../../utils/animation'
|
|
12
13
|
import { apiUrl, getAuthHeaders, getCredentialsMode, routePath } from '../../api/config'
|
|
13
14
|
import {
|
|
14
|
-
useCloudRole, useVersionCheck, useClusterInfo, usePrometheusStatus, useArgoStatus,
|
|
15
|
+
useCloudRole, useVersionCheck, useClusterInfo, usePrometheusStatus, useArgoStatus, useCapabilities,
|
|
15
16
|
} from '../../api/client'
|
|
16
17
|
import { useCapabilitiesContext } from '../../contexts/CapabilitiesContext'
|
|
17
|
-
import { Input } from '@skyhook-io/k8s-ui'
|
|
18
|
+
import { Input, SelectMenu } from '@skyhook-io/k8s-ui'
|
|
18
19
|
import { Tooltip } from '../ui/Tooltip'
|
|
19
20
|
import { AISettingsSection, type AIDraft } from '../diagnose/AISettings'
|
|
20
21
|
import { MyPermissionsContent } from './MyPermissionsDialog'
|
|
21
22
|
import { useDiagnose } from '../diagnose/DiagnoseContext'
|
|
23
|
+
import { currencyOptionsForValue } from './currency-options'
|
|
24
|
+
import { versionUpdateURL } from '../../utils/version'
|
|
22
25
|
|
|
23
26
|
// The loopback URL an MCP client is told to connect to. Shared by the overview
|
|
24
27
|
// row and the MCP section: both must carry the base path, or the URL they
|
|
@@ -39,15 +42,18 @@ interface Config {
|
|
|
39
42
|
timelineDbPath?: string
|
|
40
43
|
historyLimit?: number
|
|
41
44
|
prometheusUrl?: string
|
|
45
|
+
opencostCurrency?: string
|
|
42
46
|
argoCdUrl?: string
|
|
43
47
|
argoCdInsecureTls?: boolean
|
|
44
48
|
mcp?: boolean | null
|
|
49
|
+
restoreLastDesktopContext?: boolean | null
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
interface ConfigResponse {
|
|
48
53
|
file: Config
|
|
49
54
|
effective: Config
|
|
50
55
|
isDesktop: boolean
|
|
56
|
+
openCostCurrencyManaged?: boolean
|
|
51
57
|
prometheusHeaderKeys?: string[]
|
|
52
58
|
// True when an Argo CD auth token is stored. The token itself is never
|
|
53
59
|
// returned — the card shows a "configured" placeholder and omits the token
|
|
@@ -72,17 +78,19 @@ interface SettingsDialogProps {
|
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
// The settings surface splits into three honest apply buckets:
|
|
75
|
-
// •
|
|
76
|
-
// owner-gated footer
|
|
81
|
+
// • Persisted config (kubeconfig, server, timeline, MCP, cost currency) —
|
|
82
|
+
// saved by the owner-gated footer. Currency applies live unless a startup
|
|
83
|
+
// flag owns it; the rest restart.
|
|
77
84
|
// • Live integrations (Prometheus, Argo CD) — their own Apply/Connect endpoints
|
|
78
85
|
// re-point the running server; effect immediately, NOT part of footer dirty.
|
|
79
86
|
// • AI diagnose — client-side prefs, self-saving, editable by everyone.
|
|
80
87
|
export type SettingsSectionId =
|
|
81
|
-
| 'overview' | 'perms' | 'connection' | 'prometheus' | 'argocd' | 'ai' | 'advanced'
|
|
88
|
+
| 'overview' | 'perms' | 'connection' | 'prometheus' | 'cost' | 'argocd' | 'ai' | 'advanced'
|
|
82
89
|
|
|
83
|
-
//
|
|
84
|
-
// argoCdUrl, argoCdInsecureTls) apply
|
|
85
|
-
//
|
|
90
|
+
// Persisted footer fields include startup settings plus the live currency override.
|
|
91
|
+
// Integration fields (prometheusUrl, argoCdUrl, argoCdInsecureTls) apply through
|
|
92
|
+
// their own controls and are excluded here. Every field is normalized so
|
|
93
|
+
// unset≡default doesn't read as a change.
|
|
86
94
|
function normalizeStartup(c: Config) {
|
|
87
95
|
return {
|
|
88
96
|
kubeconfig: c.kubeconfig ?? '',
|
|
@@ -95,6 +103,8 @@ function normalizeStartup(c: Config) {
|
|
|
95
103
|
timelineDbPath: c.timelineDbPath ?? '',
|
|
96
104
|
historyLimit: c.historyLimit ?? null,
|
|
97
105
|
mcp: c.mcp ?? true,
|
|
106
|
+
opencostCurrency: c.opencostCurrency?.trim().toUpperCase() ?? '',
|
|
107
|
+
restoreLastDesktopContext: c.restoreLastDesktopContext ?? true,
|
|
98
108
|
}
|
|
99
109
|
}
|
|
100
110
|
|
|
@@ -103,6 +113,7 @@ export function SettingsDialog({
|
|
|
103
113
|
onClose,
|
|
104
114
|
initialSection = 'overview',
|
|
105
115
|
}: SettingsDialogProps) {
|
|
116
|
+
const queryClient = useQueryClient()
|
|
106
117
|
const dialogRef = useRef<HTMLDivElement>(null)
|
|
107
118
|
const { shouldRender, isOpen } = useAnimatedUnmount(open, 200)
|
|
108
119
|
const { data: versionInfo } = useVersionCheck()
|
|
@@ -122,6 +133,9 @@ export function SettingsDialog({
|
|
|
122
133
|
const [loadError, setLoadError] = useState<string | null>(null)
|
|
123
134
|
const [section, setSection] = useState<SettingsSectionId>('overview')
|
|
124
135
|
const [confirmingClose, setConfirmingClose] = useState(false)
|
|
136
|
+
const { data: argoSectionStatus, refetch: refetchArgoSectionStatus } = useArgoStatus(
|
|
137
|
+
open && section === 'argocd'
|
|
138
|
+
)
|
|
125
139
|
|
|
126
140
|
// AI Diagnosis prefs are client-side (localStorage) and now SELF-SAVING: the
|
|
127
141
|
// section has its own Save that commits the draft to DiagnoseContext, so it's
|
|
@@ -148,7 +162,8 @@ export function SettingsDialog({
|
|
|
148
162
|
const clusterDirty =
|
|
149
163
|
edN.kubeconfig !== svN.kubeconfig ||
|
|
150
164
|
edN.kubeconfigDirs !== svN.kubeconfigDirs ||
|
|
151
|
-
edN.namespace !== svN.namespace
|
|
165
|
+
edN.namespace !== svN.namespace ||
|
|
166
|
+
edN.restoreLastDesktopContext !== svN.restoreLastDesktopContext
|
|
152
167
|
const serverDirty =
|
|
153
168
|
edN.port !== svN.port || edN.noBrowser !== svN.noBrowser || edN.browser !== svN.browser
|
|
154
169
|
const mcpDirty = edN.mcp !== svN.mcp
|
|
@@ -156,10 +171,11 @@ export function SettingsDialog({
|
|
|
156
171
|
edN.timelineStorage !== svN.timelineStorage ||
|
|
157
172
|
edN.timelineDbPath !== svN.timelineDbPath ||
|
|
158
173
|
edN.historyLimit !== svN.historyLimit
|
|
174
|
+
const costDirty = edN.opencostCurrency !== svN.opencostCurrency
|
|
159
175
|
// Merged-pane dirty for the flat nav (Connection = cluster+server, Advanced = mcp+timeline).
|
|
160
176
|
const connectionDirty = clusterDirty || serverDirty
|
|
161
177
|
const advancedDirty = mcpDirty || timelineDirty
|
|
162
|
-
const
|
|
178
|
+
const configDirty = configData != null && (connectionDirty || costDirty || advancedDirty)
|
|
163
179
|
|
|
164
180
|
// Load config on open + snapshot AI prefs + pick a default section that's
|
|
165
181
|
// actually accessible to the current identity.
|
|
@@ -237,9 +253,27 @@ export function SettingsDialog({
|
|
|
237
253
|
setSaveMessage(`Error: ${data?.error || res.statusText}`)
|
|
238
254
|
return false
|
|
239
255
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
256
|
+
const saved = await res.json() as Config
|
|
257
|
+
const committed = { ...body, opencostCurrency: saved.opencostCurrency }
|
|
258
|
+
setEditedConfig((prev) => ({ ...prev, opencostCurrency: saved.opencostCurrency }))
|
|
259
|
+
setConfigData((prev) => (prev ? { ...prev, file: committed } : prev))
|
|
260
|
+
if (costDirty && !configData.openCostCurrencyManaged) {
|
|
261
|
+
void queryClient.invalidateQueries({
|
|
262
|
+
predicate: (query) =>
|
|
263
|
+
typeof query.queryKey[0] === 'string' && query.queryKey[0].startsWith('opencost-'),
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
if (costDirty && configData.openCostCurrencyManaged) {
|
|
267
|
+
setSaveMessage(connectionDirty || advancedDirty
|
|
268
|
+
? 'Saved. CLI/Helm currency remains active; restart without that override to apply it. Restart Radar for other changes.'
|
|
269
|
+
: 'Saved. CLI/Helm currency remains active; restart without that override to apply this setting.')
|
|
270
|
+
} else {
|
|
271
|
+
setSaveMessage(connectionDirty || advancedDirty
|
|
272
|
+
? costDirty
|
|
273
|
+
? 'Saved. Currency applied immediately; restart Radar for other changes.'
|
|
274
|
+
: 'Saved. Restart Radar to apply.'
|
|
275
|
+
: 'Saved. Applied immediately.')
|
|
276
|
+
}
|
|
243
277
|
return true
|
|
244
278
|
} catch (err) {
|
|
245
279
|
setSaveMessage(`Error: ${err}`)
|
|
@@ -247,7 +281,7 @@ export function SettingsDialog({
|
|
|
247
281
|
} finally {
|
|
248
282
|
setSaving(false)
|
|
249
283
|
}
|
|
250
|
-
}, [editedConfig, configData])
|
|
284
|
+
}, [editedConfig, configData, costDirty, connectionDirty, advancedDirty, queryClient])
|
|
251
285
|
|
|
252
286
|
// AI prefs are client-side (localStorage) — commit the staged draft now.
|
|
253
287
|
// setSelectedAgent clears model/effort (they're agent-specific), so set the
|
|
@@ -277,24 +311,26 @@ export function SettingsDialog({
|
|
|
277
311
|
|
|
278
312
|
// Close guard: a pending startup edit prompts an inline confirm rather than
|
|
279
313
|
// silently discarding. An unsaved AI draft is re-derivable, so it's fine to
|
|
280
|
-
// drop it on close.
|
|
314
|
+
// drop it on close.
|
|
281
315
|
const requestCloseRef = useRef<() => void>(() => {})
|
|
282
316
|
requestCloseRef.current = () => {
|
|
283
|
-
if (canEditConfig &&
|
|
317
|
+
if (canEditConfig && configDirty) setConfirmingClose(true)
|
|
284
318
|
else onClose()
|
|
285
319
|
}
|
|
286
320
|
|
|
287
|
-
// ESC key
|
|
288
321
|
useEffect(() => {
|
|
289
322
|
if (!open) return
|
|
290
|
-
const
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
323
|
+
const handleDocumentKeyDown = (event: KeyboardEvent) => {
|
|
324
|
+
if (event.key !== 'Escape') return
|
|
325
|
+
const modalDialogs = Array.from(document.querySelectorAll<HTMLElement>('[role="dialog"][aria-modal="true"]'))
|
|
326
|
+
const topDialog = modalDialogs[modalDialogs.length - 1]
|
|
327
|
+
if (topDialog !== dialogRef.current || dialogRef.current?.contains(event.target as Node)) return
|
|
328
|
+
event.preventDefault()
|
|
329
|
+
event.stopPropagation()
|
|
330
|
+
requestCloseRef.current()
|
|
295
331
|
}
|
|
296
|
-
document.addEventListener('keydown',
|
|
297
|
-
return () => document.removeEventListener('keydown',
|
|
332
|
+
document.addEventListener('keydown', handleDocumentKeyDown, true)
|
|
333
|
+
return () => document.removeEventListener('keydown', handleDocumentKeyDown, true)
|
|
298
334
|
}, [open])
|
|
299
335
|
|
|
300
336
|
// Post-save feedback ("Saved. Restart Radar to apply.") is scoped to the
|
|
@@ -326,12 +362,13 @@ export function SettingsDialog({
|
|
|
326
362
|
{ id: 'perms', label: 'My permissions', icon: Shield, ownerOnly: false, dirty: false },
|
|
327
363
|
{ id: 'connection', label: 'Connection', icon: Boxes, ownerOnly: true, dirty: connectionDirty },
|
|
328
364
|
{ id: 'prometheus', label: 'Prometheus', icon: Activity, ownerOnly: true, dirty: false },
|
|
365
|
+
{ id: 'cost', label: 'Cost', icon: Coins, ownerOnly: true, dirty: costDirty },
|
|
329
366
|
{ id: 'argocd', label: 'Argo CD', icon: GitBranch, ownerOnly: true, dirty: false },
|
|
330
367
|
{ id: 'ai', label: 'AI diagnose', icon: Sparkles, ownerOnly: false, dirty: aiDirty },
|
|
331
368
|
{ id: 'advanced', label: 'Advanced', icon: SlidersHorizontal, ownerOnly: true, dirty: advancedDirty },
|
|
332
369
|
]
|
|
333
370
|
|
|
334
|
-
const showFooter = canEditConfig && (confirmingClose ||
|
|
371
|
+
const showFooter = canEditConfig && (confirmingClose || configDirty || !!saveMessage)
|
|
335
372
|
|
|
336
373
|
return createPortal(
|
|
337
374
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
@@ -348,7 +385,16 @@ export function SettingsDialog({
|
|
|
348
385
|
{/* Dialog */}
|
|
349
386
|
<div
|
|
350
387
|
ref={dialogRef}
|
|
388
|
+
role="dialog"
|
|
389
|
+
aria-modal="true"
|
|
390
|
+
aria-labelledby="settings-dialog-title"
|
|
351
391
|
tabIndex={-1}
|
|
392
|
+
onKeyDown={(event) => {
|
|
393
|
+
if (event.key !== 'Escape') return
|
|
394
|
+
event.preventDefault()
|
|
395
|
+
event.stopPropagation()
|
|
396
|
+
requestCloseRef.current()
|
|
397
|
+
}}
|
|
352
398
|
className={clsx(
|
|
353
399
|
'relative bg-theme-surface border border-theme-border shadow-theme-lg w-full outline-none flex flex-col',
|
|
354
400
|
'max-sm:inset-0 max-sm:absolute max-sm:rounded-none max-sm:max-h-full max-sm:border-0',
|
|
@@ -365,7 +411,7 @@ export function SettingsDialog({
|
|
|
365
411
|
<div className="flex items-center gap-2">
|
|
366
412
|
<Settings className="w-5 h-5 text-theme-text-secondary" />
|
|
367
413
|
<div className="flex items-baseline gap-2">
|
|
368
|
-
<h2 className="text-lg font-semibold text-theme-text-primary">Settings</h2>
|
|
414
|
+
<h2 id="settings-dialog-title" className="text-lg font-semibold text-theme-text-primary">Settings</h2>
|
|
369
415
|
<span className="text-[11px] text-theme-text-tertiary">
|
|
370
416
|
Radar{versionInfo?.currentVersion ? ` v${versionInfo.currentVersion}` : ''}
|
|
371
417
|
<span className="text-theme-text-disabled"> · by Skyhook</span>
|
|
@@ -462,6 +508,7 @@ export function SettingsDialog({
|
|
|
462
508
|
<ClusterSection
|
|
463
509
|
config={editedConfig}
|
|
464
510
|
effectiveConfig={configData?.effective}
|
|
511
|
+
isDesktop={isDesktop}
|
|
465
512
|
onChange={updateConfigField}
|
|
466
513
|
/>
|
|
467
514
|
</div>
|
|
@@ -498,6 +545,24 @@ export function SettingsDialog({
|
|
|
498
545
|
/>
|
|
499
546
|
</SectionPane>
|
|
500
547
|
|
|
548
|
+
<SectionPane
|
|
549
|
+
id="cost"
|
|
550
|
+
active={section}
|
|
551
|
+
title="Cost"
|
|
552
|
+
caption={configData?.openCostCurrencyManaged
|
|
553
|
+
? 'Saved to config. A CLI or Helm override is currently active.'
|
|
554
|
+
: 'Saved to config and applied immediately.'}
|
|
555
|
+
live={!configData?.openCostCurrencyManaged}
|
|
556
|
+
locked={!canEditConfig}
|
|
557
|
+
>
|
|
558
|
+
<CostSection
|
|
559
|
+
currency={editedConfig.opencostCurrency ?? ''}
|
|
560
|
+
managed={configData?.openCostCurrencyManaged ?? false}
|
|
561
|
+
effectiveCurrency={configData?.effective.opencostCurrency ?? ''}
|
|
562
|
+
onChange={(value) => updateConfigField('opencostCurrency', value || undefined)}
|
|
563
|
+
/>
|
|
564
|
+
</SectionPane>
|
|
565
|
+
|
|
501
566
|
{/* Argo CD — live */}
|
|
502
567
|
<SectionPane
|
|
503
568
|
id="argocd"
|
|
@@ -514,9 +579,14 @@ export function SettingsDialog({
|
|
|
514
579
|
envManaged={configData?.argoCdEnvManaged ?? false}
|
|
515
580
|
envError={configData?.argoCdEnvError}
|
|
516
581
|
cliSession={configData?.argoCdCliSession}
|
|
582
|
+
statusReason={
|
|
583
|
+
argoSectionStatus?.configured && !argoSectionStatus.connected
|
|
584
|
+
? argoSectionStatus.reason
|
|
585
|
+
: undefined
|
|
586
|
+
}
|
|
517
587
|
onChangeUrl={(v) => updateConfigField('argoCdUrl', v || undefined)}
|
|
518
588
|
onChangeInsecureTls={(v) => updateConfigField('argoCdInsecureTls', v || undefined)}
|
|
519
|
-
onApplied={({ url, insecureTls, tokenSet }) =>
|
|
589
|
+
onApplied={({ url, insecureTls, tokenSet }) => {
|
|
520
590
|
setConfigData((prev) =>
|
|
521
591
|
prev
|
|
522
592
|
? {
|
|
@@ -526,7 +596,8 @@ export function SettingsDialog({
|
|
|
526
596
|
}
|
|
527
597
|
: prev
|
|
528
598
|
)
|
|
529
|
-
|
|
599
|
+
void refetchArgoSectionStatus()
|
|
600
|
+
}}
|
|
530
601
|
/>
|
|
531
602
|
</SectionPane>
|
|
532
603
|
|
|
@@ -609,8 +680,8 @@ export function SettingsDialog({
|
|
|
609
680
|
</div>
|
|
610
681
|
</div>
|
|
611
682
|
|
|
612
|
-
{/* Footer — owner-gated
|
|
613
|
-
apply
|
|
683
|
+
{/* Footer — owner-gated persisted config. AI self-saves and integrations
|
|
684
|
+
apply separately. Shown whenever an edit is pending (any section),
|
|
614
685
|
while confirming a close, or briefly after a save. */}
|
|
615
686
|
<div
|
|
616
687
|
className={clsx(
|
|
@@ -653,7 +724,7 @@ export function SettingsDialog({
|
|
|
653
724
|
<Tooltip content="Discard unsaved changes and revert to the last saved values">
|
|
654
725
|
<button
|
|
655
726
|
onClick={discardChanges}
|
|
656
|
-
disabled={saving || !
|
|
727
|
+
disabled={saving || !configDirty}
|
|
657
728
|
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded-md transition-colors disabled:opacity-50 disabled:pointer-events-none"
|
|
658
729
|
>
|
|
659
730
|
<RotateCcw className="w-3.5 h-3.5" />
|
|
@@ -668,7 +739,7 @@ export function SettingsDialog({
|
|
|
668
739
|
</div>
|
|
669
740
|
<button
|
|
670
741
|
onClick={saveConfig}
|
|
671
|
-
disabled={saving || !
|
|
742
|
+
disabled={saving || !configDirty}
|
|
672
743
|
className="flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium btn-brand rounded-md"
|
|
673
744
|
>
|
|
674
745
|
{saving && <Loader2 className="w-3.5 h-3.5 animate-spin" />}
|
|
@@ -841,6 +912,8 @@ function OverviewPanel({ active, onNavigate }: { active: boolean; onNavigate: (s
|
|
|
841
912
|
const { data: cluster } = useClusterInfo()
|
|
842
913
|
const { data: prom } = usePrometheusStatus()
|
|
843
914
|
const { data: argo } = useArgoStatus(active)
|
|
915
|
+
const { data: capabilitiesData } = useCapabilities()
|
|
916
|
+
const deploymentMode = capabilitiesData ? (capabilitiesData.deployment?.mode ?? 'local') : undefined
|
|
844
917
|
const { data: version } = useVersionCheck()
|
|
845
918
|
const capabilities = useCapabilitiesContext()
|
|
846
919
|
const diag = useDiagnose()
|
|
@@ -872,7 +945,7 @@ function OverviewPanel({ active, onNavigate }: { active: boolean; onNavigate: (s
|
|
|
872
945
|
// token, not a transient reconnect — "Not reachable" matches Prometheus and
|
|
873
946
|
// doesn't imply it will recover on its own.
|
|
874
947
|
value: argo?.connected ? 'Connected' : argo?.configured ? 'Not reachable' : 'Not connected',
|
|
875
|
-
detail: argo?.connected ? argo.address :
|
|
948
|
+
detail: argo?.connected ? argo.address : argo?.reason,
|
|
876
949
|
},
|
|
877
950
|
{
|
|
878
951
|
id: 'advanced', icon: Zap, label: 'MCP',
|
|
@@ -897,9 +970,9 @@ function OverviewPanel({ active, onNavigate }: { active: boolean; onNavigate: (s
|
|
|
897
970
|
|
|
898
971
|
return (
|
|
899
972
|
<div className="space-y-4">
|
|
900
|
-
{version?.updateAvailable && (
|
|
973
|
+
{version?.updateAvailable && deploymentMode !== undefined && deploymentMode !== 'cloud' && (
|
|
901
974
|
<a
|
|
902
|
-
href={version.releaseUrl}
|
|
975
|
+
href={versionUpdateURL(deploymentMode, version.releaseUrl)}
|
|
903
976
|
target="_blank"
|
|
904
977
|
rel="noreferrer"
|
|
905
978
|
className="flex items-center gap-2 px-3 py-2 text-xs rounded-md border border-skyhook-500/30 bg-skyhook-500/10 hover:bg-skyhook-500/15 transition-colors"
|
|
@@ -986,25 +1059,30 @@ function AIUnavailableNotice() {
|
|
|
986
1059
|
function ClusterSection({
|
|
987
1060
|
config,
|
|
988
1061
|
effectiveConfig,
|
|
1062
|
+
isDesktop,
|
|
989
1063
|
onChange,
|
|
990
1064
|
}: {
|
|
991
1065
|
config: Config
|
|
992
1066
|
effectiveConfig?: Config
|
|
1067
|
+
isDesktop: boolean
|
|
993
1068
|
onChange: <K extends keyof Config>(field: K, value: Config[K]) => void
|
|
994
1069
|
}) {
|
|
1070
|
+
const kubeconfigDirs = effectiveConfig ? (effectiveConfig.kubeconfigDirs ?? []) : config.kubeconfigDirs
|
|
1071
|
+
const hasKubeconfigDirs = (kubeconfigDirs?.length ?? 0) > 0
|
|
1072
|
+
|
|
995
1073
|
return (
|
|
996
1074
|
<>
|
|
997
1075
|
<ConfigField
|
|
998
|
-
label="Kubeconfig"
|
|
999
|
-
help="
|
|
1076
|
+
label="Primary Kubeconfig"
|
|
1077
|
+
help="File path loaded before additional directories"
|
|
1000
1078
|
value={config.kubeconfig ?? ''}
|
|
1001
1079
|
effectiveValue={effectiveConfig?.kubeconfig}
|
|
1002
|
-
placeholder=
|
|
1080
|
+
placeholder={hasKubeconfigDirs ? 'No primary file configured' : '~/.kube/config'}
|
|
1003
1081
|
onChange={(v) => onChange('kubeconfig', v || undefined)}
|
|
1004
1082
|
/>
|
|
1005
1083
|
<ConfigArrayField
|
|
1006
1084
|
label="Kubeconfig Directories"
|
|
1007
|
-
help="
|
|
1085
|
+
help="Additional kubeconfig directories. Without a primary file, they replace KUBECONFIG"
|
|
1008
1086
|
value={config.kubeconfigDirs}
|
|
1009
1087
|
effectiveValue={effectiveConfig?.kubeconfigDirs}
|
|
1010
1088
|
placeholder="/path/to/dir1, /path/to/dir2"
|
|
@@ -1018,6 +1096,14 @@ function ClusterSection({
|
|
|
1018
1096
|
placeholder="All namespaces"
|
|
1019
1097
|
onChange={(v) => onChange('namespace', v || undefined)}
|
|
1020
1098
|
/>
|
|
1099
|
+
{isDesktop && (
|
|
1100
|
+
<ConfigToggle
|
|
1101
|
+
label="Reopen on the last used cluster"
|
|
1102
|
+
description="Come back to the cluster you were working in. Turn off to use your kubeconfig's current context on the next Desktop start."
|
|
1103
|
+
value={config.restoreLastDesktopContext ?? true}
|
|
1104
|
+
onChange={(v) => onChange('restoreLastDesktopContext', v ? undefined : false)}
|
|
1105
|
+
/>
|
|
1106
|
+
)}
|
|
1021
1107
|
</>
|
|
1022
1108
|
)
|
|
1023
1109
|
}
|
|
@@ -1108,6 +1194,46 @@ function TimelineSection({
|
|
|
1108
1194
|
)
|
|
1109
1195
|
}
|
|
1110
1196
|
|
|
1197
|
+
function CostSection({
|
|
1198
|
+
currency,
|
|
1199
|
+
managed,
|
|
1200
|
+
effectiveCurrency,
|
|
1201
|
+
onChange,
|
|
1202
|
+
}: {
|
|
1203
|
+
currency: string
|
|
1204
|
+
managed: boolean
|
|
1205
|
+
effectiveCurrency: string
|
|
1206
|
+
onChange: (value: string) => void
|
|
1207
|
+
}) {
|
|
1208
|
+
return (
|
|
1209
|
+
<div>
|
|
1210
|
+
<label className="mb-1 block text-sm font-medium text-theme-text-primary">
|
|
1211
|
+
Currency override
|
|
1212
|
+
</label>
|
|
1213
|
+
<p className="mb-1 text-xs text-theme-text-tertiary">
|
|
1214
|
+
Choose a currency, or use Auto to read <code>currencyCode</code> or{' '}
|
|
1215
|
+
<code>DISPLAY_CURRENCY</code> from an active OpenCost/Kubecost installation, then fall back
|
|
1216
|
+
to USD. A custom Prometheus URL disables detection. Radar labels values but does not convert
|
|
1217
|
+
them.
|
|
1218
|
+
</p>
|
|
1219
|
+
<SelectMenu
|
|
1220
|
+
value={currency}
|
|
1221
|
+
options={currencyOptionsForValue(currency)}
|
|
1222
|
+
onChange={onChange}
|
|
1223
|
+
ariaLabel="Currency override"
|
|
1224
|
+
searchPlaceholder="Search currencies by name or code"
|
|
1225
|
+
className="w-full"
|
|
1226
|
+
/>
|
|
1227
|
+
{managed && (
|
|
1228
|
+
<p className="mt-1 text-xs text-amber-600 dark:text-amber-400/80">
|
|
1229
|
+
Currently managed by CLI or Helm: {effectiveCurrency || 'Auto'}. Saved changes apply
|
|
1230
|
+
after Radar starts without that override.
|
|
1231
|
+
</p>
|
|
1232
|
+
)}
|
|
1233
|
+
</div>
|
|
1234
|
+
)
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1111
1237
|
// -- MCP Section --------------------------------------------------------------
|
|
1112
1238
|
|
|
1113
1239
|
function MCPSection({
|
|
@@ -1440,6 +1566,7 @@ function ArgoCDConfigField({
|
|
|
1440
1566
|
envManaged,
|
|
1441
1567
|
envError,
|
|
1442
1568
|
cliSession,
|
|
1569
|
+
statusReason,
|
|
1443
1570
|
onChangeUrl,
|
|
1444
1571
|
onChangeInsecureTls,
|
|
1445
1572
|
onApplied,
|
|
@@ -1450,6 +1577,7 @@ function ArgoCDConfigField({
|
|
|
1450
1577
|
envManaged?: boolean
|
|
1451
1578
|
envError?: string
|
|
1452
1579
|
cliSession?: { server: string; user: string; insecure?: boolean }
|
|
1580
|
+
statusReason?: string
|
|
1453
1581
|
onChangeUrl: (value: string) => void
|
|
1454
1582
|
onChangeInsecureTls: (value: boolean) => void
|
|
1455
1583
|
onApplied?: (v: { url: string; insecureTls: boolean; tokenSet: boolean }) => void
|
|
@@ -1463,6 +1591,7 @@ function ArgoCDConfigField({
|
|
|
1463
1591
|
insecureTls={insecureTls}
|
|
1464
1592
|
tokenSet={tokenSet}
|
|
1465
1593
|
cliSession={cliSession}
|
|
1594
|
+
statusReason={statusReason}
|
|
1466
1595
|
onChangeUrl={onChangeUrl}
|
|
1467
1596
|
onChangeInsecureTls={onChangeInsecureTls}
|
|
1468
1597
|
onApplied={onApplied}
|
|
@@ -1555,6 +1684,7 @@ function ArgoCDEditableField({
|
|
|
1555
1684
|
insecureTls,
|
|
1556
1685
|
tokenSet,
|
|
1557
1686
|
cliSession,
|
|
1687
|
+
statusReason,
|
|
1558
1688
|
onChangeUrl,
|
|
1559
1689
|
onChangeInsecureTls,
|
|
1560
1690
|
onApplied,
|
|
@@ -1563,6 +1693,7 @@ function ArgoCDEditableField({
|
|
|
1563
1693
|
insecureTls: boolean
|
|
1564
1694
|
tokenSet: boolean
|
|
1565
1695
|
cliSession?: { server: string; user: string; insecure?: boolean }
|
|
1696
|
+
statusReason?: string
|
|
1566
1697
|
onChangeUrl: (value: string) => void
|
|
1567
1698
|
onChangeInsecureTls: (value: boolean) => void
|
|
1568
1699
|
onApplied?: (v: { url: string; insecureTls: boolean; tokenSet: boolean }) => void
|
|
@@ -1665,6 +1796,16 @@ function ArgoCDEditableField({
|
|
|
1665
1796
|
back to a lighter annotation-based drift that can miss fields.
|
|
1666
1797
|
</p>
|
|
1667
1798
|
|
|
1799
|
+
{statusReason && state.status !== 'connected' && (
|
|
1800
|
+
<div className="mb-3 rounded-md border border-theme-border bg-theme-elevated p-3">
|
|
1801
|
+
<p className="flex items-center gap-1.5 text-sm font-medium text-warning-text">
|
|
1802
|
+
<AlertTriangle className="w-3.5 h-3.5 shrink-0" />
|
|
1803
|
+
Argo CD token needs attention
|
|
1804
|
+
</p>
|
|
1805
|
+
<p className="mt-1 text-xs text-theme-text-secondary">{statusReason}</p>
|
|
1806
|
+
</div>
|
|
1807
|
+
)}
|
|
1808
|
+
|
|
1668
1809
|
<label className="block text-sm font-medium text-theme-text-primary mb-1">Server URL</label>
|
|
1669
1810
|
<p className="text-xs text-theme-text-tertiary mb-1">
|
|
1670
1811
|
Leave blank to auto-discover the argocd-server in this cluster, or enter its API URL.
|