style(settings): polish Settings page – accent gradients, status badges, better field layout
This commit is contained in:
@ -2,7 +2,8 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { authFetch } from '../lib/auth';
|
import { authFetch } from '../lib/auth';
|
||||||
import { User } from '../types';
|
import { User } from '../types';
|
||||||
import {
|
import {
|
||||||
Shield, Activity, Save, CheckCircle, AlertCircle, Eye, EyeOff, Settings2,
|
Shield, Activity, Save, CheckCircle, AlertCircle, Eye, EyeOff,
|
||||||
|
Settings2, ExternalLink, KeyRound, Globe, Clock, ChevronRight,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const SECRET_SENTINEL = '__SET__';
|
const SECRET_SENTINEL = '__SET__';
|
||||||
@ -22,36 +23,67 @@ interface SettingsProps {
|
|||||||
currentUser: User;
|
currentUser: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
function FieldLabel({ children }: { children: React.ReactNode }) {
|
function Label({ children }: { children: React.ReactNode }) {
|
||||||
|
return <label className="block text-[11px] font-semibold text-slate-400 uppercase tracking-wide mb-1.5">{children}</label>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Hint({ children }: { children: React.ReactNode }) {
|
||||||
|
return <p className="mt-1 text-[10px] text-slate-600 font-mono">{children}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConfiguredBadge() {
|
||||||
return (
|
return (
|
||||||
<label className="block text-xs font-semibold text-slate-300 mb-1.5">{children}</label>
|
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-semibold bg-emerald-950/60 border border-emerald-900/50 text-emerald-400">
|
||||||
|
<span className="w-1 h-1 rounded-full bg-emerald-400 inline-block" />
|
||||||
|
CONFIGURED
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TextInput({
|
function FieldRow({ label, hint, badge, children }: {
|
||||||
value, onChange, placeholder, disabled, monospace,
|
label: string;
|
||||||
}: {
|
hint?: string;
|
||||||
|
badge?: React.ReactNode;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label>{label}</Label>
|
||||||
|
{badge}
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
{hint && <Hint>{hint}</Hint>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Input({ value, onChange, placeholder, monospace, icon }: {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (v: string) => void;
|
onChange: (v: string) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
disabled?: boolean;
|
|
||||||
monospace?: boolean;
|
monospace?: boolean;
|
||||||
|
icon?: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<input
|
<div className="relative">
|
||||||
type="text"
|
{icon && (
|
||||||
value={value}
|
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-600 pointer-events-none">
|
||||||
onChange={e => onChange(e.target.value)}
|
{icon}
|
||||||
placeholder={placeholder}
|
</span>
|
||||||
disabled={disabled}
|
)}
|
||||||
className={`w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2.5 text-sm text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50 transition-all disabled:opacity-50 disabled:cursor-not-allowed ${monospace ? 'font-mono' : ''}`}
|
<input
|
||||||
/>
|
type="text"
|
||||||
|
value={value}
|
||||||
|
onChange={e => onChange(e.target.value)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className={`w-full bg-slate-950 border border-slate-800 rounded-lg py-2.5 text-sm text-white placeholder-slate-600 focus:outline-none focus:ring-1 focus:ring-cyan-500/40 focus:border-cyan-500/40 transition-all ${icon ? 'pl-9 pr-3' : 'px-3'} ${monospace ? 'font-mono text-xs' : ''}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SecretInput({
|
function SecretInput({ value, onChange, alreadySet, show, onToggleShow }: {
|
||||||
value, onChange, alreadySet, show, onToggleShow,
|
|
||||||
}: {
|
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (v: string) => void;
|
onChange: (v: string) => void;
|
||||||
alreadySet: boolean;
|
alreadySet: boolean;
|
||||||
@ -60,32 +92,48 @@ function SecretInput({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-600 pointer-events-none">
|
||||||
|
<KeyRound className="w-3.5 h-3.5" />
|
||||||
|
</span>
|
||||||
<input
|
<input
|
||||||
type={show ? 'text' : 'password'}
|
type={show ? 'text' : 'password'}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={e => onChange(e.target.value)}
|
onChange={e => onChange(e.target.value)}
|
||||||
placeholder={alreadySet ? 'Already configured – leave blank to keep' : 'Enter value'}
|
placeholder={alreadySet ? '•••••••• (leave blank to keep)' : 'Paste secret value'}
|
||||||
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2.5 pr-10 text-sm text-white placeholder-slate-500 font-mono focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50 transition-all"
|
className="w-full bg-slate-950 border border-slate-800 rounded-lg pl-9 pr-10 py-2.5 text-xs font-mono text-white placeholder-slate-600 focus:outline-none focus:ring-1 focus:ring-cyan-500/40 focus:border-cyan-500/40 transition-all"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onToggleShow}
|
onClick={onToggleShow}
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-200 transition-colors"
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-600 hover:text-slate-300 transition-colors"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
{show ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
{show ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SectionCard({ accentColor, children }: {
|
||||||
|
accentColor: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={`bg-[#0F172A] border border-[#1E293B] rounded-2xl overflow-hidden`}>
|
||||||
|
<div className={`h-0.5 w-full ${accentColor}`} />
|
||||||
|
<div className="p-6 space-y-5">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function Settings({ currentUser: _currentUser }: SettingsProps) {
|
export default function Settings({ currentUser: _currentUser }: SettingsProps) {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [successMsg, setSuccessMsg] = useState('');
|
const [successMsg, setSuccessMsg] = useState('');
|
||||||
|
|
||||||
// Entra ID
|
|
||||||
const [azureEnabled, setAzureEnabled] = useState(false);
|
const [azureEnabled, setAzureEnabled] = useState(false);
|
||||||
const [azureClientId, setAzureClientId] = useState('');
|
const [azureClientId, setAzureClientId] = useState('');
|
||||||
const [azureTenantId, setAzureTenantId] = useState('');
|
const [azureTenantId, setAzureTenantId] = useState('');
|
||||||
@ -94,7 +142,6 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
|
|||||||
const [azureRedirectUri, setAzureRedirectUri] = useState('');
|
const [azureRedirectUri, setAzureRedirectUri] = useState('');
|
||||||
const [showAzureSecret, setShowAzureSecret] = useState(false);
|
const [showAzureSecret, setShowAzureSecret] = useState(false);
|
||||||
|
|
||||||
// CheckMK
|
|
||||||
const [checkmkApiUrl, setCheckmkApiUrl] = useState('');
|
const [checkmkApiUrl, setCheckmkApiUrl] = useState('');
|
||||||
const [checkmkApiSecret, setCheckmkApiSecret] = useState('');
|
const [checkmkApiSecret, setCheckmkApiSecret] = useState('');
|
||||||
const [checkmkSecretSet, setCheckmkSecretSet] = useState(false);
|
const [checkmkSecretSet, setCheckmkSecretSet] = useState(false);
|
||||||
@ -108,7 +155,12 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
|
|||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
const res = await authFetch('/api/settings');
|
const res = await authFetch('/api/settings');
|
||||||
if (!res.ok) { setError('Failed to load settings.'); return; }
|
if (!res.ok) {
|
||||||
|
let msg = `Failed to load settings (HTTP ${res.status}).`;
|
||||||
|
try { const d = await res.json(); if (d.error) msg = `${d.error} (HTTP ${res.status})`; } catch {}
|
||||||
|
setError(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const data: RawSettings = await res.json();
|
const data: RawSettings = await res.json();
|
||||||
setAzureEnabled(data.azure_enabled === 'true');
|
setAzureEnabled(data.azure_enabled === 'true');
|
||||||
setAzureClientId(data.azure_client_id || '');
|
setAzureClientId(data.azure_client_id || '');
|
||||||
@ -153,7 +205,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
|
|||||||
setCheckmkSecretSet(data.checkmk_api_secret === SECRET_SENTINEL);
|
setCheckmkSecretSet(data.checkmk_api_secret === SECRET_SENTINEL);
|
||||||
setAzureClientSecret('');
|
setAzureClientSecret('');
|
||||||
setCheckmkApiSecret('');
|
setCheckmkApiSecret('');
|
||||||
setSuccessMsg('Settings saved.');
|
setSuccessMsg('Settings saved successfully.');
|
||||||
setTimeout(() => setSuccessMsg(''), 4000);
|
setTimeout(() => setSuccessMsg(''), 4000);
|
||||||
} catch {
|
} catch {
|
||||||
setError('Network error saving settings.');
|
setError('Network error saving settings.');
|
||||||
@ -165,72 +217,90 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<span className="w-5 h-5 border-2 border-slate-600 border-t-cyan-400 rounded-full animate-spin" />
|
<span className="w-5 h-5 border-2 border-slate-700 border-t-cyan-400 rounded-full animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 max-w-3xl">
|
<div className="space-y-6 max-w-2xl">
|
||||||
|
|
||||||
{/* Page header */}
|
{/* Page header */}
|
||||||
<div className="flex items-center gap-3">
|
<div>
|
||||||
<div className="p-2 bg-slate-900 border border-slate-800 rounded-xl">
|
<div className="flex items-center gap-1.5 text-[10px] font-mono text-slate-600 mb-3">
|
||||||
<Settings2 className="w-5 h-5 text-cyan-400" />
|
<Settings2 className="w-3 h-3" />
|
||||||
</div>
|
<span>SYSTEM</span>
|
||||||
<div>
|
<ChevronRight className="w-3 h-3" />
|
||||||
<h1 className="text-lg font-bold text-white">Settings</h1>
|
<span className="text-slate-400">SETTINGS</span>
|
||||||
<p className="text-xs text-slate-400">Integrations and service configuration</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<h1 className="text-xl font-bold text-white tracking-tight">Settings</h1>
|
||||||
|
<p className="text-xs text-slate-500 mt-0.5">Configure integrations and authentication providers.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Feedback */}
|
{/* Feedback banners */}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="flex items-center gap-2 bg-red-950/60 border border-red-800/60 rounded-lg px-3 py-2.5 text-xs text-red-300">
|
<div className="flex items-start gap-2.5 bg-red-950/40 border border-red-900/50 rounded-xl px-4 py-3 text-xs text-red-300">
|
||||||
<AlertCircle className="w-4 h-4 shrink-0 text-red-400" />
|
<AlertCircle className="w-4 h-4 shrink-0 text-red-400 mt-0.5" />
|
||||||
{error}
|
<span>{error}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{successMsg && (
|
{successMsg && (
|
||||||
<div className="flex items-center gap-2 bg-emerald-950/60 border border-emerald-800/60 rounded-lg px-3 py-2.5 text-xs text-emerald-300">
|
<div className="flex items-center gap-2.5 bg-emerald-950/40 border border-emerald-900/50 rounded-xl px-4 py-3 text-xs text-emerald-300">
|
||||||
<CheckCircle className="w-4 h-4 shrink-0 text-emerald-400" />
|
<CheckCircle className="w-4 h-4 shrink-0 text-emerald-400" />
|
||||||
{successMsg}
|
{successMsg}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Microsoft Entra ID */}
|
{/* ── Microsoft Entra ID ── */}
|
||||||
<div className="bg-[#0F172A] border border-[#1E293B] rounded-2xl p-6 space-y-5">
|
<SectionCard accentColor="bg-gradient-to-r from-blue-600 to-indigo-600">
|
||||||
|
{/* Card header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="p-1.5 bg-blue-950/50 border border-blue-900/50 rounded-lg">
|
<div className="p-2 bg-blue-950/60 border border-blue-900/40 rounded-xl">
|
||||||
<Shield className="w-4 h-4 text-blue-400" />
|
<Shield className="w-4 h-4 text-blue-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold text-white">Microsoft Entra ID</h3>
|
<div className="flex items-center gap-2">
|
||||||
<p className="text-xs text-slate-500">OAuth 2.0 login for organizational accounts</p>
|
<h2 className="text-sm font-semibold text-white">Microsoft Entra ID</h2>
|
||||||
|
{azureEnabled && azureSecretSet && (
|
||||||
|
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-semibold bg-blue-950/60 border border-blue-900/50 text-blue-400">
|
||||||
|
<span className="w-1 h-1 rounded-full bg-blue-400 animate-pulse inline-block" />
|
||||||
|
ACTIVE
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] text-slate-500 mt-0.5">OAuth 2.0 SSO for organizational accounts</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
{/* Toggle */}
|
||||||
type="button"
|
<div className="flex items-center gap-2.5">
|
||||||
onClick={() => setAzureEnabled(v => !v)}
|
<span className={`text-[10px] font-semibold font-mono ${azureEnabled ? 'text-blue-400' : 'text-slate-600'}`}>
|
||||||
className={`relative w-11 h-6 rounded-full transition-colors ${azureEnabled ? 'bg-blue-600' : 'bg-slate-700'}`}
|
{azureEnabled ? 'ENABLED' : 'DISABLED'}
|
||||||
title={azureEnabled ? 'Disable' : 'Enable'}
|
</span>
|
||||||
>
|
<button
|
||||||
<span className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${azureEnabled ? 'translate-x-5' : 'translate-x-0'}`} />
|
type="button"
|
||||||
</button>
|
onClick={() => setAzureEnabled(v => !v)}
|
||||||
|
className={`relative w-10 h-5 rounded-full transition-all duration-200 focus:outline-none ${azureEnabled ? 'bg-blue-600 shadow-[0_0_10px_rgba(37,99,235,0.4)]' : 'bg-slate-800 border border-slate-700'}`}
|
||||||
|
>
|
||||||
|
<span className={`absolute top-0.5 w-4 h-4 bg-white rounded-full shadow transition-all duration-200 ${azureEnabled ? 'left-5' : 'left-0.5'}`} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`grid grid-cols-1 sm:grid-cols-2 gap-4 ${!azureEnabled ? 'opacity-50 pointer-events-none' : ''}`}>
|
<div className="h-px bg-slate-800/60" />
|
||||||
<div>
|
|
||||||
<FieldLabel>Tenant ID</FieldLabel>
|
{/* Fields */}
|
||||||
<TextInput value={azureTenantId} onChange={setAzureTenantId} placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" monospace />
|
<div className={`grid grid-cols-1 sm:grid-cols-2 gap-5 transition-opacity duration-200 ${!azureEnabled ? 'opacity-40 pointer-events-none' : ''}`}>
|
||||||
</div>
|
<FieldRow label="Tenant ID" hint="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx">
|
||||||
<div>
|
<Input value={azureTenantId} onChange={setAzureTenantId} placeholder="Paste Tenant ID" monospace />
|
||||||
<FieldLabel>Client ID (Application ID)</FieldLabel>
|
</FieldRow>
|
||||||
<TextInput value={azureClientId} onChange={setAzureClientId} placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" monospace />
|
<FieldRow label="Client ID (Application ID)" hint="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx">
|
||||||
</div>
|
<Input value={azureClientId} onChange={setAzureClientId} placeholder="Paste Client ID" monospace />
|
||||||
<div>
|
</FieldRow>
|
||||||
<FieldLabel>Client Secret</FieldLabel>
|
<FieldRow
|
||||||
|
label="Client Secret"
|
||||||
|
badge={azureSecretSet ? <ConfiguredBadge /> : undefined}
|
||||||
|
>
|
||||||
<SecretInput
|
<SecretInput
|
||||||
value={azureClientSecret}
|
value={azureClientSecret}
|
||||||
onChange={setAzureClientSecret}
|
onChange={setAzureClientSecret}
|
||||||
@ -238,59 +308,84 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
|
|||||||
show={showAzureSecret}
|
show={showAzureSecret}
|
||||||
onToggleShow={() => setShowAzureSecret(v => !v)}
|
onToggleShow={() => setShowAzureSecret(v => !v)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</FieldRow>
|
||||||
<div>
|
<FieldRow label="Redirect URI" hint="Leave blank to auto-derive from APP_URL">
|
||||||
<FieldLabel>Redirect URI <span className="text-slate-500 font-normal">(leave blank for auto)</span></FieldLabel>
|
<Input
|
||||||
<TextInput value={azureRedirectUri} onChange={setAzureRedirectUri} placeholder="https://ghostgrid.internal/api/auth/azure/callback" monospace />
|
value={azureRedirectUri}
|
||||||
</div>
|
onChange={setAzureRedirectUri}
|
||||||
|
placeholder="https://…/api/auth/azure/callback"
|
||||||
|
monospace
|
||||||
|
icon={<ExternalLink className="w-3.5 h-3.5" />}
|
||||||
|
/>
|
||||||
|
</FieldRow>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SectionCard>
|
||||||
|
|
||||||
{/* CheckMK */}
|
{/* ── CheckMK ── */}
|
||||||
<div className="bg-[#0F172A] border border-[#1E293B] rounded-2xl p-6 space-y-5">
|
<SectionCard accentColor="bg-gradient-to-r from-emerald-600 to-teal-600">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="p-1.5 bg-emerald-950/50 border border-emerald-900/50 rounded-lg">
|
<div className="p-2 bg-emerald-950/60 border border-emerald-900/40 rounded-xl">
|
||||||
<Activity className="w-4 h-4 text-emerald-400" />
|
<Activity className="w-4 h-4 text-emerald-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold text-white">CheckMK</h3>
|
<div className="flex items-center gap-2">
|
||||||
<p className="text-xs text-slate-500">Device status synchronization via the CheckMK REST API</p>
|
<h2 className="text-sm font-semibold text-white">CheckMK</h2>
|
||||||
|
{checkmkApiUrl && checkmkSecretSet && (
|
||||||
|
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-semibold bg-emerald-950/60 border border-emerald-900/50 text-emerald-400">
|
||||||
|
<span className="w-1 h-1 rounded-full bg-emerald-400 animate-pulse inline-block" />
|
||||||
|
ACTIVE
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] text-slate-500 mt-0.5">Device status sync via CheckMK REST API</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="h-px bg-slate-800/60" />
|
||||||
<div className="sm:col-span-2">
|
|
||||||
<FieldLabel>API URL</FieldLabel>
|
<div className="space-y-5">
|
||||||
<TextInput
|
<FieldRow label="API URL">
|
||||||
|
<Input
|
||||||
value={checkmkApiUrl}
|
value={checkmkApiUrl}
|
||||||
onChange={setCheckmkApiUrl}
|
onChange={setCheckmkApiUrl}
|
||||||
placeholder="https://checkmk.internal/<site>/check_mk/api/1.0"
|
placeholder="https://checkmk.internal/<site>/check_mk/api/1.0"
|
||||||
monospace
|
monospace
|
||||||
|
icon={<Globe className="w-3.5 h-3.5" />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</FieldRow>
|
||||||
<div>
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
||||||
<FieldLabel>Automation Secret</FieldLabel>
|
<FieldRow
|
||||||
<SecretInput
|
label="Automation Secret"
|
||||||
value={checkmkApiSecret}
|
badge={checkmkSecretSet ? <ConfiguredBadge /> : undefined}
|
||||||
onChange={setCheckmkApiSecret}
|
>
|
||||||
alreadySet={checkmkSecretSet}
|
<SecretInput
|
||||||
show={showCheckmkSecret}
|
value={checkmkApiSecret}
|
||||||
onToggleShow={() => setShowCheckmkSecret(v => !v)}
|
onChange={setCheckmkApiSecret}
|
||||||
/>
|
alreadySet={checkmkSecretSet}
|
||||||
</div>
|
show={showCheckmkSecret}
|
||||||
<div>
|
onToggleShow={() => setShowCheckmkSecret(v => !v)}
|
||||||
<FieldLabel>Sync Interval (ms)</FieldLabel>
|
/>
|
||||||
<TextInput value={checkmkSyncInterval} onChange={setCheckmkSyncInterval} placeholder="60000" monospace />
|
</FieldRow>
|
||||||
|
<FieldRow label="Sync Interval" hint="Milliseconds between status polls (default: 60000)">
|
||||||
|
<Input
|
||||||
|
value={checkmkSyncInterval}
|
||||||
|
onChange={setCheckmkSyncInterval}
|
||||||
|
placeholder="60000"
|
||||||
|
monospace
|
||||||
|
icon={<Clock className="w-3.5 h-3.5" />}
|
||||||
|
/>
|
||||||
|
</FieldRow>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SectionCard>
|
||||||
|
|
||||||
{/* Save */}
|
{/* Save bar */}
|
||||||
<div className="flex justify-end">
|
<div className="flex items-center justify-between pt-2 border-t border-slate-800/60">
|
||||||
|
<p className="text-[11px] text-slate-600">Changes are applied after saving and a server restart.</p>
|
||||||
<button
|
<button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
className="flex items-center gap-2 bg-cyan-600 hover:bg-cyan-500 disabled:bg-slate-700 disabled:text-slate-500 text-white font-semibold text-sm px-5 py-2.5 rounded-lg transition-all shadow-lg shadow-cyan-900/30"
|
className="flex items-center gap-2 bg-cyan-600 hover:bg-cyan-500 active:bg-cyan-700 disabled:bg-slate-800 disabled:text-slate-600 disabled:border disabled:border-slate-700 text-white font-semibold text-sm px-5 py-2.5 rounded-xl transition-all shadow-lg shadow-cyan-950/50 focus:outline-none focus:ring-2 focus:ring-cyan-500/40"
|
||||||
>
|
>
|
||||||
{saving ? (
|
{saving ? (
|
||||||
<span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
<span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||||
|
|||||||
Reference in New Issue
Block a user