Extend badge color settings to cover integration types too
Settings → DNS Badges only let you customize DNS provider badge colors. Expand it into a general Badges page with a second section for the six integration types (Tailscale, Proxmox, Synology, Semaphore, Gitea, Dockhand), applied to the type badge in the Manage integrations table. - New integrationColors key on AppSettings, stored/merged the same way as providerColors via the existing settingsStore. - New GET /api/settings/integration-colors — non-secret, any signed-in user, mirroring /provider-colors — so the badge color can be read without needing admin access to the full settings payload. - Renamed DnsBadgeSettings.tsx -> BadgeSettings.tsx (route /settings/dns-badges -> /settings/badges, sub-nav label "DNS Badges" -> "Badges") with both color sections saved together. Verified end-to-end against the real dev server: PUT persists integration colors independently of provider colors, the public integration-colors endpoint reflects updates immediately, and the change is audit-logged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7fed1dbfa4
commit
ad6783b6a1
9 files changed
+175
-100
No files matched your search
@@ -0,0 +1,142 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { api } from "../../api/client";
|
||||
|
||||
const PROVIDER_NAMES: Record<string, string> = {
|
||||
cloudflare: "Cloudflare",
|
||||
loopia: "Loopia",
|
||||
pihole: "Pi-hole",
|
||||
azure: "Azure DNS",
|
||||
cpanel: "cPanel",
|
||||
technitium: "Technitium",
|
||||
};
|
||||
|
||||
const INTEGRATION_NAMES: Record<string, string> = {
|
||||
tailscale: "Tailscale",
|
||||
proxmox: "Proxmox",
|
||||
synology: "Synology",
|
||||
semaphore: "Semaphore",
|
||||
gitea: "Gitea",
|
||||
dockhand: "Dockhand",
|
||||
};
|
||||
|
||||
function ColorList({
|
||||
names,
|
||||
colors,
|
||||
onChange,
|
||||
}: {
|
||||
names: Record<string, string>;
|
||||
colors: Record<string, string>;
|
||||
onChange: (id: string, color: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="d-flex flex-column gap-2">
|
||||
{Object.entries(names).map(([id, name]) => {
|
||||
const color = colors[id] ?? "#6366f1";
|
||||
return (
|
||||
<div key={id} className="d-flex align-items-center gap-2">
|
||||
<span className="badge" style={{ backgroundColor: `${color}22`, color, border: `1px solid ${color}55`, minWidth: 110 }}>
|
||||
{name}
|
||||
</span>
|
||||
<input
|
||||
type="color"
|
||||
value={color}
|
||||
className="form-control form-control-color"
|
||||
onChange={(e) => onChange(id, e.target.value)}
|
||||
/>
|
||||
<span className="text-secondary small">{color}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function BadgeSettings() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
const [providerColors, setProviderColors] = useState<Record<string, string>>({});
|
||||
const [integrationColors, setIntegrationColors] = useState<Record<string, string>>({});
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([api.settings.providerColors(), api.settings.integrationColors()])
|
||||
.then(([providerRes, integrationRes]) => {
|
||||
setProviderColors(providerRes.providerColors);
|
||||
setIntegrationColors(integrationRes.integrationColors);
|
||||
})
|
||||
.catch((err) => setLoadError(err instanceof Error ? err.message : String(err)))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
async function handleSave() {
|
||||
setSaving(true);
|
||||
setSaveError(null);
|
||||
setSaved(false);
|
||||
try {
|
||||
const res = await api.settings.update({ providerColors, integrationColors });
|
||||
setProviderColors(res.settings.providerColors);
|
||||
setIntegrationColors(res.settings.integrationColors);
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 3000);
|
||||
} catch (err) {
|
||||
setSaveError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) return <div className="text-secondary">Loading…</div>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="d-flex align-items-center justify-content-between mb-3">
|
||||
<h3 className="mb-0">Badges</h3>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
{saveError && <span className="text-danger small">{saveError}</span>}
|
||||
{saved && <span className="text-success small">✓ Colors saved</span>}
|
||||
<button className="btn btn-primary" onClick={handleSave} disabled={saving}>
|
||||
{saving ? "Saving…" : "Save Colors"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loadError && <div className="alert alert-danger">{loadError}</div>}
|
||||
|
||||
<div className="row row-cards">
|
||||
<div className="col-md-6">
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">DNS provider badge colors</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<p className="text-secondary">Customize the color of each DNS provider's badge throughout the app.</p>
|
||||
<ColorList
|
||||
names={PROVIDER_NAMES}
|
||||
colors={providerColors}
|
||||
onChange={(id, color) => setProviderColors((c) => ({ ...c, [id]: color }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">Integration badge colors</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<p className="text-secondary">Customize the color of each integration type's badge throughout the app.</p>
|
||||
<ColorList
|
||||
names={INTEGRATION_NAMES}
|
||||
colors={integrationColors}
|
||||
onChange={(id, color) => setIntegrationColors((c) => ({ ...c, [id]: color }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user