@skyhook-io/radar-app 1.15.0 → 1.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +247 -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 +223 -0
- package/src/api/radarFeatures.test.ts +142 -0
- package/src/api/radarFeatures.ts +148 -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 +44 -6
- 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
|
@@ -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
|
+
}
|