mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 20:21:29 +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:
@@ -7,10 +7,10 @@ export default function AuthGate({ children }: { children: ReactNode }) {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-900 flex items-center justify-center">
|
||||
<div className="min-h-screen bg-page flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold text-blue-400 mb-2">certctl</h1>
|
||||
<p className="text-sm text-slate-400">Connecting...</p>
|
||||
<h1 className="text-2xl font-bold text-brand-500 mb-2">certctl</h1>
|
||||
<p className="text-sm text-ink-muted">Connecting...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -20,7 +20,7 @@ interface DataTableProps<T> {
|
||||
export default function DataTable<T>({ columns, data, onRowClick, emptyMessage, isLoading, keyField = 'id', selectable, selectedKeys, onSelectionChange }: DataTableProps<T>) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-16 text-slate-400">
|
||||
<div className="flex items-center justify-center py-16 text-ink-muted">
|
||||
<svg className="animate-spin h-5 w-5 mr-3" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
@@ -32,7 +32,7 @@ export default function DataTable<T>({ columns, data, onRowClick, emptyMessage,
|
||||
|
||||
if (!data.length) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-16 text-slate-500">
|
||||
<div className="flex items-center justify-center py-16 text-ink-faint">
|
||||
{emptyMessage || 'No data found'}
|
||||
</div>
|
||||
);
|
||||
@@ -62,19 +62,19 @@ export default function DataTable<T>({ columns, data, onRowClick, emptyMessage,
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b-2 border-slate-700">
|
||||
<tr className="border-b-2 border-surface-border bg-surface-muted">
|
||||
{selectable && (
|
||||
<th className="px-3 py-3 w-10">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allSelected || false}
|
||||
onChange={toggleAll}
|
||||
className="rounded border-slate-600 bg-slate-900 text-blue-600 focus:ring-blue-500 focus:ring-offset-0 cursor-pointer"
|
||||
className="rounded border-surface-border bg-white text-brand-500 focus:ring-brand-500 focus:ring-offset-0 cursor-pointer"
|
||||
/>
|
||||
</th>
|
||||
)}
|
||||
{columns.map(col => (
|
||||
<th key={col.key} className={`px-4 py-3 text-left text-xs font-semibold text-slate-400 uppercase tracking-wider ${col.className || ''}`}>
|
||||
<th key={col.key} className={`px-4 py-3 text-left text-xs font-semibold text-ink-muted uppercase tracking-wider ${col.className || ''}`}>
|
||||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
@@ -88,7 +88,7 @@ export default function DataTable<T>({ columns, data, onRowClick, emptyMessage,
|
||||
<tr
|
||||
key={rowKey}
|
||||
onClick={() => onRowClick?.(item)}
|
||||
className={`border-b border-slate-700/50 transition-colors hover:bg-blue-500/5 ${onRowClick ? 'cursor-pointer' : ''} ${isSelected ? 'bg-blue-500/10' : ''}`}
|
||||
className={`border-b border-surface-border/50 transition-colors hover:bg-surface-muted ${onRowClick ? 'cursor-pointer' : ''} ${isSelected ? 'bg-brand-50' : ''}`}
|
||||
>
|
||||
{selectable && (
|
||||
<td className="px-3 py-3 w-10">
|
||||
@@ -97,12 +97,12 @@ export default function DataTable<T>({ columns, data, onRowClick, emptyMessage,
|
||||
checked={isSelected || false}
|
||||
onChange={(e) => { e.stopPropagation(); toggleOne(rowKey); }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="rounded border-slate-600 bg-slate-900 text-blue-600 focus:ring-blue-500 focus:ring-offset-0 cursor-pointer"
|
||||
className="rounded border-surface-border bg-white text-brand-500 focus:ring-brand-500 focus:ring-offset-0 cursor-pointer"
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{columns.map(col => (
|
||||
<td key={col.key} className={`px-4 py-3 ${col.className || ''}`}>
|
||||
<td key={col.key} className={`px-4 py-3 text-ink ${col.className || ''}`}>
|
||||
{col.render(item)}
|
||||
</td>
|
||||
))}
|
||||
|
||||
@@ -26,10 +26,10 @@ export default class ErrorBoundary extends Component<Props, State> {
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-slate-900">
|
||||
<div className="flex items-center justify-center min-h-screen bg-page">
|
||||
<div className="text-center p-8">
|
||||
<h1 className="text-xl font-semibold text-red-400 mb-2">Something went wrong</h1>
|
||||
<p className="text-sm text-slate-400 mb-4">
|
||||
<h1 className="text-xl font-semibold text-red-700 mb-2">Something went wrong</h1>
|
||||
<p className="text-sm text-ink-muted mb-4">
|
||||
{this.state.error?.message || 'An unexpected error occurred'}
|
||||
</p>
|
||||
<button
|
||||
@@ -37,7 +37,7 @@ export default class ErrorBoundary extends Component<Props, State> {
|
||||
this.setState({ hasError: false, error: null });
|
||||
window.location.reload();
|
||||
}}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-500"
|
||||
className="px-4 py-2 bg-brand-500 text-white rounded text-sm hover:bg-brand-600"
|
||||
>
|
||||
Reload Page
|
||||
</button>
|
||||
|
||||
@@ -5,12 +5,12 @@ interface ErrorStateProps {
|
||||
|
||||
export default function ErrorState({ error, onRetry }: ErrorStateProps) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-slate-400">
|
||||
<svg className="w-12 h-12 text-red-400 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<div className="flex flex-col items-center justify-center py-16 text-ink-muted">
|
||||
<svg className="w-12 h-12 text-red-700 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
||||
</svg>
|
||||
<p className="text-sm mb-2">Failed to load data</p>
|
||||
<p className="text-xs text-slate-500 mb-4">{error.message}</p>
|
||||
<p className="text-sm mb-2 text-ink">Failed to load data</p>
|
||||
<p className="text-xs text-ink-faint mb-4">{error.message}</p>
|
||||
{onRetry && (
|
||||
<button onClick={onRetry} className="btn btn-primary text-xs">
|
||||
Retry
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NavLink, Outlet } from 'react-router-dom';
|
||||
import { useAuth } from './AuthProvider';
|
||||
import logo from '../assets/certctl-logo.png';
|
||||
|
||||
const nav = [
|
||||
{ to: '/', label: 'Dashboard', icon: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-4 0h4' },
|
||||
@@ -21,7 +22,7 @@ const nav = [
|
||||
|
||||
function Icon({ d }: { d: string }) {
|
||||
return (
|
||||
<svg className="w-5 h-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<svg className="w-[18px] h-[18px] shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d={d} />
|
||||
</svg>
|
||||
);
|
||||
@@ -32,23 +33,30 @@ export default function Layout() {
|
||||
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-64 bg-slate-800 border-r border-slate-700 flex flex-col">
|
||||
<div className="p-6 border-b border-slate-700">
|
||||
<h1 className="text-xl font-bold text-blue-400">certctl</h1>
|
||||
<p className="text-xs text-slate-400 uppercase tracking-wider mt-1">Certificate Control Plane</p>
|
||||
{/* Sidebar — deep teal from logo */}
|
||||
<aside className="w-60 bg-sidebar flex flex-col shadow-xl">
|
||||
{/* Logo — large and prominent */}
|
||||
<div className="px-4 pt-5 pb-4 flex flex-col items-center gap-2">
|
||||
<div className="bg-white rounded-xl p-2 shadow-lg">
|
||||
<img src={logo} alt="certctl" className="h-16 w-16" />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h1 className="text-lg font-bold text-white tracking-tight">certctl</h1>
|
||||
<p className="text-[10px] text-brand-300 uppercase tracking-[0.2em]">Control Plane</p>
|
||||
</div>
|
||||
</div>
|
||||
<nav className="flex-1 p-4 space-y-1 overflow-y-auto">
|
||||
|
||||
<nav className="flex-1 py-2 px-3 space-y-0.5 overflow-y-auto">
|
||||
{nav.map(item => (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
end={item.to === '/'}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-colors ${
|
||||
`flex items-center gap-3 px-3 py-2 text-[13px] rounded transition-all duration-150 ${
|
||||
isActive
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'text-slate-400 hover:bg-slate-700 hover:text-slate-200'
|
||||
? 'bg-white/15 text-white font-semibold shadow-sm'
|
||||
: 'text-sidebar-text hover:text-white hover:bg-white/10'
|
||||
}`
|
||||
}
|
||||
>
|
||||
@@ -57,12 +65,13 @@ export default function Layout() {
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
<div className="p-4 border-t border-slate-700 flex items-center justify-between">
|
||||
<span className="text-xs text-slate-500">certctl v1.0-dev</span>
|
||||
|
||||
<div className="px-5 py-3 border-t border-white/10 flex items-center justify-between">
|
||||
<span className="text-[10px] text-brand-300/60 font-mono">v2.0.2</span>
|
||||
{authRequired && (
|
||||
<button
|
||||
onClick={logout}
|
||||
className="text-xs text-slate-500 hover:text-slate-300 transition-colors"
|
||||
className="text-xs text-sidebar-text hover:text-white transition-colors"
|
||||
title="Sign out"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
@@ -73,8 +82,8 @@ export default function Layout() {
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main content */}
|
||||
<main className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Main content — light background */}
|
||||
<main className="flex-1 flex flex-col overflow-hidden bg-page">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -6,10 +6,10 @@ interface PageHeaderProps {
|
||||
|
||||
export default function PageHeader({ title, subtitle, action }: PageHeaderProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-700 bg-slate-800">
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-surface-border bg-surface">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">{title}</h2>
|
||||
{subtitle && <p className="text-sm text-slate-400 mt-0.5">{subtitle}</p>}
|
||||
<h2 className="text-lg font-semibold text-ink">{title}</h2>
|
||||
{subtitle && <p className="text-sm text-ink-muted mt-0.5">{subtitle}</p>}
|
||||
</div>
|
||||
{action}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const statusStyles: Record<string, string> = {
|
||||
// Certificate statuses
|
||||
Active: 'badge-success',
|
||||
Expiring: 'badge-warning',
|
||||
Expired: 'badge-danger',
|
||||
@@ -8,6 +9,8 @@ const statusStyles: Record<string, string> = {
|
||||
Revoked: 'badge-danger',
|
||||
// Job statuses
|
||||
Pending: 'badge-info',
|
||||
AwaitingCSR: 'badge-info',
|
||||
AwaitingApproval: 'badge-info',
|
||||
Running: 'badge-warning',
|
||||
Completed: 'badge-success',
|
||||
Failed: 'badge-danger',
|
||||
|
||||
Reference in New Issue
Block a user