mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-08 13:08:52 +00:00
feat: M13 — GUI operations (bulk ops, deployment timeline, policy editor, target wizard, audit export, short-lived creds)
Bulk certificate operations: multi-select checkboxes on certificates list with bulk action bar for triggering renewal, revocation (with RFC 5280 reason modal and progress bar), and owner reassignment across selected certificates. Deployment status timeline: visual 4-step lifecycle pipeline (Requested → Issued → Deploying → Active) on certificate detail page, powered by per-cert job queries with animated status indicators for active steps and failure states. Inline policy editor: edit/save/cancel interface on certificate detail page for changing renewal policy and certificate profile assignments via dropdown selectors with lazy-loaded policy and profile lists. Target connector configuration wizard: 3-step modal (Select Type → Configure → Review) with type-specific configuration fields for NGINX, Apache, HAProxy, F5 BIG-IP, and IIS targets including required field validation. Audit trail export: CSV and JSON download buttons on audit page with applied filters preserved in export. Added action filter input for narrower searches. Short-lived credentials dashboard: new page at /short-lived showing ephemeral certificates (profile TTL < 1 hour) with live TTL countdown, auto-refresh every 10 seconds, profile lookup, and stats bar (active/expired/profiles). DataTable enhanced with optional selectable/selectedKeys/onSelectionChange props for checkbox multi-select with select-all toggle and row highlighting. Frontend tests expanded from 53 to 79: full API client endpoint coverage for profiles, owners, teams, agent groups, revocation, approval/rejection, policy violations, and issuer creation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,9 +12,12 @@ interface DataTableProps<T> {
|
||||
emptyMessage?: string;
|
||||
isLoading?: boolean;
|
||||
keyField?: string;
|
||||
selectable?: boolean;
|
||||
selectedKeys?: Set<string>;
|
||||
onSelectionChange?: (keys: Set<string>) => void;
|
||||
}
|
||||
|
||||
export default function DataTable<T>({ columns, data, onRowClick, emptyMessage, isLoading, keyField = 'id' }: 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">
|
||||
@@ -35,11 +38,41 @@ export default function DataTable<T>({ columns, data, onRowClick, emptyMessage,
|
||||
);
|
||||
}
|
||||
|
||||
const allKeys = data.map((item) => (item as Record<string, unknown>)[keyField] as string);
|
||||
const allSelected = selectable && selectedKeys && allKeys.length > 0 && allKeys.every(k => selectedKeys.has(k));
|
||||
|
||||
const toggleAll = () => {
|
||||
if (!onSelectionChange) return;
|
||||
if (allSelected) {
|
||||
onSelectionChange(new Set());
|
||||
} else {
|
||||
onSelectionChange(new Set(allKeys));
|
||||
}
|
||||
};
|
||||
|
||||
const toggleOne = (key: string) => {
|
||||
if (!onSelectionChange || !selectedKeys) return;
|
||||
const next = new Set(selectedKeys);
|
||||
if (next.has(key)) next.delete(key);
|
||||
else next.add(key);
|
||||
onSelectionChange(next);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b-2 border-slate-700">
|
||||
{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"
|
||||
/>
|
||||
</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 || ''}`}>
|
||||
{col.label}
|
||||
@@ -48,19 +81,34 @@ export default function DataTable<T>({ columns, data, onRowClick, emptyMessage,
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((item, i) => (
|
||||
<tr
|
||||
key={(item as Record<string, unknown>)[keyField] as string ?? `row-${i}`}
|
||||
onClick={() => onRowClick?.(item)}
|
||||
className={`border-b border-slate-700/50 transition-colors hover:bg-blue-500/5 ${onRowClick ? 'cursor-pointer' : ''}`}
|
||||
>
|
||||
{columns.map(col => (
|
||||
<td key={col.key} className={`px-4 py-3 ${col.className || ''}`}>
|
||||
{col.render(item)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
{data.map((item, i) => {
|
||||
const rowKey = (item as Record<string, unknown>)[keyField] as string ?? `row-${i}`;
|
||||
const isSelected = selectable && selectedKeys?.has(rowKey);
|
||||
return (
|
||||
<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' : ''}`}
|
||||
>
|
||||
{selectable && (
|
||||
<td className="px-3 py-3 w-10">
|
||||
<input
|
||||
type="checkbox"
|
||||
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"
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{columns.map(col => (
|
||||
<td key={col.key} className={`px-4 py-3 ${col.className || ''}`}>
|
||||
{col.render(item)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@ const nav = [
|
||||
{ to: '/owners', label: 'Owners', icon: 'M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z' },
|
||||
{ to: '/teams', label: 'Teams', icon: 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z' },
|
||||
{ to: '/agent-groups', label: 'Agent Groups', icon: 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10 M9 3v2m6-2v2' },
|
||||
{ to: '/short-lived', label: 'Short-Lived', icon: 'M13 10V3L4 14h7v7l9-11h-7z' },
|
||||
{ to: '/audit', label: 'Audit Trail', icon: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z' },
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user