@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.
Files changed (89) hide show
  1. package/README.md +2 -0
  2. package/package.json +6 -5
  3. package/src/App.tsx +14 -11
  4. package/src/RadarApp.tsx +51 -21
  5. package/src/api/apiResources.test.ts +19 -1
  6. package/src/api/apiResources.ts +18 -3
  7. package/src/api/client.capacity.test.ts +15 -0
  8. package/src/api/client.trace.test.ts +9 -1
  9. package/src/api/client.ts +232 -201
  10. package/src/api/diagnose.test.ts +102 -1
  11. package/src/api/diagnose.ts +42 -11
  12. package/src/api/drain-plan.test.ts +0 -10
  13. package/src/api/httpErrors.test.ts +95 -0
  14. package/src/api/httpErrors.ts +101 -0
  15. package/src/api/policy.ts +33 -5
  16. package/src/api/radarFeatures.render.test.tsx +190 -0
  17. package/src/api/radarFeatures.test.ts +133 -0
  18. package/src/api/radarFeatures.ts +147 -0
  19. package/src/api/timelineSource.test.ts +2 -10
  20. package/src/api/timelineSource.ts +3 -2
  21. package/src/api/traffic.ts +4 -3
  22. package/src/components/CloudFunnelButton.tsx +9 -9
  23. package/src/components/NamespaceSwitcher.test.tsx +47 -0
  24. package/src/components/NamespaceSwitcher.tsx +17 -1
  25. package/src/components/SelfManagedStart.tsx +5 -6
  26. package/src/components/applications/ApplicationsView.tsx +20 -6
  27. package/src/components/audit/UpgradeReadinessView.test.ts +14 -6
  28. package/src/components/audit/UpgradeReadinessView.tsx +9 -13
  29. package/src/components/capacity/CapacityView.test.tsx +13 -4
  30. package/src/components/capacity/shared.tsx +14 -13
  31. package/src/components/cost/ApplicationCostTab.tsx +9 -1
  32. package/src/components/cost/CostConnectionAction.tsx +30 -0
  33. package/src/components/cost/CostTrendChart.tsx +2 -0
  34. package/src/components/cost/CostView.tsx +4 -24
  35. package/src/components/cost/WorkloadCostTab.tsx +7 -1
  36. package/src/components/cost/source.test.ts +30 -3
  37. package/src/components/cost/source.ts +37 -2
  38. package/src/components/curl/ServiceCurlButton.tsx +7 -3
  39. package/src/components/dock/NodeTerminalTab.tsx +2 -1
  40. package/src/components/dock/TerminalTab.tsx +2 -1
  41. package/src/components/dock/TrafficFlowListTab.tsx +2 -2
  42. package/src/components/gitops/GitOpsView.tsx +12 -0
  43. package/src/components/home/HomeView.tsx +1 -1
  44. package/src/components/home/MCPSetupDialog.tsx +37 -37
  45. package/src/components/resource/PVCUsageBar.render.test.tsx +16 -0
  46. package/src/components/resource/PVCUsageBar.tsx +4 -1
  47. package/src/components/resource/PrometheusChartsGrid.render.test.tsx +1 -0
  48. package/src/components/resource/PrometheusChartsGrid.tsx +4 -0
  49. package/src/components/resources/ImageFilesystemModal.tsx +2 -1
  50. package/src/components/resources/PodFilePreview.tsx +4 -1
  51. package/src/components/resources/PodFilesystemModal.tsx +3 -2
  52. package/src/components/resources/ResourcesView.tsx +2 -1
  53. package/src/components/rightsizing/RightsizingScanView.tsx +4 -1
  54. package/src/components/settings/ConnectionHeadersEditor.tsx +109 -0
  55. package/src/components/settings/FormSaveActions.tsx +76 -0
  56. package/src/components/settings/IntegrationConnectionForms.tsx +391 -0
  57. package/src/components/settings/LocalConfigurationDetails.tsx +389 -0
  58. package/src/components/settings/LocalConnectionSettings.test.tsx +189 -0
  59. package/src/components/settings/LocalConnectionSettings.tsx +1015 -0
  60. package/src/components/settings/LocalIntegrationStatus.tsx +50 -0
  61. package/src/components/settings/PrometheusConfigField.tsx +30 -0
  62. package/src/components/settings/PrometheusConnectionForm.tsx +295 -0
  63. package/src/components/settings/SettingsDialog.tsx +382 -521
  64. package/src/components/settings/currency-options.test.ts +1 -1
  65. package/src/components/settings/currency-options.ts +1 -1
  66. package/src/components/settings/settings-state.test.ts +17 -10
  67. package/src/components/settings/settings-state.ts +20 -12
  68. package/src/components/timeline/TimelineList.test.ts +56 -0
  69. package/src/components/timeline/TimelineList.tsx +7 -1
  70. package/src/components/traffic/TrafficFlowList.tsx +5 -21
  71. package/src/components/traffic/TrafficFlowListContext.tsx +6 -4
  72. package/src/components/traffic/TrafficGraph.tsx +79 -48
  73. package/src/components/traffic/TrafficView.tsx +61 -37
  74. package/src/components/traffic/TrafficWizard.tsx +4 -2
  75. package/src/components/traffic/trafficFilters.test.ts +154 -1
  76. package/src/components/traffic/trafficFilters.ts +141 -0
  77. package/src/components/ui/Omnibar.tsx +1 -1
  78. package/src/components/ui/UpdateNotification.tsx +57 -12
  79. package/src/components/workload/WorkloadView.tsx +36 -13
  80. package/src/components/workload/historyPaging.test.ts +72 -0
  81. package/src/components/workload/historyPaging.ts +147 -0
  82. package/src/components/workload/useHistoryPaging.test.tsx +81 -0
  83. package/src/context/ConnectionContext.tsx +2 -1
  84. package/src/context/RadarUpgrade.tsx +30 -0
  85. package/src/context/RadarUpgradeHost.tsx +23 -0
  86. package/src/hooks/usePreviousIntegrationSettings.render.test.tsx +62 -0
  87. package/src/hooks/usePreviousIntegrationSettings.test.ts +62 -0
  88. package/src/hooks/usePreviousIntegrationSettings.ts +89 -0
  89. 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
+ }