@skyhook-io/radar-app 1.6.2 → 1.8.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/package.json +8 -2
- package/src/App.tsx +110 -45
- package/src/api/client.ts +10 -6
- package/src/components/ConnectionErrorView.tsx +5 -2
- package/src/components/DebugOverlay.tsx +4 -2
- package/src/components/UserMenu.tsx +5 -2
- package/src/components/audit/AuditSettingsDialog.tsx +14 -10
- package/src/components/audit/AuditView.tsx +3 -1
- package/src/components/compare/useCompareLauncher.tsx +6 -3
- package/src/components/cost/CostView.tsx +13 -4
- package/src/components/helm/ChartBrowser.tsx +6 -4
- package/src/components/helm/HelmReleaseDrawer.tsx +25 -13
- package/src/components/helm/HelmView.tsx +7 -6
- package/src/components/helm/InstallWizard.tsx +3 -2
- package/src/components/helm/OwnedResources.tsx +24 -13
- package/src/components/helm/RevisionHistory.tsx +2 -1
- package/src/components/helm/ValuesViewer.tsx +4 -2
- package/src/components/home/ClusterHealthCard.tsx +21 -18
- package/src/components/home/HelmSummary.tsx +3 -1
- package/src/components/home/HomeView.tsx +9 -2
- package/src/components/home/MCPSetupDialog.tsx +10 -4
- package/src/components/nav/PrimaryNavRail.tsx +5 -2
- package/src/components/portforward/PortForwardButton.tsx +14 -8
- package/src/components/resource/PrometheusChartsGrid.tsx +9 -2
- package/src/components/resource/RestartChart.tsx +6 -5
- package/src/components/resource/RightsizingStrip.tsx +4 -1
- package/src/components/resource-drawer/ResourceDrawer.tsx +3 -1
- package/src/components/resources/ImageFilesystemModal.tsx +16 -8
- package/src/components/resources/PodFilesystemModal.tsx +4 -2
- package/src/components/settings/SettingsDialog.tsx +371 -141
- package/src/components/traffic/TrafficFilterSidebar.tsx +8 -43
- package/src/components/traffic/TrafficFlowList.tsx +13 -4
- package/src/components/traffic/TrafficGraph.tsx +8 -3
- package/src/components/traffic/TrafficView.tsx +4 -2
- package/src/components/ui/Omnibar.tsx +0 -1
- package/src/components/workload/WorkloadView.tsx +15 -10
- package/src/context/NavCustomization.tsx +48 -9
- package/src/index.ts +1 -1
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { useState, useEffect, useRef, useCallback, type ReactNode } from 'react'
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
|
-
import { Settings, X, RotateCcw, Loader2, Copy, Check, Pin, Shield, Lock } from 'lucide-react'
|
|
3
|
+
import { Settings, X, RotateCcw, RotateCw, Loader2, Copy, Check, Pin, Shield, Lock, Plug, Plus } from 'lucide-react'
|
|
4
4
|
import { clsx } from 'clsx'
|
|
5
5
|
import { useAnimatedUnmount } from '../../hooks/useAnimatedUnmount'
|
|
6
6
|
import { TRANSITION_BACKDROP, TRANSITION_PANEL } from '../../utils/animation'
|
|
7
7
|
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
|
|
8
8
|
import { useCloudRole, useVersionCheck } from '../../api/client'
|
|
9
9
|
import { useCapabilitiesContext } from '../../contexts/CapabilitiesContext'
|
|
10
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
10
11
|
import type { DeploymentMode } from '../../types'
|
|
11
12
|
|
|
12
13
|
interface Config {
|
|
@@ -27,6 +28,7 @@ interface ConfigResponse {
|
|
|
27
28
|
file: Config
|
|
28
29
|
effective: Config
|
|
29
30
|
isDesktop: boolean
|
|
31
|
+
prometheusHeaderKeys?: string[]
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
interface SettingsDialogProps {
|
|
@@ -156,7 +158,7 @@ export function SettingsDialog({ open, onClose, onShowMyPermissions }: SettingsD
|
|
|
156
158
|
className={clsx(
|
|
157
159
|
'relative bg-theme-surface border border-theme-border shadow-theme-lg w-full outline-none flex flex-col',
|
|
158
160
|
'max-sm:inset-0 max-sm:absolute max-sm:rounded-none max-sm:max-h-full max-sm:border-0',
|
|
159
|
-
'sm:rounded-xl sm:max-w-
|
|
161
|
+
'sm:rounded-xl sm:max-w-2xl sm:mx-4 sm:max-h-[85vh]',
|
|
160
162
|
TRANSITION_PANEL,
|
|
161
163
|
isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95'
|
|
162
164
|
)}
|
|
@@ -165,7 +167,13 @@ export function SettingsDialog({ open, onClose, onShowMyPermissions }: SettingsD
|
|
|
165
167
|
<div className="flex items-center justify-between p-4 border-b border-theme-border shrink-0">
|
|
166
168
|
<div className="flex items-center gap-2">
|
|
167
169
|
<Settings className="w-5 h-5 text-theme-text-secondary" />
|
|
168
|
-
<
|
|
170
|
+
<div className="flex items-baseline gap-2">
|
|
171
|
+
<h2 className="text-lg font-semibold text-theme-text-primary">Settings</h2>
|
|
172
|
+
<span className="text-[11px] text-theme-text-tertiary">
|
|
173
|
+
Radar{versionInfo?.currentVersion ? ` v${versionInfo.currentVersion}` : ''}
|
|
174
|
+
<span className="text-theme-text-disabled"> · by Skyhook</span>
|
|
175
|
+
</span>
|
|
176
|
+
</div>
|
|
169
177
|
</div>
|
|
170
178
|
<button
|
|
171
179
|
onClick={onClose}
|
|
@@ -212,6 +220,7 @@ export function SettingsDialog({ open, onClose, onShowMyPermissions }: SettingsD
|
|
|
212
220
|
effectiveConfig={configData?.effective}
|
|
213
221
|
isDesktop={isDesktop}
|
|
214
222
|
deploymentMode={deploymentMode}
|
|
223
|
+
prometheusHeaderKeys={configData?.prometheusHeaderKeys ?? []}
|
|
215
224
|
onChange={updateConfigField}
|
|
216
225
|
/>
|
|
217
226
|
) : (
|
|
@@ -233,17 +242,18 @@ export function SettingsDialog({ open, onClose, onShowMyPermissions }: SettingsD
|
|
|
233
242
|
the save controls entirely for non-owners (personal sections save
|
|
234
243
|
themselves). */}
|
|
235
244
|
{canEditConfig && (
|
|
236
|
-
<div className="flex items-center justify-between gap-3
|
|
245
|
+
<div className="flex items-center justify-between gap-3 px-4 py-2.5 border-t border-theme-border shrink-0">
|
|
237
246
|
<div className="flex items-center gap-2">
|
|
247
|
+
<Tooltip content="Clear all fields — reverts to defaults when saved">
|
|
238
248
|
<button
|
|
239
249
|
onClick={resetConfig}
|
|
240
250
|
disabled={saving}
|
|
241
|
-
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"
|
|
242
|
-
title="Reset all configuration to defaults"
|
|
251
|
+
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"
|
|
243
252
|
>
|
|
244
253
|
<RotateCcw className="w-3.5 h-3.5" />
|
|
245
254
|
Reset
|
|
246
255
|
</button>
|
|
256
|
+
</Tooltip>
|
|
247
257
|
{saveMessage && (
|
|
248
258
|
<span className={clsx(
|
|
249
259
|
'text-xs',
|
|
@@ -253,32 +263,22 @@ export function SettingsDialog({ open, onClose, onShowMyPermissions }: SettingsD
|
|
|
253
263
|
</span>
|
|
254
264
|
)}
|
|
255
265
|
</div>
|
|
256
|
-
<
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
266
|
+
<div className="flex items-center gap-3">
|
|
267
|
+
<span className="hidden sm:flex items-center gap-1.5 text-[11px] text-theme-text-tertiary">
|
|
268
|
+
<RotateCw className="w-3 h-3" />
|
|
269
|
+
Applies on next launch
|
|
270
|
+
</span>
|
|
271
|
+
<button
|
|
272
|
+
onClick={saveConfig}
|
|
273
|
+
disabled={saving || !configDirty}
|
|
274
|
+
className="flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium btn-brand rounded-md"
|
|
275
|
+
>
|
|
276
|
+
{saving && <Loader2 className="w-3.5 h-3.5 animate-spin" />}
|
|
277
|
+
Save
|
|
278
|
+
</button>
|
|
279
|
+
</div>
|
|
264
280
|
</div>
|
|
265
281
|
)}
|
|
266
|
-
|
|
267
|
-
{/* About — muted version footer (canonical "Settings → About"). */}
|
|
268
|
-
<div className="flex items-center justify-between gap-2 px-4 py-2 border-t border-theme-border/60 text-[11px] text-theme-text-tertiary shrink-0">
|
|
269
|
-
<span>
|
|
270
|
-
Radar{versionInfo?.currentVersion ? ` v${versionInfo.currentVersion}` : ''}
|
|
271
|
-
<span className="text-theme-text-disabled"> · by Skyhook</span>
|
|
272
|
-
</span>
|
|
273
|
-
<a
|
|
274
|
-
href="https://github.com/skyhook-io/radar"
|
|
275
|
-
target="_blank"
|
|
276
|
-
rel="noopener noreferrer"
|
|
277
|
-
className="text-accent-text hover:underline"
|
|
278
|
-
>
|
|
279
|
-
GitHub
|
|
280
|
-
</a>
|
|
281
|
-
</div>
|
|
282
282
|
</div>
|
|
283
283
|
</div>,
|
|
284
284
|
document.body
|
|
@@ -295,6 +295,19 @@ function SectionLabel({ children }: { children: ReactNode }) {
|
|
|
295
295
|
)
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
+
// A titled card grouping related config fields. Cards (vs thin dividers) give the
|
|
299
|
+
// sections clear visual separation in the scroll.
|
|
300
|
+
function ConfigSection({ title, children }: { title: string; children: ReactNode }) {
|
|
301
|
+
return (
|
|
302
|
+
<section className="rounded-lg border border-theme-border bg-theme-elevated/30 p-4">
|
|
303
|
+
<h4 className="text-xs font-medium text-theme-text-secondary uppercase tracking-wider mb-3">
|
|
304
|
+
{title}
|
|
305
|
+
</h4>
|
|
306
|
+
<div className="space-y-4">{children}</div>
|
|
307
|
+
</section>
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
|
|
298
311
|
// -- Startup Configuration Tab ------------------------------------------------
|
|
299
312
|
|
|
300
313
|
function StartupConfigTab({
|
|
@@ -302,133 +315,130 @@ function StartupConfigTab({
|
|
|
302
315
|
effectiveConfig,
|
|
303
316
|
isDesktop,
|
|
304
317
|
deploymentMode,
|
|
318
|
+
prometheusHeaderKeys,
|
|
305
319
|
onChange,
|
|
306
320
|
}: {
|
|
307
321
|
config: Config
|
|
308
322
|
effectiveConfig?: Config
|
|
309
323
|
isDesktop: boolean
|
|
310
324
|
deploymentMode: DeploymentMode
|
|
325
|
+
prometheusHeaderKeys: string[]
|
|
311
326
|
onChange: <K extends keyof Config>(field: K, value: Config[K]) => void
|
|
312
327
|
}) {
|
|
313
328
|
const showBrowserLaunchControls = !isDesktop && deploymentMode === 'local'
|
|
314
329
|
return (
|
|
315
|
-
<div className="space-y-
|
|
330
|
+
<div className="space-y-3">
|
|
316
331
|
<p className="text-xs text-theme-text-tertiary">
|
|
317
|
-
|
|
332
|
+
Most changes require a restart to take effect.
|
|
318
333
|
{isDesktop
|
|
319
334
|
? ' Quit and relaunch Radar to apply.'
|
|
320
335
|
: ' Stop and restart the radar command to apply.'}
|
|
321
336
|
</p>
|
|
322
337
|
|
|
323
|
-
<
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
<ConfigArrayField
|
|
333
|
-
label="Kubeconfig Directories"
|
|
334
|
-
help="Comma-separated directories containing kubeconfig files"
|
|
335
|
-
value={config.kubeconfigDirs}
|
|
336
|
-
effectiveValue={effectiveConfig?.kubeconfigDirs}
|
|
337
|
-
placeholder="/path/to/dir1, /path/to/dir2"
|
|
338
|
-
onChange={(v) => onChange('kubeconfigDirs', v)}
|
|
339
|
-
/>
|
|
340
|
-
|
|
341
|
-
<ConfigField
|
|
342
|
-
label="Default Namespace"
|
|
343
|
-
help="Initial namespace filter on startup"
|
|
344
|
-
value={config.namespace ?? ''}
|
|
345
|
-
effectiveValue={effectiveConfig?.namespace}
|
|
346
|
-
placeholder="All namespaces"
|
|
347
|
-
onChange={(v) => onChange('namespace', v || undefined)}
|
|
348
|
-
/>
|
|
349
|
-
|
|
350
|
-
<ConfigNumberField
|
|
351
|
-
label="Port"
|
|
352
|
-
help={isDesktop
|
|
353
|
-
? 'Fixed server port (leave empty for random). Set this to keep a stable MCP endpoint.'
|
|
354
|
-
: 'Server port'}
|
|
355
|
-
value={config.port}
|
|
356
|
-
effectiveValue={effectiveConfig?.port}
|
|
357
|
-
placeholder={isDesktop ? 'Random' : '9280'}
|
|
358
|
-
onChange={(v) => onChange('port', v)}
|
|
359
|
-
/>
|
|
338
|
+
<ConfigSection title="Cluster">
|
|
339
|
+
<ConfigField
|
|
340
|
+
label="Kubeconfig"
|
|
341
|
+
help="Path to kubeconfig file"
|
|
342
|
+
value={config.kubeconfig ?? ''}
|
|
343
|
+
effectiveValue={effectiveConfig?.kubeconfig}
|
|
344
|
+
placeholder="~/.kube/config"
|
|
345
|
+
onChange={(v) => onChange('kubeconfig', v || undefined)}
|
|
346
|
+
/>
|
|
360
347
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
<ConfigField
|
|
370
|
-
label="Browser"
|
|
371
|
-
help="Browser for automatic launch; macOS app names are supported"
|
|
372
|
-
value={config.browser ?? ''}
|
|
373
|
-
effectiveValue={effectiveConfig?.browser}
|
|
374
|
-
placeholder="System default"
|
|
375
|
-
onChange={(v) => onChange('browser', v || undefined)}
|
|
376
|
-
/>
|
|
377
|
-
</>
|
|
378
|
-
)}
|
|
348
|
+
<ConfigArrayField
|
|
349
|
+
label="Kubeconfig Directories"
|
|
350
|
+
help="Comma-separated directories containing kubeconfig files"
|
|
351
|
+
value={config.kubeconfigDirs}
|
|
352
|
+
effectiveValue={effectiveConfig?.kubeconfigDirs}
|
|
353
|
+
placeholder="/path/to/dir1, /path/to/dir2"
|
|
354
|
+
onChange={(v) => onChange('kubeconfigDirs', v)}
|
|
355
|
+
/>
|
|
379
356
|
|
|
380
|
-
|
|
381
|
-
|
|
357
|
+
<ConfigField
|
|
358
|
+
label="Default Namespace"
|
|
359
|
+
help="Startup default only — change the active namespace live anytime from the header switcher"
|
|
360
|
+
value={config.namespace ?? ''}
|
|
361
|
+
effectiveValue={effectiveConfig?.namespace}
|
|
362
|
+
placeholder="All namespaces"
|
|
363
|
+
onChange={(v) => onChange('namespace', v || undefined)}
|
|
364
|
+
/>
|
|
365
|
+
</ConfigSection>
|
|
366
|
+
|
|
367
|
+
<ConfigSection title="Server">
|
|
368
|
+
<ConfigNumberField
|
|
369
|
+
label="Port"
|
|
370
|
+
help={isDesktop
|
|
371
|
+
? 'Fixed server port (leave empty for random). Set this to keep a stable MCP endpoint.'
|
|
372
|
+
: 'Server port'}
|
|
373
|
+
value={config.port}
|
|
374
|
+
effectiveValue={effectiveConfig?.port}
|
|
375
|
+
placeholder={isDesktop ? 'Random' : '9280'}
|
|
376
|
+
onChange={(v) => onChange('port', v)}
|
|
377
|
+
/>
|
|
382
378
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
<
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
onChange={(e) => onChange('timelineStorage', e.target.value === 'memory' ? undefined : e.target.value as 'sqlite')}
|
|
391
|
-
className="w-full px-3 py-1.5 text-sm bg-theme-elevated border border-theme-border rounded-md text-theme-text-primary focus:outline-none focus:border-blue-500"
|
|
392
|
-
>
|
|
393
|
-
<option value="memory">Memory (default)</option>
|
|
394
|
-
<option value="sqlite">SQLite (persistent)</option>
|
|
395
|
-
</select>
|
|
396
|
-
<EffectiveHint current={config.timelineStorage} effective={effectiveConfig?.timelineStorage} />
|
|
397
|
-
</div>
|
|
379
|
+
{showBrowserLaunchControls && (
|
|
380
|
+
<>
|
|
381
|
+
<ConfigToggle
|
|
382
|
+
label="Open browser on start"
|
|
383
|
+
value={!(config.noBrowser ?? false)}
|
|
384
|
+
onChange={(v) => onChange('noBrowser', !v ? true : undefined)}
|
|
385
|
+
/>
|
|
398
386
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
387
|
+
<ConfigField
|
|
388
|
+
label="Browser"
|
|
389
|
+
help="Browser for automatic launch; macOS app names are supported"
|
|
390
|
+
value={config.browser ?? ''}
|
|
391
|
+
effectiveValue={effectiveConfig?.browser}
|
|
392
|
+
placeholder="System default"
|
|
393
|
+
onChange={(v) => onChange('browser', v || undefined)}
|
|
394
|
+
/>
|
|
395
|
+
</>
|
|
396
|
+
)}
|
|
397
|
+
</ConfigSection>
|
|
398
|
+
|
|
399
|
+
<ConfigSection title="AI Tools">
|
|
400
|
+
<MCPSection
|
|
401
|
+
mcpEnabled={config.mcp ?? true}
|
|
402
|
+
onToggle={(v) => onChange('mcp', v)}
|
|
403
|
+
isDesktop={isDesktop}
|
|
404
|
+
portPinned={config.port != null && config.port > 0}
|
|
405
|
+
onPinPort={(port) => onChange('port', port)}
|
|
406
|
+
/>
|
|
407
|
+
</ConfigSection>
|
|
408
|
+
|
|
409
|
+
<ConfigSection title="Timeline">
|
|
410
|
+
<div>
|
|
411
|
+
<label className="block text-sm font-medium text-theme-text-primary mb-1">
|
|
412
|
+
Storage Backend
|
|
413
|
+
</label>
|
|
414
|
+
<select
|
|
415
|
+
value={config.timelineStorage ?? 'memory'}
|
|
416
|
+
onChange={(e) => onChange('timelineStorage', e.target.value === 'memory' ? undefined : e.target.value as 'sqlite')}
|
|
417
|
+
className="w-full px-3 py-1.5 text-sm bg-theme-elevated border border-theme-border rounded-md text-theme-text-primary focus:outline-none focus:border-skyhook-500"
|
|
418
|
+
>
|
|
419
|
+
<option value="memory">Memory (default)</option>
|
|
420
|
+
<option value="sqlite">SQLite (persistent)</option>
|
|
421
|
+
</select>
|
|
422
|
+
<EffectiveHint current={config.timelineStorage} effective={effectiveConfig?.timelineStorage} />
|
|
407
423
|
</div>
|
|
408
|
-
</div>
|
|
409
424
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
portPinned={config.port != null && config.port > 0}
|
|
428
|
-
onPinPort={(port) => onChange('port', port)}
|
|
429
|
-
/>
|
|
430
|
-
</div>
|
|
431
|
-
</div>
|
|
425
|
+
<ConfigNumberField
|
|
426
|
+
label="History Limit"
|
|
427
|
+
help="Maximum events to retain"
|
|
428
|
+
value={config.historyLimit}
|
|
429
|
+
effectiveValue={effectiveConfig?.historyLimit}
|
|
430
|
+
placeholder="10000"
|
|
431
|
+
onChange={(v) => onChange('historyLimit', v)}
|
|
432
|
+
/>
|
|
433
|
+
</ConfigSection>
|
|
434
|
+
|
|
435
|
+
<ConfigSection title="Integrations">
|
|
436
|
+
<PrometheusConfigField
|
|
437
|
+
value={config.prometheusUrl ?? ''}
|
|
438
|
+
configuredHeaderKeys={prometheusHeaderKeys}
|
|
439
|
+
onChange={(v) => onChange('prometheusUrl', v || undefined)}
|
|
440
|
+
/>
|
|
441
|
+
</ConfigSection>
|
|
432
442
|
</div>
|
|
433
443
|
)
|
|
434
444
|
}
|
|
@@ -466,7 +476,7 @@ function MCPSection({
|
|
|
466
476
|
return (
|
|
467
477
|
<div className="space-y-3">
|
|
468
478
|
<ConfigToggle
|
|
469
|
-
label="MCP Server
|
|
479
|
+
label="MCP Server"
|
|
470
480
|
value={mcpEnabled}
|
|
471
481
|
onChange={onToggle}
|
|
472
482
|
/>
|
|
@@ -479,13 +489,14 @@ function MCPSection({
|
|
|
479
489
|
<code className="flex-1 px-2.5 py-1.5 text-xs font-mono bg-theme-elevated border border-theme-border rounded-md text-theme-text-primary truncate">
|
|
480
490
|
{mcpUrl}
|
|
481
491
|
</code>
|
|
492
|
+
<Tooltip content="Copy MCP URL" wrapperClassName="shrink-0">
|
|
482
493
|
<button
|
|
483
494
|
onClick={handleCopy}
|
|
484
|
-
className="
|
|
485
|
-
title="Copy MCP URL"
|
|
495
|
+
className="p-1.5 text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-elevated rounded-md transition-colors"
|
|
486
496
|
>
|
|
487
497
|
{copied ? <Check className="w-3.5 h-3.5 text-green-500" /> : <Copy className="w-3.5 h-3.5" />}
|
|
488
498
|
</button>
|
|
499
|
+
</Tooltip>
|
|
489
500
|
</div>
|
|
490
501
|
</div>
|
|
491
502
|
|
|
@@ -515,6 +526,225 @@ function MCPSection({
|
|
|
515
526
|
)
|
|
516
527
|
}
|
|
517
528
|
|
|
529
|
+
// -- Prometheus (live-appliable) ----------------------------------------------
|
|
530
|
+
|
|
531
|
+
// Unlike the rest of this dialog, the Prometheus URL can be re-pointed without
|
|
532
|
+
// a restart — the metrics path reads it from a mutable global. "Apply now" hits
|
|
533
|
+
// PUT /integrations/prometheus, which persists the URL AND re-points the running
|
|
534
|
+
// client, then probes it so we can confirm reachability inline. The global Save
|
|
535
|
+
// still persists this field too (taking effect next launch, like everything
|
|
536
|
+
// else); Apply is the shortcut to "now". No EffectiveHint here — the per-field
|
|
537
|
+
// restart-diff hint would contradict the whole point of applying live.
|
|
538
|
+
type ApplyState =
|
|
539
|
+
| { status: 'idle' }
|
|
540
|
+
| { status: 'applying' }
|
|
541
|
+
| { status: 'connected'; address: string }
|
|
542
|
+
| { status: 'unreachable'; error: string } // persisted, but the probe failed
|
|
543
|
+
| { status: 'failed'; error: string } // request itself failed — nothing saved
|
|
544
|
+
|
|
545
|
+
type HeaderRow = { key: string; value: string }
|
|
546
|
+
|
|
547
|
+
function PrometheusConfigField({
|
|
548
|
+
value,
|
|
549
|
+
onChange,
|
|
550
|
+
configuredHeaderKeys,
|
|
551
|
+
}: {
|
|
552
|
+
value: string
|
|
553
|
+
onChange: (value: string) => void
|
|
554
|
+
configuredHeaderKeys: string[]
|
|
555
|
+
}) {
|
|
556
|
+
const [apply, setApply] = useState<ApplyState>({ status: 'idle' })
|
|
557
|
+
// null = not editing headers (preserve what's stored). A non-null array means
|
|
558
|
+
// the user opened the editor; on Apply we send it verbatim, replacing all
|
|
559
|
+
// stored headers (values are write-only, so the server never sends them back).
|
|
560
|
+
const [headerRows, setHeaderRows] = useState<HeaderRow[] | null>(null)
|
|
561
|
+
// Show the server's configured header keys, but let a successful apply override
|
|
562
|
+
// optimistically (config isn't refetched). Derived from the prop — not a
|
|
563
|
+
// mount-time snapshot — so it stays correct as config loads asynchronously.
|
|
564
|
+
const [appliedKeys, setAppliedKeys] = useState<string[] | null>(null)
|
|
565
|
+
const storedKeys = appliedKeys ?? configuredHeaderKeys
|
|
566
|
+
|
|
567
|
+
const clearStatus = () => {
|
|
568
|
+
if (apply.status !== 'applying') setApply({ status: 'idle' })
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Footer Reset (and any external edit) clears the URL field without a keystroke;
|
|
572
|
+
// drop a stale "Connected"/"Saved" status so it doesn't describe an emptied field.
|
|
573
|
+
useEffect(() => {
|
|
574
|
+
setApply((s) => (s.status === 'idle' || s.status === 'applying' ? s : { status: 'idle' }))
|
|
575
|
+
}, [value])
|
|
576
|
+
|
|
577
|
+
const handleApply = async () => {
|
|
578
|
+
setApply({ status: 'applying' })
|
|
579
|
+
// Decide what to do with headers. undefined = leave them untouched. Only send
|
|
580
|
+
// a replacement when the editor has real content, or {} when the user emptied
|
|
581
|
+
// every row (explicit clear) — blank in-progress rows must NOT wipe stored
|
|
582
|
+
// secrets just because the editor happens to be open for a URL-only change.
|
|
583
|
+
let editedHeaders: Record<string, string> | undefined
|
|
584
|
+
if (headerRows !== null) {
|
|
585
|
+
const entered = Object.fromEntries(
|
|
586
|
+
headerRows
|
|
587
|
+
.map((r) => [r.key.trim(), r.value] as const)
|
|
588
|
+
.filter(([k, v]) => k !== '' && v !== '')
|
|
589
|
+
)
|
|
590
|
+
if (Object.keys(entered).length > 0) editedHeaders = entered
|
|
591
|
+
else if (headerRows.length === 0) editedHeaders = {}
|
|
592
|
+
}
|
|
593
|
+
try {
|
|
594
|
+
const res = await fetch(apiUrl('/integrations/prometheus'), {
|
|
595
|
+
method: 'PUT',
|
|
596
|
+
credentials: getCredentialsMode(),
|
|
597
|
+
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
598
|
+
body: JSON.stringify({
|
|
599
|
+
prometheusUrl: value.trim(),
|
|
600
|
+
...(editedHeaders !== undefined ? { headers: editedHeaders } : {}),
|
|
601
|
+
}),
|
|
602
|
+
})
|
|
603
|
+
const data = await res.json().catch(() => null)
|
|
604
|
+
if (!res.ok) {
|
|
605
|
+
setApply({ status: 'failed', error: data?.error || res.statusText })
|
|
606
|
+
return
|
|
607
|
+
}
|
|
608
|
+
if (editedHeaders !== undefined) {
|
|
609
|
+
setAppliedKeys(Object.keys(editedHeaders).sort())
|
|
610
|
+
}
|
|
611
|
+
if (headerRows !== null) {
|
|
612
|
+
setHeaderRows(null)
|
|
613
|
+
}
|
|
614
|
+
if (data?.connected) {
|
|
615
|
+
setApply({ status: 'connected', address: data.address || value.trim() })
|
|
616
|
+
} else {
|
|
617
|
+
setApply({ status: 'unreachable', error: data?.error || 'not reachable' })
|
|
618
|
+
}
|
|
619
|
+
} catch (err) {
|
|
620
|
+
setApply({ status: 'failed', error: String(err) })
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return (
|
|
625
|
+
<div>
|
|
626
|
+
<label className="block text-sm font-medium text-theme-text-primary mb-1">
|
|
627
|
+
Prometheus URL
|
|
628
|
+
</label>
|
|
629
|
+
<p className="text-xs text-theme-text-tertiary mb-1">
|
|
630
|
+
Manual Prometheus/VictoriaMetrics URL (skips auto-discovery)
|
|
631
|
+
</p>
|
|
632
|
+
<div className="flex items-center gap-2">
|
|
633
|
+
<input
|
|
634
|
+
type="text"
|
|
635
|
+
value={value}
|
|
636
|
+
onChange={(e) => onChange(e.target.value)}
|
|
637
|
+
placeholder="http://prometheus-server.monitoring:9090"
|
|
638
|
+
className="flex-1 min-w-0 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"
|
|
639
|
+
/>
|
|
640
|
+
<Tooltip content="Apply this URL to the running server now — no restart" wrapperClassName="shrink-0">
|
|
641
|
+
<button
|
|
642
|
+
onClick={handleApply}
|
|
643
|
+
disabled={apply.status === 'applying'}
|
|
644
|
+
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 border border-theme-border rounded-md transition-colors disabled:opacity-50"
|
|
645
|
+
>
|
|
646
|
+
{apply.status === 'applying'
|
|
647
|
+
? <Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
648
|
+
: <Plug className="w-3.5 h-3.5" />}
|
|
649
|
+
Apply now
|
|
650
|
+
</button>
|
|
651
|
+
</Tooltip>
|
|
652
|
+
</div>
|
|
653
|
+
{apply.status === 'connected' ? (
|
|
654
|
+
<p className="mt-1 flex items-center gap-1 text-xs text-green-600 dark:text-green-400/80">
|
|
655
|
+
<Check className="w-3 h-3 shrink-0" />
|
|
656
|
+
Connected to {apply.address} — applied, no restart needed
|
|
657
|
+
</p>
|
|
658
|
+
) : apply.status === 'unreachable' ? (
|
|
659
|
+
<p className="mt-1 text-xs text-amber-600 dark:text-amber-400/80">
|
|
660
|
+
Saved, but not reachable: {apply.error}
|
|
661
|
+
</p>
|
|
662
|
+
) : apply.status === 'failed' ? (
|
|
663
|
+
<p className="mt-1 text-xs text-red-600 dark:text-red-400/80">
|
|
664
|
+
Couldn't apply: {apply.error}
|
|
665
|
+
</p>
|
|
666
|
+
) : (
|
|
667
|
+
<p className="mt-1 text-xs text-theme-text-tertiary">
|
|
668
|
+
Applies immediately — no restart needed.
|
|
669
|
+
</p>
|
|
670
|
+
)}
|
|
671
|
+
|
|
672
|
+
{/* Auth headers — for token / multi-tenant backends (Bearer, X-Scope-OrgID). */}
|
|
673
|
+
<div className="mt-3">
|
|
674
|
+
{headerRows === null ? (
|
|
675
|
+
<div className="flex items-center justify-between gap-2">
|
|
676
|
+
<span className="text-xs text-theme-text-tertiary">
|
|
677
|
+
{storedKeys.length > 0
|
|
678
|
+
? <>Auth headers: <span className="text-theme-text-secondary">{storedKeys.join(', ')}</span> <span className="text-theme-text-disabled">(values hidden)</span></>
|
|
679
|
+
: 'No auth headers'}
|
|
680
|
+
</span>
|
|
681
|
+
<button
|
|
682
|
+
onClick={() => { setHeaderRows([{ key: '', value: '' }]); clearStatus() }}
|
|
683
|
+
className="shrink-0 text-xs font-medium text-accent-text hover:underline"
|
|
684
|
+
>
|
|
685
|
+
{storedKeys.length > 0 ? 'Edit headers' : 'Add auth headers'}
|
|
686
|
+
</button>
|
|
687
|
+
</div>
|
|
688
|
+
) : (
|
|
689
|
+
<div className="rounded-md border border-theme-border bg-theme-elevated/40 p-2.5 space-y-2">
|
|
690
|
+
{headerRows.map((row, i) => (
|
|
691
|
+
<div key={i} className="flex items-center gap-2">
|
|
692
|
+
<input
|
|
693
|
+
type="text"
|
|
694
|
+
value={row.key}
|
|
695
|
+
onChange={(e) => {
|
|
696
|
+
setHeaderRows((rows) => rows!.map((r, j) => j === i ? { ...r, key: e.target.value } : r))
|
|
697
|
+
clearStatus()
|
|
698
|
+
}}
|
|
699
|
+
placeholder="Header (e.g. Authorization)"
|
|
700
|
+
className="flex-1 min-w-0 px-2.5 py-1.5 text-xs bg-theme-base border border-theme-border rounded-md text-theme-text-primary placeholder:text-theme-text-tertiary focus:outline-none focus:border-skyhook-500"
|
|
701
|
+
/>
|
|
702
|
+
<input
|
|
703
|
+
type="password"
|
|
704
|
+
value={row.value}
|
|
705
|
+
onChange={(e) => {
|
|
706
|
+
setHeaderRows((rows) => rows!.map((r, j) => j === i ? { ...r, value: e.target.value } : r))
|
|
707
|
+
clearStatus()
|
|
708
|
+
}}
|
|
709
|
+
placeholder="Value (e.g. Bearer …)"
|
|
710
|
+
className="flex-1 min-w-0 px-2.5 py-1.5 text-xs bg-theme-base border border-theme-border rounded-md text-theme-text-primary placeholder:text-theme-text-tertiary focus:outline-none focus:border-skyhook-500"
|
|
711
|
+
/>
|
|
712
|
+
<Tooltip content="Remove header" wrapperClassName="shrink-0">
|
|
713
|
+
<button
|
|
714
|
+
onClick={() => setHeaderRows((rows) => rows!.filter((_, j) => j !== i))}
|
|
715
|
+
className="p-1 text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-hover rounded"
|
|
716
|
+
>
|
|
717
|
+
<X className="w-3.5 h-3.5" />
|
|
718
|
+
</button>
|
|
719
|
+
</Tooltip>
|
|
720
|
+
</div>
|
|
721
|
+
))}
|
|
722
|
+
<div className="flex items-center justify-between gap-2">
|
|
723
|
+
<button
|
|
724
|
+
onClick={() => setHeaderRows((rows) => [...rows!, { key: '', value: '' }])}
|
|
725
|
+
className="flex items-center gap-1 text-xs font-medium text-accent-text hover:underline"
|
|
726
|
+
>
|
|
727
|
+
<Plus className="w-3 h-3" /> Add header
|
|
728
|
+
</button>
|
|
729
|
+
<button
|
|
730
|
+
onClick={() => { setHeaderRows(null); clearStatus() }}
|
|
731
|
+
className="text-xs text-theme-text-tertiary hover:text-theme-text-primary"
|
|
732
|
+
>
|
|
733
|
+
Cancel
|
|
734
|
+
</button>
|
|
735
|
+
</div>
|
|
736
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
737
|
+
Saved when you click Apply now. Entered headers replace all stored
|
|
738
|
+
ones — values are hidden, so re-enter any you want to keep. Leave
|
|
739
|
+
blank to keep existing headers unchanged.
|
|
740
|
+
</p>
|
|
741
|
+
</div>
|
|
742
|
+
)}
|
|
743
|
+
</div>
|
|
744
|
+
</div>
|
|
745
|
+
)
|
|
746
|
+
}
|
|
747
|
+
|
|
518
748
|
// -- Shared Field Components --------------------------------------------------
|
|
519
749
|
|
|
520
750
|
function ConfigField({
|
|
@@ -543,7 +773,7 @@ function ConfigField({
|
|
|
543
773
|
value={value}
|
|
544
774
|
onChange={(e) => onChange(e.target.value)}
|
|
545
775
|
placeholder={placeholder}
|
|
546
|
-
className="w-full 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-
|
|
776
|
+
className="w-full 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"
|
|
547
777
|
/>
|
|
548
778
|
<EffectiveHint current={value || undefined} effective={effectiveValue} />
|
|
549
779
|
</div>
|
|
@@ -603,7 +833,7 @@ function ConfigArrayField({
|
|
|
603
833
|
commit(e.target.value)
|
|
604
834
|
}}
|
|
605
835
|
placeholder={placeholder}
|
|
606
|
-
className="w-full 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-
|
|
836
|
+
className="w-full 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"
|
|
607
837
|
/>
|
|
608
838
|
<EffectiveHint current={canonical(value) || undefined} effective={canonical(effectiveValue) || undefined} />
|
|
609
839
|
</div>
|
|
@@ -636,7 +866,7 @@ function ConfigNumberField({
|
|
|
636
866
|
value={value ?? ''}
|
|
637
867
|
onChange={(e) => onChange(e.target.value ? parseInt(e.target.value, 10) || undefined : undefined)}
|
|
638
868
|
placeholder={placeholder}
|
|
639
|
-
className="w-full 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-
|
|
869
|
+
className="w-full 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"
|
|
640
870
|
/>
|
|
641
871
|
<EffectiveHint current={value} effective={effectiveValue} />
|
|
642
872
|
</div>
|