@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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { useEffect, useId, useState, type ReactNode } from 'react'
|
|
2
|
+
import { Input, Disclosure, SelectMenu } from '@skyhook-io/k8s-ui'
|
|
3
|
+
import { FormSaveActions, type FormFeedback } from './FormSaveActions'
|
|
4
|
+
import { COST_SOURCE_OPTIONS } from '../cost/source'
|
|
5
|
+
|
|
6
|
+
export interface SecretEdit {
|
|
7
|
+
action: 'keep' | 'set' | 'clear'
|
|
8
|
+
value?: string
|
|
9
|
+
}
|
|
10
|
+
export interface ArgoConnectionDraft {
|
|
11
|
+
url: string
|
|
12
|
+
insecureTls: boolean
|
|
13
|
+
secret?: SecretEdit
|
|
14
|
+
useCliToken?: boolean
|
|
15
|
+
}
|
|
16
|
+
export interface CostConnectionDraft {
|
|
17
|
+
url: string
|
|
18
|
+
mode: 'auto' | 'prometheus' | 'kubecost'
|
|
19
|
+
clusterId: string
|
|
20
|
+
secret?: SecretEdit
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function CredentialField({
|
|
24
|
+
label,
|
|
25
|
+
saved,
|
|
26
|
+
value,
|
|
27
|
+
onChange
|
|
28
|
+
}: {
|
|
29
|
+
label: string
|
|
30
|
+
saved: boolean
|
|
31
|
+
value: SecretEdit
|
|
32
|
+
onChange: (value: SecretEdit) => void
|
|
33
|
+
}) {
|
|
34
|
+
const id = useId()
|
|
35
|
+
return (
|
|
36
|
+
<section className="space-y-2 border-t border-theme-border pt-4">
|
|
37
|
+
<label
|
|
38
|
+
htmlFor={id}
|
|
39
|
+
className="block text-sm font-medium text-theme-text-primary"
|
|
40
|
+
>
|
|
41
|
+
{label}
|
|
42
|
+
</label>
|
|
43
|
+
<div className="flex items-center gap-2">
|
|
44
|
+
<Input
|
|
45
|
+
id={id}
|
|
46
|
+
type="password"
|
|
47
|
+
autoComplete="new-password"
|
|
48
|
+
spellCheck={false}
|
|
49
|
+
value={value.value ?? ''}
|
|
50
|
+
onChange={(e) =>
|
|
51
|
+
onChange({
|
|
52
|
+
action: e.target.value ? 'set' : 'keep',
|
|
53
|
+
value: e.target.value || undefined
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
placeholder={
|
|
57
|
+
value.action === 'clear'
|
|
58
|
+
? 'Will be removed'
|
|
59
|
+
: saved
|
|
60
|
+
? 'Saved value'
|
|
61
|
+
: 'Optional'
|
|
62
|
+
}
|
|
63
|
+
className="block w-full min-w-0 px-3 py-2 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 flex-1"
|
|
64
|
+
/>
|
|
65
|
+
{saved && (
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
className="text-xs text-accent-text hover:underline"
|
|
69
|
+
onClick={() =>
|
|
70
|
+
onChange(
|
|
71
|
+
value.action === 'keep'
|
|
72
|
+
? { action: 'clear' }
|
|
73
|
+
: { action: 'keep' }
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
>
|
|
77
|
+
{value.action === 'keep' ? 'Remove' : 'Undo'}
|
|
78
|
+
</button>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
82
|
+
{value.action === 'clear'
|
|
83
|
+
? 'Will be removed when you save.'
|
|
84
|
+
: 'Leave unchanged to keep the saved value.'}
|
|
85
|
+
</p>
|
|
86
|
+
</section>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function ArgoCDConnectionForm({
|
|
91
|
+
value,
|
|
92
|
+
secretSet,
|
|
93
|
+
cliSession,
|
|
94
|
+
onChange,
|
|
95
|
+
onApply,
|
|
96
|
+
dirty,
|
|
97
|
+
onDiscard,
|
|
98
|
+
connectionAction,
|
|
99
|
+
feedback,
|
|
100
|
+
onDirtyChange
|
|
101
|
+
}: {
|
|
102
|
+
value: ArgoConnectionDraft
|
|
103
|
+
secretSet: boolean
|
|
104
|
+
cliSession?: { server: string; user: string; insecure?: boolean }
|
|
105
|
+
onChange: (value: ArgoConnectionDraft) => void
|
|
106
|
+
onApply: (value: ArgoConnectionDraft) => Promise<void | boolean>
|
|
107
|
+
dirty: boolean
|
|
108
|
+
onDiscard: () => void
|
|
109
|
+
connectionAction?: ReactNode
|
|
110
|
+
feedback?: FormFeedback
|
|
111
|
+
onDirtyChange?: (dirty: boolean) => void
|
|
112
|
+
}) {
|
|
113
|
+
const id = useId()
|
|
114
|
+
const [secret, setSecret] = useState<SecretEdit>({ action: 'keep' })
|
|
115
|
+
const [busy, setBusy] = useState(false)
|
|
116
|
+
const [error, setError] = useState('')
|
|
117
|
+
useEffect(
|
|
118
|
+
() => onDirtyChange?.(secret.action !== 'keep'),
|
|
119
|
+
[secret, onDirtyChange]
|
|
120
|
+
)
|
|
121
|
+
const apply = async (useCliToken = false) => {
|
|
122
|
+
setBusy(true)
|
|
123
|
+
setError('')
|
|
124
|
+
try {
|
|
125
|
+
const applied = await onApply({
|
|
126
|
+
...value,
|
|
127
|
+
...(useCliToken ? { useCliToken: true } : { secret })
|
|
128
|
+
})
|
|
129
|
+
if (applied === false) return
|
|
130
|
+
setSecret({ action: 'keep' })
|
|
131
|
+
} catch (error) {
|
|
132
|
+
setError(error instanceof Error ? error.message : String(error))
|
|
133
|
+
} finally {
|
|
134
|
+
setBusy(false)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return (
|
|
138
|
+
<fieldset disabled={busy} className="min-w-0 space-y-4">
|
|
139
|
+
<p className="text-sm text-theme-text-secondary">
|
|
140
|
+
Compare Git configuration with live resources and show Argo CD health.
|
|
141
|
+
Auto-discovery works without setup where anonymous reads are allowed.
|
|
142
|
+
</p>
|
|
143
|
+
<div className="space-y-1">
|
|
144
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
145
|
+
<label
|
|
146
|
+
htmlFor={id}
|
|
147
|
+
className="block text-sm font-medium text-theme-text-primary"
|
|
148
|
+
>
|
|
149
|
+
Argo CD server URL
|
|
150
|
+
</label>
|
|
151
|
+
</div>
|
|
152
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
153
|
+
Reachable from Radar. Leave empty to discover the server in this
|
|
154
|
+
cluster.
|
|
155
|
+
</p>
|
|
156
|
+
<Input
|
|
157
|
+
className="block w-full min-w-0 px-3 py-2 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"
|
|
158
|
+
id={id}
|
|
159
|
+
value={value.url}
|
|
160
|
+
onChange={(e) => onChange({ ...value, url: e.target.value })}
|
|
161
|
+
placeholder="Auto-discover, or https://argocd.example.com"
|
|
162
|
+
/>
|
|
163
|
+
{connectionAction}
|
|
164
|
+
</div>
|
|
165
|
+
<CredentialField
|
|
166
|
+
label="API token"
|
|
167
|
+
saved={secretSet}
|
|
168
|
+
value={secret}
|
|
169
|
+
onChange={setSecret}
|
|
170
|
+
/>
|
|
171
|
+
{cliSession && (
|
|
172
|
+
<div className="text-xs text-theme-text-secondary space-y-1">
|
|
173
|
+
<p>
|
|
174
|
+
CLI session available: {cliSession.user} at {cliSession.server}
|
|
175
|
+
</p>
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
disabled={!value.url.trim()}
|
|
179
|
+
onClick={() => void apply(true)}
|
|
180
|
+
className="text-accent-text hover:underline disabled:opacity-50"
|
|
181
|
+
>
|
|
182
|
+
Connect using CLI token
|
|
183
|
+
</button>
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
<label className="flex items-center gap-2 text-xs text-theme-text-secondary">
|
|
187
|
+
<input
|
|
188
|
+
type="checkbox"
|
|
189
|
+
checked={value.insecureTls}
|
|
190
|
+
onChange={(e) =>
|
|
191
|
+
onChange({ ...value, insecureTls: e.target.checked })
|
|
192
|
+
}
|
|
193
|
+
/>
|
|
194
|
+
Skip TLS verification (self-signed server)
|
|
195
|
+
</label>
|
|
196
|
+
{value.insecureTls && (
|
|
197
|
+
<p className="text-xs text-warning-text">
|
|
198
|
+
Server identity will not be verified. Use only for a trusted server.
|
|
199
|
+
</p>
|
|
200
|
+
)}
|
|
201
|
+
{value.url.startsWith('http://') &&
|
|
202
|
+
(secretSet || secret.action === 'set') && (
|
|
203
|
+
<p className="text-xs text-warning-text">
|
|
204
|
+
Credentials will travel over unencrypted HTTP. Prefer HTTPS outside
|
|
205
|
+
a trusted private network.
|
|
206
|
+
</p>
|
|
207
|
+
)}
|
|
208
|
+
<FormSaveActions
|
|
209
|
+
dirty={dirty}
|
|
210
|
+
busy={busy}
|
|
211
|
+
onSave={() => void apply()}
|
|
212
|
+
onDiscard={onDiscard}
|
|
213
|
+
feedback={feedback}
|
|
214
|
+
error={error}
|
|
215
|
+
/>
|
|
216
|
+
</fieldset>
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function CostConnectionForm({
|
|
221
|
+
value,
|
|
222
|
+
secretSet,
|
|
223
|
+
onChange,
|
|
224
|
+
onApply,
|
|
225
|
+
dirty,
|
|
226
|
+
onDiscard,
|
|
227
|
+
connectionAction,
|
|
228
|
+
feedback,
|
|
229
|
+
onDirtyChange
|
|
230
|
+
}: {
|
|
231
|
+
value: CostConnectionDraft
|
|
232
|
+
secretSet: boolean
|
|
233
|
+
onChange: (value: CostConnectionDraft) => void
|
|
234
|
+
onApply: (value: CostConnectionDraft) => Promise<void | boolean>
|
|
235
|
+
dirty: boolean
|
|
236
|
+
onDiscard: () => void
|
|
237
|
+
connectionAction?: ReactNode
|
|
238
|
+
feedback?: FormFeedback
|
|
239
|
+
onDirtyChange?: (dirty: boolean) => void
|
|
240
|
+
}) {
|
|
241
|
+
const id = useId()
|
|
242
|
+
const [secret, setSecret] = useState<SecretEdit>({ action: 'keep' })
|
|
243
|
+
const [busy, setBusy] = useState(false)
|
|
244
|
+
const [error, setError] = useState('')
|
|
245
|
+
const [overridesOpen, setOverridesOpen] = useState(
|
|
246
|
+
!!value.url || !!value.clusterId || secretSet
|
|
247
|
+
)
|
|
248
|
+
const automatic = value.mode === 'auto'
|
|
249
|
+
useEffect(
|
|
250
|
+
() => onDirtyChange?.(secret.action !== 'keep'),
|
|
251
|
+
[secret, onDirtyChange]
|
|
252
|
+
)
|
|
253
|
+
const apply = async () => {
|
|
254
|
+
setBusy(true)
|
|
255
|
+
setError('')
|
|
256
|
+
try {
|
|
257
|
+
const applied = await onApply({ ...value, secret })
|
|
258
|
+
if (applied === false) return
|
|
259
|
+
setSecret({ action: 'keep' })
|
|
260
|
+
} catch (error) {
|
|
261
|
+
setError(error instanceof Error ? error.message : String(error))
|
|
262
|
+
setOverridesOpen(true)
|
|
263
|
+
} finally {
|
|
264
|
+
setBusy(false)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return (
|
|
268
|
+
<fieldset disabled={busy} className="min-w-0 space-y-4">
|
|
269
|
+
<p className="text-sm text-theme-text-secondary">
|
|
270
|
+
Use OpenCost metrics from this cluster's metrics connection, or connect
|
|
271
|
+
to a Kubecost Aggregator.
|
|
272
|
+
</p>
|
|
273
|
+
<div className="space-y-1">
|
|
274
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
275
|
+
<label
|
|
276
|
+
htmlFor={`${id}-source`}
|
|
277
|
+
className="text-sm font-medium text-theme-text-primary"
|
|
278
|
+
>
|
|
279
|
+
Cost source
|
|
280
|
+
</label>
|
|
281
|
+
</div>
|
|
282
|
+
<SelectMenu
|
|
283
|
+
id={`${id}-source`}
|
|
284
|
+
ariaLabel="Cost source"
|
|
285
|
+
value={value.mode}
|
|
286
|
+
options={COST_SOURCE_OPTIONS}
|
|
287
|
+
onChange={(mode) => {
|
|
288
|
+
if (
|
|
289
|
+
mode === 'auto' &&
|
|
290
|
+
(value.url ||
|
|
291
|
+
value.clusterId ||
|
|
292
|
+
secretSet ||
|
|
293
|
+
secret.action !== 'keep')
|
|
294
|
+
)
|
|
295
|
+
setOverridesOpen(true)
|
|
296
|
+
onChange({
|
|
297
|
+
...value,
|
|
298
|
+
mode: mode as CostConnectionDraft['mode']
|
|
299
|
+
})
|
|
300
|
+
}}
|
|
301
|
+
className="w-full"
|
|
302
|
+
/>
|
|
303
|
+
{(value.mode === 'prometheus' || (automatic && !overridesOpen)) && connectionAction}
|
|
304
|
+
</div>
|
|
305
|
+
{value.mode !== 'prometheus' && (
|
|
306
|
+
<Disclosure
|
|
307
|
+
summary={
|
|
308
|
+
value.url ||
|
|
309
|
+
value.clusterId ||
|
|
310
|
+
secretSet ||
|
|
311
|
+
secret.action !== 'keep'
|
|
312
|
+
? 'Kubecost connection overrides · configured'
|
|
313
|
+
: 'Kubecost connection overrides (optional)'
|
|
314
|
+
}
|
|
315
|
+
summaryClassName={
|
|
316
|
+
automatic
|
|
317
|
+
? 'text-sm font-medium text-theme-text-secondary'
|
|
318
|
+
: 'hidden'
|
|
319
|
+
}
|
|
320
|
+
open={!automatic || overridesOpen}
|
|
321
|
+
onOpenChange={setOverridesOpen}
|
|
322
|
+
>
|
|
323
|
+
<div className={automatic ? 'space-y-4 pt-3' : 'space-y-4'}>
|
|
324
|
+
<div className="space-y-1">
|
|
325
|
+
<label
|
|
326
|
+
htmlFor={id}
|
|
327
|
+
className="block text-sm font-medium text-theme-text-primary"
|
|
328
|
+
>
|
|
329
|
+
Kubecost Aggregator URL
|
|
330
|
+
</label>
|
|
331
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
332
|
+
Leave empty for discovery in this cluster. Use the central
|
|
333
|
+
Aggregator URL for a federated setup.
|
|
334
|
+
</p>
|
|
335
|
+
<Input
|
|
336
|
+
className="block w-full min-w-0 px-3 py-2 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"
|
|
337
|
+
id={id}
|
|
338
|
+
value={value.url}
|
|
339
|
+
onChange={(e) => onChange({ ...value, url: e.target.value })}
|
|
340
|
+
placeholder="Auto-discover, or https://kubecost.example.com"
|
|
341
|
+
/>
|
|
342
|
+
{(!automatic || overridesOpen) && connectionAction}
|
|
343
|
+
</div>
|
|
344
|
+
<CredentialField
|
|
345
|
+
label="API key"
|
|
346
|
+
saved={secretSet}
|
|
347
|
+
value={secret}
|
|
348
|
+
onChange={setSecret}
|
|
349
|
+
/>
|
|
350
|
+
{value.url.startsWith('http://') &&
|
|
351
|
+
(secretSet || secret.action === 'set') && (
|
|
352
|
+
<p className="text-xs text-warning-text">
|
|
353
|
+
The API key will travel over unencrypted HTTP. Prefer HTTPS
|
|
354
|
+
outside a trusted private network.
|
|
355
|
+
</p>
|
|
356
|
+
)}
|
|
357
|
+
<section className="space-y-1 border-t border-theme-border pt-4">
|
|
358
|
+
<label
|
|
359
|
+
htmlFor={`${id}-cluster`}
|
|
360
|
+
className="block text-sm font-medium text-theme-text-primary"
|
|
361
|
+
>
|
|
362
|
+
Kubecost cluster ID
|
|
363
|
+
</label>
|
|
364
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
365
|
+
The FinOps Agent's CLUSTER_ID, not the kubeconfig context
|
|
366
|
+
name. Leave empty to detect it. Applies only to this cluster.
|
|
367
|
+
</p>
|
|
368
|
+
<Input
|
|
369
|
+
className="block w-full min-w-0 px-3 py-2 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"
|
|
370
|
+
id={`${id}-cluster`}
|
|
371
|
+
value={value.clusterId}
|
|
372
|
+
onChange={(e) =>
|
|
373
|
+
onChange({ ...value, clusterId: e.target.value })
|
|
374
|
+
}
|
|
375
|
+
placeholder="Detected automatically"
|
|
376
|
+
/>
|
|
377
|
+
</section>
|
|
378
|
+
</div>
|
|
379
|
+
</Disclosure>
|
|
380
|
+
)}
|
|
381
|
+
<FormSaveActions
|
|
382
|
+
dirty={dirty}
|
|
383
|
+
busy={busy}
|
|
384
|
+
onSave={() => void apply()}
|
|
385
|
+
onDiscard={onDiscard}
|
|
386
|
+
feedback={feedback}
|
|
387
|
+
error={error}
|
|
388
|
+
/>
|
|
389
|
+
</fieldset>
|
|
390
|
+
)
|
|
391
|
+
}
|