Skip to content

Commit

Permalink
add admin page for usage summary
Browse files Browse the repository at this point in the history
fixes useMemo warning

add error message to admin page
  • Loading branch information
bearice committed Oct 5, 2023
1 parent 00935ff commit d3735ff
Show file tree
Hide file tree
Showing 9 changed files with 330 additions and 1 deletion.
32 changes: 32 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"dependencies": {
"@tabler/icons-react": "^2.18.0",
"@tanstack/react-query": "^4.35.3",
"@tanstack/react-table": "^8.10.3",
"@trpc/client": "^10.38.1",
"@trpc/next": "^10.38.1",
"@trpc/react-query": "^10.38.1",
Expand Down
1 change: 1 addition & 0 deletions pages/admin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './api/admin';
219 changes: 219 additions & 0 deletions pages/api/admin/admin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import { useEffect, useMemo, useReducer, useState } from "react";
import Link from 'next/link'
import { trpc } from '@/utils/trpc';
import { UsageReport } from "@/types/admin";

import {
ColumnDef,
flexRender,
getCoreRowModel,
getSortedRowModel,
SortingState,
useReactTable,
} from '@tanstack/react-table'
import { useSession } from "next-auth/react";
import { UserRole } from "@/types/user";

function UsageReportTable({ start }: { start: Date }) {
const end = new Date(start.getFullYear(), start.getMonth() + 1);
const startTs = start.valueOf();
const endTs = end.valueOf();
const usageReportQuery = trpc.admin.usageReport.useQuery({ start: startTs, end: endTs });
const [data, setData] = useState<UsageReport[]>([]);
const [sorting, setSorting] = useState<SortingState>([])
const columns = useMemo<ColumnDef<UsageReport>[]>(
() => {
const USDollar = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
});
return [
{
header: 'User',
footer: props => props.column.id,
columns: [
{
accessorFn: row => row.user.name,
id: 'name',
cell: info => info.getValue(),
header: 'Name',
footer: props => props.column.id,
},
{
accessorFn: row => row.user.email,
id: 'email',
cell: info => info.getValue(),
header: 'Email',
footer: props => props.column.id,
},
],
},
{
header: 'Usage',
footer: props => props.column.id,
columns: [
{
accessorKey: 'totalPriceUSD',
cell: cell => USDollar.format(cell.getValue()),
header: 'Cost',
footer: props => props.column.id,
},
{
accessorKey: 'conversions',
header: 'Conversions',
footer: props => props.column.id,
},
{
accessorKey: 'tokens',
header: 'Tokens',
footer: props => props.column.id,
},
]
}
]
},
[]
)
const table = useReactTable({
data,
columns,
state: {
sorting,
},
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
// debugTable: true,
})

useEffect(() => {
// console.info(apiResponse);
setData(usageReportQuery.data || []);
}, [usageReportQuery.data]);

if (usageReportQuery.isLoading) {
return <div>Loading...</div>
}
if (usageReportQuery.isError) {
return <div>Error: {usageReportQuery.error.message}</div>
}

return (
<table className="w-full border-collapse border border-slate-500">
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => {
return (
<th className="border border-slate-600" key={header.id} colSpan={header.colSpan}>
{header.isPlaceholder ? null : (
<div
{...{
className: header.column.getCanSort()
? 'cursor-pointer select-none'
: '',
onClick: header.column.getToggleSortingHandler(),
}}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{{
asc: ' 🔼',
desc: ' 🔽',
}[header.column.getIsSorted() as string] ?? null}
</div>
)}
</th>
)
})}
</tr>
))}
</thead>
<tbody>
{table
.getRowModel()
.rows
.map(row => {
return (
<tr key={row.id}>
{row.getVisibleCells().map(cell => {
return (
<td className="border border-slate-700" key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</td>
)
})}
</tr>
)
})}
</tbody>
</table>
);
}

