mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-13 13:58:51 +00:00
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:
@@ -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">—</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>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user