mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-12 13:38:56 +00:00
feat: dashboard theme overhaul — light content area with branded teal sidebar
Complete frontend visual redesign using certctl logo color palette: - Deep teal sidebar (#0c2e25) with prominent centered logo (64px in white pill) - Light content area (#f0f4f8) with white cards and visible borders - Brand colors from logo: teal (#2ea88f), blue (#3b7dd8), orange (#e8873a), green (#4ebe6e) - Inter + JetBrains Mono typography, colored stat card top borders - All 17 pages + 7 components updated (25 files, ~700 lines changed) - 15 new dashboard screenshots replacing old dark theme screenshots - Prometheus metrics e2e test added, integration test mock fixes - Docs updated: architecture.md theme description, testing-guide.md DNS-PERSIST-01 coverage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+26
-26
@@ -9,16 +9,16 @@ import { formatDateTime } from '../api/utils';
|
||||
import type { AuditEvent } from '../api/types';
|
||||
|
||||
const actionColors: Record<string, string> = {
|
||||
certificate_created: 'text-emerald-400',
|
||||
renewal_triggered: 'text-blue-400',
|
||||
renewal_job_created: 'text-blue-400',
|
||||
renewal_completed: 'text-emerald-400',
|
||||
deployment_completed: 'text-emerald-400',
|
||||
deployment_failed: 'text-red-400',
|
||||
expiration_alert_sent: 'text-amber-400',
|
||||
agent_registered: 'text-blue-400',
|
||||
policy_violated: 'text-red-400',
|
||||
certificate_revoked: 'text-red-400',
|
||||
certificate_created: 'text-emerald-600',
|
||||
renewal_triggered: 'text-brand-500',
|
||||
renewal_job_created: 'text-brand-500',
|
||||
renewal_completed: 'text-emerald-600',
|
||||
deployment_completed: 'text-emerald-600',
|
||||
deployment_failed: 'text-red-600',
|
||||
expiration_alert_sent: 'text-amber-600',
|
||||
agent_registered: 'text-brand-500',
|
||||
policy_violated: 'text-red-600',
|
||||
certificate_revoked: 'text-red-600',
|
||||
};
|
||||
|
||||
const RESOURCE_TYPES = ['', 'certificate', 'agent', 'job', 'notification', 'policy', 'target', 'issuer'];
|
||||
@@ -94,7 +94,7 @@ export default function AuditPage() {
|
||||
key: 'action',
|
||||
label: 'Action',
|
||||
render: (e) => (
|
||||
<span className={`text-sm font-medium ${actionColors[e.action] || 'text-slate-300'}`}>
|
||||
<span className={`text-sm font-medium ${actionColors[e.action] || 'text-ink'}`}>
|
||||
{e.action.replace(/_/g, ' ')}
|
||||
</span>
|
||||
),
|
||||
@@ -104,8 +104,8 @@ export default function AuditPage() {
|
||||
label: 'Actor',
|
||||
render: (e) => (
|
||||
<div>
|
||||
<div className="text-sm text-slate-200">{e.actor}</div>
|
||||
<div className="text-xs text-slate-500">{e.actor_type}</div>
|
||||
<div className="text-sm text-ink">{e.actor}</div>
|
||||
<div className="text-xs text-ink-faint">{e.actor_type}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -114,8 +114,8 @@ export default function AuditPage() {
|
||||
label: 'Resource',
|
||||
render: (e) => (
|
||||
<div>
|
||||
<div className="text-sm text-slate-300">{e.resource_type}</div>
|
||||
<div className="text-xs text-slate-500 font-mono">{e.resource_id}</div>
|
||||
<div className="text-sm text-ink">{e.resource_type}</div>
|
||||
<div className="text-xs text-ink-faint font-mono">{e.resource_id}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -123,15 +123,15 @@ export default function AuditPage() {
|
||||
key: 'details',
|
||||
label: 'Details',
|
||||
render: (e) => {
|
||||
if (!e.details || Object.keys(e.details).length === 0) return <span className="text-slate-500">—</span>;
|
||||
if (!e.details || Object.keys(e.details).length === 0) return <span className="text-ink-faint">—</span>;
|
||||
return (
|
||||
<span className="text-xs text-slate-400 font-mono truncate max-w-xs block">
|
||||
<span className="text-xs text-ink-muted font-mono truncate max-w-xs block">
|
||||
{JSON.stringify(e.details).slice(0, 60)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{ key: 'time', label: 'Time', render: (e) => <span className="text-xs text-slate-400">{formatDateTime(e.timestamp)}</span> },
|
||||
{ key: 'time', label: 'Time', render: (e) => <span className="text-xs text-ink-muted">{formatDateTime(e.timestamp)}</span> },
|
||||
];
|
||||
|
||||
const hasFilters = resourceType || actorFilter || timeRange || actionFilter;
|
||||
@@ -144,21 +144,21 @@ export default function AuditPage() {
|
||||
action={
|
||||
filtered.length > 0 ? (
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => exportCSV(filtered)} className="btn btn-ghost text-xs border border-slate-600">
|
||||
<button onClick={() => exportCSV(filtered)} className="btn btn-ghost text-xs border border-surface-border">
|
||||
Export CSV
|
||||
</button>
|
||||
<button onClick={() => exportJSON(filtered)} className="btn btn-ghost text-xs border border-slate-600">
|
||||
<button onClick={() => exportJSON(filtered)} className="btn btn-ghost text-xs border border-surface-border">
|
||||
Export JSON
|
||||
</button>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
<div className="px-4 py-3 flex flex-wrap gap-3 border-b border-slate-700/50">
|
||||
<div className="px-4 py-3 flex flex-wrap gap-3 border-b border-surface-border/50">
|
||||
<select
|
||||
value={resourceType}
|
||||
onChange={(e) => setResourceType(e.target.value)}
|
||||
className="bg-slate-800 border border-slate-600 rounded px-3 py-1.5 text-xs text-slate-300 focus:outline-none focus:border-blue-500"
|
||||
className="bg-surface border border-surface-border rounded px-3 py-1.5 text-xs text-ink focus:outline-none focus:border-brand-400"
|
||||
>
|
||||
<option value="">All resources</option>
|
||||
{RESOURCE_TYPES.filter(Boolean).map((t) => (
|
||||
@@ -170,19 +170,19 @@ export default function AuditPage() {
|
||||
placeholder="Filter by actor..."
|
||||
value={actorFilter}
|
||||
onChange={(e) => setActorFilter(e.target.value)}
|
||||
className="bg-slate-800 border border-slate-600 rounded px-3 py-1.5 text-xs text-slate-300 placeholder-slate-500 focus:outline-none focus:border-blue-500 w-40"
|
||||
className="bg-surface border border-surface-border rounded px-3 py-1.5 text-xs text-ink placeholder-ink-faint focus:outline-none focus:border-brand-400 w-40"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filter by action..."
|
||||
value={actionFilter}
|
||||
onChange={(e) => setActionFilter(e.target.value)}
|
||||
className="bg-slate-800 border border-slate-600 rounded px-3 py-1.5 text-xs text-slate-300 placeholder-slate-500 focus:outline-none focus:border-blue-500 w-40"
|
||||
className="bg-surface border border-surface-border rounded px-3 py-1.5 text-xs text-ink placeholder-ink-faint focus:outline-none focus:border-brand-400 w-40"
|
||||
/>
|
||||
<select
|
||||
value={timeRange}
|
||||
onChange={(e) => setTimeRange(e.target.value)}
|
||||
className="bg-slate-800 border border-slate-600 rounded px-3 py-1.5 text-xs text-slate-300 focus:outline-none focus:border-blue-500"
|
||||
className="bg-surface border border-surface-border rounded px-3 py-1.5 text-xs text-ink focus:outline-none focus:border-brand-400"
|
||||
>
|
||||
{TIME_RANGES.map((r) => (
|
||||
<option key={r.value} value={r.value}>{r.label}</option>
|
||||
@@ -191,7 +191,7 @@ export default function AuditPage() {
|
||||
{hasFilters && (
|
||||
<button
|
||||
onClick={() => { setResourceType(''); setActorFilter(''); setTimeRange(''); setActionFilter(''); }}
|
||||
className="text-xs text-slate-400 hover:text-slate-200 transition-colors"
|
||||
className="text-xs text-ink-muted hover:text-ink transition-colors"
|
||||
>
|
||||
Clear filters
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user