function AdminPage() {
const session = useSession()
const isAdminUser: boolean = session.data?.user?.role === UserRole.ADMIN;
const now = new Date();
const [startDate, setStartDate] = useState(new Date(now.getFullYear(), now.getMonth()));
const handlePreviousMonth = () => {
let currentMonth = startDate.getMonth();
let currentYear = startDate.getFullYear();
if (currentMonth > 0) {
currentMonth -= 1;
} else {
currentMonth = 11;
currentYear -= 1;
}
setStartDate(new Date(currentYear, currentMonth));
};

const handleNextMonth = () => {
let currentMonth = startDate.getMonth();
let currentYear = startDate.getFullYear();
if (currentMonth < 11) {
currentMonth += 1;

} else {
currentMonth = 0;
currentYear += 1;
}
setStartDate(new Date(currentYear, currentMonth));
};
if (!session.data?.user) {
return <h1 className="text-white">You need to <Link className="underline text-blue-600 hover:text-blue-800 visited:text-purple-600" href="/api/auth/signin?callbackUrl=%2Fadmin">log in</Link> to view this page.</h1>
}
if (!isAdminUser) {
return <h1 className="text-white">Admin permission required.</h1>
}
return (
<div className="container mx-auto text-white">
<div className="flex flex-row items-center justify-center">
<button className="flex w-1/4 cursor-pointer select-none items-center justify-start gap-3 rounded-md py-3 px-3 text-[14px] leading-3 text-white transition-colors duration-200 hover:bg-gray-500/10"
onClick={handlePreviousMonth}>
<svg className="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 8 14">
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 1 1.3 6.326a.91.91 0 0 0 0 1.348L7 13" />
</svg>
Previous Month
</button>
<h1 className="flex w-1/2 items-center justify-center">Monthly Cost Summary: {startDate.getFullYear()}/{startDate.getMonth() + 1}</h1> {/* Months are 0-indexed in JavaScript */}
<button className="flex w-1/4 cursor-pointer select-none items-center justify-end gap-3 rounded-md py-3 px-3 text-[14px] leading-3 text-white transition-colors duration-200 hover:bg-gray-500/10"
onClick={handleNextMonth}>
Next Month
<svg className="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 8 14">
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="m1 13 5.7-5.326a.909.909 0 0 0 0-1.348L1 1" />
</svg>
</button>
</div>
<div className="flex flex-row items-center justify-center">
<UsageReportTable start={startDate} />
</div>
</div>
);
}
export default AdminPage;
1 change: 1 addition & 0 deletions pages/api/admin/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './admin';
3 changes: 2 additions & 1 deletion server/routers/_app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import { models } from './models';
import { prompts } from './prompts';
import { publicPrompts } from './publicPrompts';
import { settings } from './settings';

import { admin } from './admin';
export const appRouter = router({
admin,
models,
settings,
prompts,
Expand Down
15 changes: 15 additions & 0 deletions server/routers/admin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { validateAdminAccess } from '../context';
import { procedure, router } from '../trpc';
import { z } from 'zod';
import { getLlmUsageReport } from '@/utils/server/admin';

export const admin = router({
usageReport: procedure
.input(z.object({ start: z.number(), end: z.number() }))
.query(async ({ ctx, input }) => {
await validateAdminAccess(ctx);
let start = new Date(input.start);
let end = new Date(input.end);
return getLlmUsageReport(start, end);
})
})
12 changes: 12 additions & 0 deletions types/admin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as z from 'zod';
import { UserSchema } from './user';
import { TokenUsageCountSchema } from './llmUsage';

export const UsageReportSchema = z.object({
user: UserSchema,
totalPriceUSD: z.number(),
conversions: z.number(),
tokens: z.number(),
});

export type UsageReport = z.infer<typeof UsageReportSchema>;
47 changes: 47 additions & 0 deletions utils/server/admin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { UsageReport } from "@/types/admin";
import { getDb } from "./storage";

export async function getLlmUsageReport(start: Date, end: Date): Promise<UsageReport[]> {
console.info("getLlmUsageReport", start, end)
const db = await getDb();
const result = await db.collection("userLlmUsage")
.aggregate<UsageReport>([
{
$match: {
date: {
$gte: start,
$lt: end,
}
}
},
{
$group: {
_id: "$userId",
totalPriceUSD: {
$sum: "$totalPriceUSD"
},
conversions: {
$count: {},
},
tokens: {
$sum: "$tokens.total",
},
}
},
{
$lookup: {
from: 'users', // join with users collection
localField: '_id',
foreignField: '_id',
as: 'user'
}
},
{
$unwind: {
path: "$user",
}
}
]).toArray();
console.info("getLlmUsageReport result", result.length)
return result;
}

0 comments on commit d3735ff

Please sign in to comment.