Skip to content

Commit 7376125

Browse files
authored
Merge pull request #212 from objectstack-ai/copilot/expose-kernel-capabilities-admin-ui
2 parents 9f09806 + 7449b03 commit 7376125

File tree

16 files changed

+1839
-38
lines changed

16 files changed

+1839
-38
lines changed

apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"@radix-ui/react-select": "^2.2.6",
1818
"@radix-ui/react-slot": "^1.2.3",
1919
"@radix-ui/react-tooltip": "^1.2.8",
20+
"@tanstack/react-query": "^5.90.20",
2021
"better-auth": "^1.4.18",
2122
"class-variance-authority": "^0.7.1",
2223
"clsx": "^2.1.1",

apps/web/src/App.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ const AuditPage = lazy(() => import('./pages/settings/audit'));
2626
const PackagesPage = lazy(() => import('./pages/settings/packages'));
2727
const AccountSettingsPage = lazy(() => import('./pages/settings/account'));
2828
const SecuritySettingsPage = lazy(() => import('./pages/settings/security'));
29+
const JobsPage = lazy(() => import('./pages/settings/jobs'));
30+
const PluginsPage = lazy(() => import('./pages/settings/plugins'));
31+
const MetricsPage = lazy(() => import('./pages/settings/metrics'));
32+
const NotificationsPage = lazy(() => import('./pages/settings/notifications'));
2933

