Implement M6: functional GUI views, GitHub Actions CI

Wire all remaining dashboard views to real API: agent detail page
with heartbeat status and capabilities, audit trail with time range/
actor/resource filters, notifications with grouped-by-cert view and
read/unread state, policies with severity summary bar, new issuers
and targets list views. Add GitHub Actions CI with parallel Go and
Frontend jobs. Update Makefile with test-cover and frontend-build
targets.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shankar0123
2026-03-15 11:12:49 -04:00
parent 9e6756d02f
commit f6139252e1
12 changed files with 708 additions and 78 deletions
+68 -5
View File
@@ -1,3 +1,4 @@
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { getAuditEvents } from '../api/client';
import PageHeader from '../components/PageHeader';
@@ -19,13 +20,39 @@ const actionColors: Record<string, string> = {
policy_violated: 'text-red-400',
};
const RESOURCE_TYPES = ['', 'certificate', 'agent', 'job', 'notification', 'policy', 'target', 'issuer'];
const TIME_RANGES = [
{ label: 'All time', value: '' },
{ label: 'Last hour', value: '1h' },
{ label: 'Last 24h', value: '24h' },
{ label: 'Last 7 days', value: '7d' },
{ label: 'Last 30 days', value: '30d' },
];
export default function AuditPage() {
const [resourceType, setResourceType] = useState('');
const [actorFilter, setActorFilter] = useState('');
const [timeRange, setTimeRange] = useState('');
const params: Record<string, string> = {};
if (resourceType) params.resource_type = resourceType;
if (actorFilter) params.actor = actorFilter;
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['audit'],
queryFn: () => getAuditEvents(),
queryKey: ['audit', params],
queryFn: () => getAuditEvents(params),
refetchInterval: 30000,
});
// Client-side time range filtering (server may not support time params)
const filtered = (data?.data || []).filter((e) => {
if (!timeRange) return true;
const ts = new Date(e.timestamp).getTime();
const now = Date.now();
const hours = timeRange === '1h' ? 1 : timeRange === '24h' ? 24 : timeRange === '7d' ? 168 : 720;
return now - ts < hours * 3600 * 1000;
});
const columns: Column<AuditEvent>[] = [
{
key: 'action',
@@ -60,7 +87,7 @@ 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-slate-500">&mdash;</span>;
return (
<span className="text-xs text-slate-400 font-mono truncate max-w-xs block">
{JSON.stringify(e.details).slice(0, 60)}
@@ -73,12 +100,48 @@ export default function AuditPage() {
return (
<>
<PageHeader title="Audit Trail" subtitle={data ? `${data.total} events` : undefined} />
<PageHeader title="Audit Trail" subtitle={data ? `${filtered.length} events` : undefined} />
<div className="px-4 py-3 flex flex-wrap gap-3 border-b border-slate-700/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"
>
<option value="">All resources</option>
{RESOURCE_TYPES.filter(Boolean).map((t) => (
<option key={t} value={t}>{t}</option>
))}
</select>
<input
type="text"
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"
/>
<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"
>
{TIME_RANGES.map((r) => (
<option key={r.value} value={r.value}>{r.label}</option>
))}
</select>
{(resourceType || actorFilter || timeRange) && (
<button
onClick={() => { setResourceType(''); setActorFilter(''); setTimeRange(''); }}
className="text-xs text-slate-400 hover:text-slate-200 transition-colors"
>
Clear filters
</button>
)}
</div>
<div className="flex-1 overflow-y-auto">
{error ? (
<ErrorState error={error as Error} onRetry={() => refetch()} />
) : (
<DataTable columns={columns} data={data?.data || []} isLoading={isLoading} emptyMessage="No audit events" />
<DataTable columns={columns} data={filtered} isLoading={isLoading} emptyMessage="No audit events" />
)}
</div>
</>