From 34c9822e42f6e49e3797a71c82b9f352358b1f93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Br=C3=BCckner?= Date: Wed, 3 Jun 2026 16:18:36 +0200 Subject: [PATCH] =?UTF-8?q?style(settings):=20polish=20Settings=20page=20?= =?UTF-8?q?=E2=80=93=20accent=20gradients,=20status=20badges,=20better=20f?= =?UTF-8?q?ield=20layout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Settings.tsx | 289 ++++++++++++++++++++++++------------ 1 file changed, 192 insertions(+), 97 deletions(-) diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index 41b8c59..074ea6d 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -2,7 +2,8 @@ import React, { useState, useEffect } from 'react'; import { authFetch } from '../lib/auth'; import { User } from '../types'; 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'; const SECRET_SENTINEL = '__SET__'; @@ -22,36 +23,67 @@ interface SettingsProps { currentUser: User; } -function FieldLabel({ children }: { children: React.ReactNode }) { +function Label({ children }: { children: React.ReactNode }) { + return ; +} + +function Hint({ children }: { children: React.ReactNode }) { + return

{children}

; +} + +function ConfiguredBadge() { return ( - + + + CONFIGURED + ); } -function TextInput({ - value, onChange, placeholder, disabled, monospace, -}: { +function FieldRow({ label, hint, badge, children }: { + label: string; + hint?: string; + badge?: React.ReactNode; + children: React.ReactNode; +}) { + return ( +
+
+ + {badge} +
+ {children} + {hint && {hint}} +
+ ); +} + +function Input({ value, onChange, placeholder, monospace, icon }: { value: string; onChange: (v: string) => void; placeholder?: string; - disabled?: boolean; monospace?: boolean; + icon?: React.ReactNode; }) { return ( - onChange(e.target.value)} - placeholder={placeholder} - 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' : ''}`} - /> +
+ {icon && ( + + {icon} + + )} + 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' : ''}`} + /> +
); } -function SecretInput({ - value, onChange, alreadySet, show, onToggleShow, -}: { +function SecretInput({ value, onChange, alreadySet, show, onToggleShow }: { value: string; onChange: (v: string) => void; alreadySet: boolean; @@ -60,32 +92,48 @@ function SecretInput({ }) { return (
+ + + onChange(e.target.value)} - placeholder={alreadySet ? 'Already configured – leave blank to keep' : 'Enter 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" + placeholder={alreadySet ? '•••••••• (leave blank to keep)' : 'Paste secret value'} + 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" />
); } +function SectionCard({ accentColor, children }: { + accentColor: string; + children: React.ReactNode; +}) { + return ( +
+
+
+ {children} +
+
+ ); +} + export default function Settings({ currentUser: _currentUser }: SettingsProps) { const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(''); const [successMsg, setSuccessMsg] = useState(''); - // Entra ID const [azureEnabled, setAzureEnabled] = useState(false); const [azureClientId, setAzureClientId] = useState(''); const [azureTenantId, setAzureTenantId] = useState(''); @@ -94,7 +142,6 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) { const [azureRedirectUri, setAzureRedirectUri] = useState(''); const [showAzureSecret, setShowAzureSecret] = useState(false); - // CheckMK const [checkmkApiUrl, setCheckmkApiUrl] = useState(''); const [checkmkApiSecret, setCheckmkApiSecret] = useState(''); const [checkmkSecretSet, setCheckmkSecretSet] = useState(false); @@ -108,7 +155,12 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) { setError(''); try { 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(); setAzureEnabled(data.azure_enabled === 'true'); setAzureClientId(data.azure_client_id || ''); @@ -153,7 +205,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) { setCheckmkSecretSet(data.checkmk_api_secret === SECRET_SENTINEL); setAzureClientSecret(''); setCheckmkApiSecret(''); - setSuccessMsg('Settings saved.'); + setSuccessMsg('Settings saved successfully.'); setTimeout(() => setSuccessMsg(''), 4000); } catch { setError('Network error saving settings.'); @@ -165,72 +217,90 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) { if (loading) { return (
- +
); } return ( -
+
{/* Page header */} -
-
- -
-
-

Settings

-

Integrations and service configuration

+
+
+ + SYSTEM + + SETTINGS
+

Settings

+

Configure integrations and authentication providers.

- {/* Feedback */} + {/* Feedback banners */} {error && ( -
- - {error} +
+ + {error}
)} {successMsg && ( -
+
{successMsg}
)} - {/* Microsoft Entra ID */} -
+ {/* ── Microsoft Entra ID ── */} + + {/* Card header */}
-
+
-

Microsoft Entra ID

-

OAuth 2.0 login for organizational accounts

+
+

Microsoft Entra ID

+ {azureEnabled && azureSecretSet && ( + + + ACTIVE + + )} +
+

OAuth 2.0 SSO for organizational accounts

- + {/* Toggle */} +
+ + {azureEnabled ? 'ENABLED' : 'DISABLED'} + + +
-
-
- Tenant ID - -
-
- Client ID (Application ID) - -
-
- Client Secret +
+ + {/* Fields */} +
+ + + + + + + : undefined} + > setShowAzureSecret(v => !v)} /> -
-
- Redirect URI (leave blank for auto) - -
+ + + } + /> +
-
+ - {/* CheckMK */} -
+ {/* ── CheckMK ── */} +
-
+
-

CheckMK

-

Device status synchronization via the CheckMK REST API

+
+

CheckMK

+ {checkmkApiUrl && checkmkSecretSet && ( + + + ACTIVE + + )} +
+

Device status sync via CheckMK REST API

-
-
- API URL - + +
+ + } /> -
-
- Automation Secret - setShowCheckmkSecret(v => !v)} - /> -
-
- Sync Interval (ms) - + +
+ : undefined} + > + setShowCheckmkSecret(v => !v)} + /> + + + } + /> +
-
+ - {/* Save */} -
+ {/* Save bar */} +
+

Changes are applied after saving and a server restart.