Skip to content

Commit ddb4390

Browse files
authored
Merge pull request #104 from amazeeio/be-ux-update
Admin UX update
2 parents abd38ca + 1fd38d0 commit ddb4390

File tree

22 files changed

+4270
-2599
lines changed

22 files changed

+4270
-2599
lines changed

frontend/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM uselagoon/node-20:25.1.0 AS builder
1+
FROM uselagoon/node-22:25.1.0 AS builder
22

33
WORKDIR /app
44

@@ -15,7 +15,7 @@ COPY . .
1515
RUN npm run build
1616

1717
# Production stage
18-
FROM uselagoon/node-20:25.1.0
18+
FROM uselagoon/node-22:25.1.0
1919

2020
WORKDIR /app
2121

frontend/package-lock.json

Lines changed: 1711 additions & 1127 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/src/app/admin/audit-logs/page.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { format, parseISO } from 'date-fns';
55
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
66
import { Input } from '@/components/ui/input';
77
import { Button } from '@/components/ui/button';
8+
89
import {
910
Table,
1011
TableBody,
@@ -206,7 +207,7 @@ export default function AuditLogsPage() {
206207
}
207208

208209
return (
209-
<div className="space-y-6">
210+
<div className="space-y-4">
210211
<div className="flex justify-between items-center">
211212
<h1 className="text-3xl font-bold">Audit Logs</h1>
212213
</div>
@@ -302,13 +303,12 @@ export default function AuditLogsPage() {
302303
{log.user_email || 'Anonymous'}
303304
</TableCell>
304305
<TableCell>
305-
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
306-
log.request_source === 'frontend'
306+
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${log.request_source === 'frontend'
307307
? 'bg-green-100 text-green-800'
308308
: log.request_source === 'api'
309-
? 'bg-yellow-100 text-yellow-800'
310-
: 'bg-gray-100 text-gray-800'
311-
}`}>
309+
? 'bg-yellow-100 text-yellow-800'
310+
: 'bg-gray-100 text-gray-800'
311+
}`}>
312312
{log.request_source || 'Unknown'}
313313
</span>
314314
</TableCell>

frontend/src/app/admin/private-ai-keys/page.tsx

Lines changed: 65 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
'use client';
22

3-
import { useState, useMemo } from 'react';
4-
import { useQuery, useMutation, useQueryClient, useQueries } from '@tanstack/react-query';
3+
import { useState } from 'react';
4+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
55
import { Button } from '@/components/ui/button';
66
import { useToast } from '@/hooks/use-toast';
77
import { Loader2, Search } from 'lucide-react';
8-
import { get, del, put } from '@/utils/api';
8+
import { get, del, put, post } from '@/utils/api';
99
import { useDebounce } from '@/hooks/use-debounce';
1010
import {
1111
Command,
@@ -21,30 +21,29 @@ import {
2121
PopoverTrigger,
2222
} from '@/components/ui/popover';
2323
import { PrivateAIKeysTable } from '@/components/private-ai-keys-table';
24+
import { CreateAIKeyDialog } from '@/components/create-ai-key-dialog';
2425
import { PrivateAIKey } from '@/types/private-ai-key';
26+
import { usePrivateAIKeysData } from '@/hooks/use-private-ai-keys-data';
2527

2628
interface User {
2729
id: number;
2830
email: string;
29-
}
30-
31-
interface SpendInfo {
32-
spend: number;
33-
expires: string;
31+
is_active: boolean;
32+
role: string;
33+
team_id: number | null;
3434
created_at: string;
35-
updated_at: string;
36-
max_budget: number | null;
37-
budget_duration: string | null;
38-
budget_reset_at: string | null;
3935
}
4036

37+
38+
4139
export default function PrivateAIKeysPage() {
4240
const { toast } = useToast();
4341
const queryClient = useQueryClient();
4442
const [searchTerm, setSearchTerm] = useState('');
4543
const [isUserSearchOpen, setIsUserSearchOpen] = useState(false);
4644
const [selectedUser, setSelectedUser] = useState<User | null>(null);
4745
const [loadedSpendKeys, setLoadedSpendKeys] = useState<Set<number>>(new Set());
46+
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
4847
const debouncedSearchTerm = useDebounce(searchTerm, 300);
4948

5049
// Queries
@@ -58,25 +57,12 @@ export default function PrivateAIKeysPage() {
5857
const data = await response.json();
5958
return data;
6059
},
60+
refetchInterval: 30000, // Refetch every 30 seconds to detect new keys
61+
refetchIntervalInBackground: true, // Continue polling even when tab is not active
6162
});
6263

63-
// Get unique team IDs from the keys
64-
const teamIds = Array.from(new Set(privateAIKeys.filter(key => key.team_id).map(key => key.team_id)));
65-
66-
// Fetch team details for each team ID
67-
const { data: teamDetails = {} } = useQuery({
68-
queryKey: ['team-details', teamIds],
69-
queryFn: async () => {
70-
const teamPromises = teamIds.map(async (teamId) => {
71-
const response = await get(`teams/${teamId}`);
72-
const data = await response.json();
73-
return [teamId, data];
74-
});
75-
const teamResults = await Promise.all(teamPromises);
76-
return Object.fromEntries(teamResults);
77-
},
78-
enabled: teamIds.length > 0,
79-
});
64+
// Use shared hook for data fetching
65+
const { teamDetails, teamMembers, spendMap, regions } = usePrivateAIKeysData(privateAIKeys, loadedSpendKeys);
8066

8167
// Query to get all users for displaying emails
8268
const { data: usersMap = {} } = useQuery<Record<number, User>>({
@@ -129,38 +115,40 @@ export default function PrivateAIKeysPage() {
129115
}
130116
};
131117

132-
// Query to get spend information for each key
133-
const spendQueries = useQueries({
134-
queries: privateAIKeys
135-
.filter(key => loadedSpendKeys.has(key.id))
136-
.map((key) => ({
137-
queryKey: ['private-ai-key-spend', key.id],
138-
queryFn: async () => {
139-
const response = await get(`/private-ai-keys/${key.id}/spend`);
140-
const data = await response.json();
141-
return data as SpendInfo;
142-
},
143-
refetchInterval: 60000, // Refetch every minute
144-
})),
118+
// Mutations
119+
const createKeyMutation = useMutation({
120+
mutationFn: async (data: { name: string; region_id: number; owner_id?: number; team_id?: number; key_type: 'full' | 'llm' | 'vector' }) => {
121+
const endpoint = data.key_type === 'full' ? 'private-ai-keys' :
122+
data.key_type === 'llm' ? 'private-ai-keys/token' :
123+
'private-ai-keys/vector-db';
124+
const response = await post(endpoint, data);
125+
return response.json();
126+
},
127+
onSuccess: () => {
128+
queryClient.invalidateQueries({ queryKey: ['private-ai-keys'] });
129+
queryClient.refetchQueries({ queryKey: ['private-ai-keys'], exact: true });
130+
setIsCreateDialogOpen(false);
131+
toast({
132+
title: 'Success',
133+
description: 'Private AI key created successfully',
134+
});
135+
},
136+
onError: (error: Error) => {
137+
toast({
138+
title: 'Error',
139+
description: error.message,
140+
variant: 'destructive',
141+
});
142+
},
145143
});
146144

147-
// Create a map of spend information
148-
const spendMap = useMemo(() => {
149-
return spendQueries.reduce((acc, query, index) => {
150-
if (query.data) {
151-
acc[privateAIKeys.filter(key => loadedSpendKeys.has(key.id))[index].id] = query.data;
152-
}
153-
return acc;
154-
}, {} as Record<number, SpendInfo>);
155-
}, [spendQueries, privateAIKeys, loadedSpendKeys]);
156-
157-
// Mutations
158145
const deletePrivateAIKeyMutation = useMutation({
159146
mutationFn: async (keyId: number) => {
160147
await del(`/private-ai-keys/${keyId}`);
161148
},
162149
onSuccess: () => {
163150
queryClient.invalidateQueries({ queryKey: ['private-ai-keys'] });
151+
queryClient.refetchQueries({ queryKey: ['private-ai-keys'], exact: true });
164152
toast({
165153
title: 'Success',
166154
description: 'Private AI key deleted successfully',
@@ -200,10 +188,33 @@ export default function PrivateAIKeysPage() {
200188
},
201189
});
202190

191+
const handleCreateKey = (data: {
192+
name: string
193+
region_id: number
194+
key_type: 'full' | 'llm' | 'vector'
195+
owner_id?: number
196+
team_id?: number
197+
}) => {
198+
createKeyMutation.mutate(data);
199+
};
200+
203201
return (
204202
<div className="space-y-4">
205203
<div className="flex justify-between items-center">
206204
<h1 className="text-3xl font-bold">Private AI Keys</h1>
205+
<CreateAIKeyDialog
206+
open={isCreateDialogOpen}
207+
onOpenChange={setIsCreateDialogOpen}
208+
onSubmit={handleCreateKey}
209+
isLoading={createKeyMutation.isPending}
210+
regions={regions}
211+
teamMembers={Object.values(usersMap)}
212+
showUserAssignment={true}
213+
currentUser={undefined}
214+
triggerText="Create Key"
215+
title="Create New Private AI Key"
216+
description="Create a new private AI key for any user or team."
217+
/>
207218
</div>
208219

209220
<div className="flex items-center gap-2">
@@ -280,7 +291,7 @@ export default function PrivateAIKeysPage() {
280291
}}
281292
isUpdatingBudget={updateBudgetPeriodMutation.isPending}
282293
teamDetails={teamDetails}
283-
teamMembers={Object.values(usersMap)}
294+
teamMembers={teamMembers}
284295
/>
285296
</div>
286297
);

0 commit comments

Comments
 (0)