3034
// ── Business Apps ─────────────────────────────────────────────
3135
const BusinessAppPage = lazy(() => import('./pages/apps/app'));
@@ -66,6 +70,10 @@ export function App() {
6670
<Route path="/settings/sso" element={<SSOSettingsPage />} />
6771
<Route path="/settings/audit" element={<AuditPage />} />
6872
<Route path="/settings/packages" element={<PackagesPage />} />
73+
<Route path="/settings/jobs" element={<JobsPage />} />
74+
<Route path="/settings/plugins" element={<PluginsPage />} />
75+
<Route path="/settings/metrics" element={<MetricsPage />} />
76+
<Route path="/settings/notifications" element={<NotificationsPage />} />
6977
<Route path="/settings/account" element={<AccountSettingsPage />} />
7078
<Route path="/settings/security" element={<SecuritySettingsPage />} />
7179
</Route>

apps/web/src/components/layouts/SettingsLayout.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import {
1212
UsersRound,
1313
ClipboardList,
1414
LayoutDashboard,
15+
Briefcase,
16+
BarChart3,
17+
Bell,
1518
} from 'lucide-react';
1619
import {
1720
Sidebar,
@@ -52,6 +55,10 @@ const navSecurity = [
5255

5356
const navSystem = [
5457
{ title: 'Packages', href: '/settings/packages', icon: Package },
58+
{ title: 'Plugins', href: '/settings/plugins', icon: Blocks },
59+
{ title: 'Jobs', href: '/settings/jobs', icon: Briefcase },
60+
{ title: 'Metrics', href: '/settings/metrics', icon: BarChart3 },
61+
{ title: 'Notifications', href: '/settings/notifications', icon: Bell },
5562
];
5663

5764
const navAccount = [

apps/web/src/main.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
11
import { StrictMode } from 'react';
22
import { createRoot } from 'react-dom/client';
33
import { BrowserRouter } from 'react-router-dom';
4+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
45
import { App } from './App';
56
import './index.css';
67

8+
const queryClient = new QueryClient({
9+
defaultOptions: {
10+
queries: {
11+
refetchOnWindowFocus: false,
12+
retry: 1,
13+
},
14+
},
15+
});
16+
717
createRoot(document.getElementById('root')!).render(
818
<StrictMode>
9-
<BrowserRouter basename="/console">
10-
<App />
11-
</BrowserRouter>
19+
<QueryClientProvider client={queryClient}>
20+
<BrowserRouter basename="/console">
21+
<App />
22+
</BrowserRouter>
23+
</QueryClientProvider>
1224
</StrictMode>,
1325
);

apps/web/src/pages/settings/audit.tsx

Lines changed: 205 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,76 @@
1+
import { useState } from 'react';
2+
import { useQuery } from '@tanstack/react-query';
13
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
24
import { Badge } from '@/components/ui/badge';
5+
import { Input } from '@/components/ui/input';
6+
import { Label } from '@/components/ui/label';
7+
import {
8+
Select,
9+
SelectContent,
10+
SelectItem,
11+
SelectTrigger,
12+
SelectValue,
13+
} from '@/components/ui/select';
14+
import {
15+
Table,
16+
TableBody,
17+
TableCell,
18+
TableHead,
19+
TableHeader,
20+
TableRow,
21+
} from '@/components/ui/table';
22+
23+
interface AuditEvent {
24+
id: string;
25+
eventType: string;
26+
objectName: string;
27+
recordId?: string;
28+
userId?: string;
29+
timestamp: string;
30+
changes?: Array<{
31+
field: string;
32+
oldValue: any;
33+
newValue: any;
34+
}>;
35+
metadata?: Record<string, any>;
36+
}
37+
38+
const eventTypeColors: Record<string, string> = {
39+
'data.create': 'default',
40+
'data.update': 'secondary',
41+
'data.delete': 'destructive',
42+
'data.find': 'outline',
43+
'job.enqueued': 'secondary',
44+
'job.completed': 'outline',
45+
'job.failed': 'destructive',
46+
};
347

448
export default function AuditPage() {
49+
const [objectFilter, setObjectFilter] = useState<string>('');
50+
const [userFilter, setUserFilter] = useState<string>('');
51+
const [eventTypeFilter, setEventTypeFilter] = useState<string>('all');
52+
const [startDate, setStartDate] = useState<string>('');
53+
const [endDate, setEndDate] = useState<string>('');
54+
55+
const { data: eventsData, isLoading } = useQuery({
56+
queryKey: ['audit', 'events', objectFilter, userFilter, eventTypeFilter, startDate, endDate],
57+
queryFn: async () => {
58+
const params = new URLSearchParams();
59+
if (objectFilter) params.append('objectName', objectFilter);
60+
if (userFilter) params.append('userId', userFilter);
61+
if (eventTypeFilter !== 'all') params.append('eventType', eventTypeFilter);
62+
if (startDate) params.append('startDate', startDate);
63+
if (endDate) params.append('endDate', endDate);
64+
params.append('limit', '100');
65+
66+
const response = await fetch(`/api/v1/audit/events?${params}`);
67+
if (!response.ok) throw new Error('Failed to fetch audit events');
68+
return response.json();
69+
},
70+
});
71+
72+
const events: AuditEvent[] = eventsData?.data || [];
73+
574
return (
675
<div className="space-y-6">
776
<div>
@@ -11,20 +80,147 @@ export default function AuditPage() {
1180
</p>
1281
</div>
1382

83+
{/* Filters */}
84+
<Card>
85+
<CardHeader>
86+
<CardTitle>Filters</CardTitle>
87+
<CardDescription>Filter audit events by various criteria</CardDescription>
88+
</CardHeader>
89+
<CardContent>
90+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
91+
<div className="space-y-2">
92+
<Label htmlFor="object">Object Name</Label>
93+
<Input
94+
id="object"
95+
placeholder="e.g., accounts"
96+
value={objectFilter}
97+
onChange={(e) => setObjectFilter(e.target.value)}
98+
/>
99+
</div>
100+
<div className="space-y-2">
101+
<Label htmlFor="user">User ID</Label>
102+
<Input
103+
id="user"
104+
placeholder="Filter by user"
105+
value={userFilter}
106+
onChange={(e) => setUserFilter(e.target.value)}
107+
/>
108+
</div>
109+
<div className="space-y-2">
110+
<Label htmlFor="eventType">Event Type</Label>
111+
<Select value={eventTypeFilter} onValueChange={setEventTypeFilter}>
112+
<SelectTrigger id="eventType">
113+
<SelectValue />
114+
</SelectTrigger>
115+
<SelectContent>
116+
<SelectItem value="all">All Types</SelectItem>
117+
<SelectItem value="data.create">Create</SelectItem>
118+
<SelectItem value="data.update">Update</SelectItem>
119+
<SelectItem value="data.delete">Delete</SelectItem>
120+
<SelectItem value="data.find">Read</SelectItem>
121+
<SelectItem value="job.enqueued">Job Enqueued</SelectItem>
122+
<SelectItem value="job.completed">Job Completed</SelectItem>
123+
<SelectItem value="job.failed">Job Failed</SelectItem>
124+
</SelectContent>
125+
</Select>
126+
</div>
127+
<div className="space-y-2">
128+
<Label htmlFor="startDate">Start Date</Label>
129+
<Input
130+
id="startDate"
131+
type="date"
132+
value={startDate}
133+
onChange={(e) => setStartDate(e.target.value)}
134+
/>
135+
</div>
136+
<div className="space-y-2">
137+
<Label htmlFor="endDate">End Date</Label>
138+
<Input
139+
id="endDate"
140+
type="date"
141+
value={endDate}
142+
onChange={(e) => setEndDate(e.target.value)}
143+
/>
144+
</div>
145+
</div>
146+
</CardContent>
147+
</Card>
148+
149+
{/* Event Log Table */}
14150
<Card>
15151
<CardHeader>
16-
<div className="flex items-center justify-between gap-2">
17-
<CardTitle>Event Log</CardTitle>
18-
<Badge variant="secondary">Scaffold</Badge>
152+
<div className="flex items-center justify-between">
153+
<div>
154+
<CardTitle>Event Log</CardTitle>
155+
<CardDescription>
156+
Showing {events.length} event{events.length !== 1 ? 's' : ''}
157+
</CardDescription>
158+
</div>
19159
</div>
20-
<CardDescription>
21-
A chronological stream of CRUD events, field-level changes, and login activity.
22-
</CardDescription>
23160
</CardHeader>
24161
<CardContent>
25-
<p className="text-sm text-muted-foreground">
26-
Connect to <code>@objectos/audit</code> API to display events here.
27-
</p>
162+
{isLoading ? (
163+
<div className="flex items-center justify-center py-8">
164+
<div className="animate-spin rounded-full size-8 border-2 border-muted border-t-primary" />
165+
</div>
166+
) : events.length === 0 ? (
167+
<div className="text-center py-8 text-muted-foreground">
168+
No audit events found
169+
</div>
170+
) : (
171+
<Table>
172+
<TableHeader>
173+
<TableRow>
174+
<TableHead>Timestamp</TableHead>
175+
<TableHead>Event Type</TableHead>
176+
<TableHead>Object</TableHead>
177+
<TableHead>Record ID</TableHead>
178+
<TableHead>User</TableHead>
179+
<TableHead>Changes</TableHead>
180+
</TableRow>
181+
</TableHeader>
182+
<TableBody>
183+
{events.map((event) => (
184+
<TableRow key={event.id}>
185+
<TableCell className="text-sm text-muted-foreground">
186+
{new Date(event.timestamp).toLocaleString()}
187+
</TableCell>
188+
<TableCell>
189+
<Badge variant={eventTypeColors[event.eventType] as any || 'secondary'}>
190+
{event.eventType}
191+
</Badge>
192+
</TableCell>
193+
<TableCell className="font-medium">{event.objectName}</TableCell>
194+
<TableCell className="text-sm text-muted-foreground">
195+
{event.recordId || '-'}
196+
</TableCell>
197+
<TableCell className="text-sm text-muted-foreground">
198+
{event.userId || 'System'}
199+
</TableCell>
200+
<TableCell className="text-sm">
201+
{event.changes && event.changes.length > 0 ? (
202+
<div className="space-y-1">
203+
{event.changes.slice(0, 2).map((change, idx) => (
204+
<div key={idx} className="text-xs">
205+
<span className="font-medium">{change.field}:</span>{' '}
206+
{JSON.stringify(change.oldValue)}{JSON.stringify(change.newValue)}
207+
</div>
208+
))}
209+
{event.changes.length > 2 && (
210+
<div className="text-xs text-muted-foreground">
211+
+{event.changes.length - 2} more
212+
</div>
213+
)}
214+
</div>
215+
) : (
216+
'-'
217+
)}
218+
</TableCell>
219+
</TableRow>
220+
))}
221+
</TableBody>
222+
</Table>
223+
)}
28224
</CardContent>
29225
</Card>
30226
</div>

0 commit comments

Comments
 (0)