diff --git a/web/app/settings/page.tsx b/web/app/settings/page.tsx index 41ae69a..5eb07e6 100644 --- a/web/app/settings/page.tsx +++ b/web/app/settings/page.tsx @@ -2,58 +2,40 @@ import { useEffect, useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { api, AlertSettings, EmailSettings } from "@/lib/api"; -import { Button, Card, CardHeader, CardTitle } from "@/components/ui"; +import Link from "next/link"; +import { api } from "@/lib/api"; +import { Button, Card } from "@/components/ui"; -function Toggle({ enabled, onChange }: { enabled: boolean; onChange: (v: boolean) => void }) { - return ( - - ); -} - -function ToggleRow({ - label, +function SectionCard({ + title, description, - enabled, - onChange, + icon, + children, + className, }: { - label: string; - description: string; - enabled: boolean; - onChange: (v: boolean) => void; + title: string; + description?: string; + icon: React.ReactNode; + children: React.ReactNode; + className?: string; }) { return ( -
-
-

{label}

-

{description}

+ +
+
+ {icon} +
+
+

{title}

+ {description &&

{description}

} +
- -
+ {children} + ); } -function Field({ - label, - hint, - children, -}: { - label: string; - hint?: string; - children: React.ReactNode; -}) { +function Field({ label, hint, children }: { label: string; hint?: string; children: React.ReactNode }) { return (
@@ -63,23 +45,44 @@ function Field({ ); } -const inputClass = - "w-full rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary placeholder:text-text-tertiary focus:border-accent/50 focus:outline-none focus:ring-1 focus:ring-accent/30"; +function BellIcon() { + return ( + + + + ); +} -function SecretsTokenCard({ - tokenSet, - rotatedAt, -}: { - tokenSet: boolean; - rotatedAt?: string; -}) { +function ServerIcon() { + return ( + + + + ); +} + +function DocumentIcon() { + return ( + + + + ); +} + +function KeyIcon() { + return ( + + + + ); +} + +function SecretsTokenCard({ tokenSet, rotatedAt }: { tokenSet: boolean; rotatedAt?: string }) { const queryClient = useQueryClient(); const [token, setToken] = useState(null); const [copied, setCopied] = useState(false); const readUrl = - typeof window !== "undefined" - ? `${window.location.origin}/api/secrets//values` - : "/api/secrets//values"; + typeof window !== "undefined" ? `${window.location.origin}/api/secrets//values` : "/api/secrets//values"; const { mutate: rotate, isPending } = useMutation({ mutationFn: api.rotateSecretsToken, @@ -97,20 +100,18 @@ function SecretsTokenCard({ } return ( - - - Secrets Read Token (ESO) - -

- Kubernetes External Secrets Operator authenticates to the read endpoint with this bearer - token. Point your ClusterSecretStore at{" "} + } + > +

+ Point your ClusterSecretStore at{" "} {readUrl}.

- + {tokenSet ? "A read token is configured" : "No read token configured yet"} {tokenSet && rotatedAt && ` ยท rotated ${new Date(rotatedAt).toLocaleString()}`} @@ -119,9 +120,7 @@ function SecretsTokenCard({ {token && (
-

- Copy this token now โ€” it will not be shown again. -

+

Copy this token now โ€” it will not be shown again.

{token} @@ -141,60 +140,27 @@ function SecretsTokenCard({ Rotating invalidates the previous token. Update the Kubernetes secret afterwards.

)} - + ); } export default function SettingsPage() { const queryClient = useQueryClient(); - const { data: settings, isLoading } = useQuery({ - queryKey: ["settings"], - queryFn: api.getSettings, - }); + const { data: settings, isLoading } = useQuery({ queryKey: ["settings"], queryFn: api.getSettings }); - // Webhook / offline alerting state - const [alertsEnabled, setAlertsEnabled] = useState(false); - const [webhookURL, setWebhookURL] = useState(""); const [thresholdMinutes, setThresholdMinutes] = useState(5); - - // Email state - const [emailEnabled, setEmailEnabled] = useState(false); - const [smtpHost, setSmtpHost] = useState(""); - const [smtpPort, setSmtpPort] = useState(587); - const [smtpUser, setSmtpUser] = useState(""); - const [smtpPass, setSmtpPass] = useState(""); - const [fromAddr, setFromAddr] = useState(""); - const [toAddrs, setToAddrs] = useState(""); // comma-separated in UI - const [useTLS, setUseTLS] = useState(false); - - // Workflow log retention (days). 0 = keep forever. const [logRetentionDays, setLogRetentionDays] = useState(30); - const [saved, setSaved] = useState(false); useEffect(() => { if (!settings) return; - setAlertsEnabled(settings.alerts.enabled); - setWebhookURL(settings.alerts.webhook_url ?? ""); setThresholdMinutes(settings.alerts.offline_threshold_minutes || 5); - setEmailEnabled(settings.email?.enabled ?? false); - setSmtpHost(settings.email?.smtp_host ?? ""); - setSmtpPort(settings.email?.smtp_port || 587); - setSmtpUser(settings.email?.username ?? ""); - setSmtpPass(settings.email?.password ?? ""); - setFromAddr(settings.email?.from_addr ?? ""); - setToAddrs((settings.email?.to_addrs ?? []).join(", ")); - setUseTLS(settings.email?.use_tls ?? false); setLogRetentionDays(settings.workflow_log_retention_days ?? 30); }, [settings]); const { mutate: save, isPending } = useMutation({ - mutationFn: (payload: { - alerts: AlertSettings; - email: EmailSettings; - workflow_log_retention_days?: number | null; - }) => api.saveSettings(payload), + mutationFn: (payload: Parameters[0]) => api.saveSettings(payload), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["settings"] }); setSaved(true); @@ -204,27 +170,12 @@ export default function SettingsPage() { function handleSubmit(e: React.FormEvent) { e.preventDefault(); - const toList = toAddrs - .split(",") - .map((s) => s.trim()) - .filter(Boolean); - + if (!settings) return; + // Preserve legacy alert/email values (managed via Notification Channels now); + // only the offline threshold and log retention are edited here. save({ - alerts: { - enabled: alertsEnabled, - webhook_url: webhookURL, - offline_threshold_minutes: thresholdMinutes, - }, - email: { - enabled: emailEnabled, - smtp_host: smtpHost, - smtp_port: smtpPort, - username: smtpUser, - password: smtpPass, - from_addr: fromAddr, - to_addrs: toList, - use_tls: useTLS, - }, + alerts: { ...settings.alerts, offline_threshold_minutes: thresholdMinutes }, + email: settings.email, workflow_log_retention_days: logRetentionDays, }); } @@ -239,181 +190,79 @@ export default function SettingsPage() { return (
-
+

Settings

-

Configure alerting and monitoring behaviour

+

Configure monitoring, alerting, and integrations.

-
- {/* Webhook alerting */} - - - Webhook Alerting - -

- POST a JSON payload to a URL when a server goes offline. Compatible with Slack, - Discord, n8n, and any service that accepts JSON. -

-
- - - setWebhookURL(e.target.value)} - placeholder="https://hooks.slack.com/... or https://discord.com/api/webhooks/..." - className={inputClass} - /> - - - setThresholdMinutes(Number(e.target.value))} - className="w-32 rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary focus:border-accent/50 focus:outline-none focus:ring-1 focus:ring-accent/30" - /> - +
+ {/* Alerting โ€” replaces the legacy webhook/email settings */} + } + > +
+ + + + + +
- - - {/* Email alerting */} - - - Email Notifications - -

- Send an email when a server goes offline. Uses the same offline threshold as the - webhook setting above. +

+ Webhook, email (SMTP), Discord, Slack, and Telegram destinations are configured under + Notification Channels and attached per monitor.

-
- -
- - setSmtpHost(e.target.value)} - placeholder="smtp.gmail.com" - className={inputClass} - /> - - + + + +
+ } + > + setSmtpPort(Number(e.target.value))} - placeholder="587" - className={inputClass} + min={1} + max={60} + value={thresholdMinutes} + onChange={(e) => setThresholdMinutes(Number(e.target.value))} + className="w-32 rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary focus:border-accent/50 focus:outline-none focus:ring-1 focus:ring-accent/30" /> -
- { - setUseTLS(v); - setSmtpPort(v ? 465 : 587); - }} - /> -
-
-
- - setSmtpUser(e.target.value)} - placeholder="user@example.com" - className={inputClass} - autoComplete="username" - /> - - - setSmtpPass(e.target.value)} - placeholder="App password or SMTP password" - className={inputClass} - autoComplete="new-password" - /> - -
- - setFromAddr(e.target.value)} - placeholder="vantage@example.com" - className={inputClass} - /> - - + + } > - setToAddrs(e.target.value)} - placeholder="admin@example.com, ops@example.com" - className={inputClass} - /> - + + setLogRetentionDays(Number(e.target.value))} + className="w-32 rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary focus:border-accent/50 focus:outline-none focus:ring-1 focus:ring-accent/30" + /> + +
- - {/* Workflow logs */} - - - Workflow Logs - -

- How long to keep workflow run logs on the server before they are - automatically deleted. -

- - setLogRetentionDays(Number(e.target.value))} - className="w-32 rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary focus:border-accent/50 focus:outline-none focus:ring-1 focus:ring-accent/30" - /> - -
+
+ + {saved && Settings saved successfully.} +
+ -
- - {saved && Settings saved successfully.} -
- - -