Better DataTable loader (#34)

This commit is contained in:
Chris Estreich 2025-05-18 15:28:02 -07:00 committed by GitHub
parent 3aec71ea16
commit 9662d16669
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 188 additions and 173 deletions

View file

@ -1,14 +1,14 @@
import { useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import type { ColumnDef } from '@tanstack/react-table';
import { useAuth } from '@clerk/nextjs';
import { type DeveloperUsage, getDeveloperUsage } from '@/actions/analytics';
import { formatCurrency, formatNumber } from '@/lib/formatters';
import { Button } from '@/components/ui';
import { Button, Skeleton } from '@/components/ui';
import { DataTable } from '@/components/layout/DataTable';
import type { Filter } from './types';
import { Loader } from './Loader';
export const Developers = ({
onFilter,
@ -23,50 +23,60 @@ export const Developers = ({
enabled: !!orgId,
});
const columns: ColumnDef<DeveloperUsage>[] = [
{
header: 'Developer',
cell: ({ row }) => (
<Button
variant="link"
onClick={() =>
onFilter({
type: 'userId',
value: row.original.userId,
label: row.original.user.name,
})
}
className="px-0"
>
{row.original.user.name}
</Button>
),
},
{
accessorKey: 'user.email',
header: 'Email',
},
{
accessorKey: 'tasksStarted',
header: 'Tasks Started',
},
{
accessorKey: 'tasksCompleted',
header: 'Tasks Completed',
},
{
header: 'Tokens',
cell: ({ row }) => formatNumber(row.original.tokens),
},
{
header: 'Cost (USD)',
cell: ({ row }) => formatCurrency(row.original.cost),
},
];
const cols: ColumnDef<DeveloperUsage>[] = useMemo(
() => [
{
header: 'Developer',
cell: ({ row }) => (
<Button
variant="link"
onClick={() =>
onFilter({
type: 'userId',
value: row.original.userId,
label: row.original.user.name,
})
}
className="px-0"
>
{row.original.user.name}
</Button>
),
},
{
accessorKey: 'user.email',
header: 'Email',
},
{
accessorKey: 'tasksStarted',
header: 'Tasks Started',
},
{
accessorKey: 'tasksCompleted',
header: 'Tasks Completed',
},
{
header: 'Tokens',
cell: ({ row }) => formatNumber(row.original.tokens),
},
{
header: 'Cost (USD)',
cell: ({ row }) => formatCurrency(row.original.cost),
},
],
[onFilter],
);
if (isPending) {
return <Loader />;
}
const columns = useMemo(
() =>
isPending
? cols.map((col) => ({
...col,
cell: () => <Skeleton className="h-9 w-full" />,
}))
: cols,
[isPending, cols],
);
return <DataTable columns={columns} data={data} />;
};

View file

@ -1,14 +0,0 @@
import { LoaderCircle } from 'lucide-react';
import { Skeleton } from '@/components/ui';
export const Loader = () => (
<div className="flex flex-col bg-card border shadow rounded">
<div className="border-b p-2">
<Skeleton className="h-6 w-full" />
</div>
<div className="flex justify-center p-2">
<LoaderCircle className="size-5 animate-spin opacity-25" />
</div>
</div>
);

View file

@ -1,14 +1,14 @@
import { useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import type { ColumnDef } from '@tanstack/react-table';
import { useAuth } from '@clerk/nextjs';
import { type ModelUsage, getModelUsage } from '@/actions/analytics';
import { formatCurrency, formatNumber } from '@/lib/formatters';
import { Button } from '@/components/ui';
import { Button, Skeleton } from '@/components/ui';
import { DataTable } from '@/components/layout/DataTable';
import type { Filter } from './types';
import { Loader } from './Loader';
export const Models = ({
onFilter,
@ -23,46 +23,56 @@ export const Models = ({
enabled: !!orgId,
});
const columns: ColumnDef<ModelUsage>[] = [
{
header: 'Model',
cell: ({ row: { original: model } }) => (
<Button
variant="link"
onClick={() =>
onFilter({
type: 'model',
value: model.model,
label: model.model,
})
}
className="px-0"
>
{model.model}
</Button>
),
},
{
accessorKey: 'provider',
header: 'Provider',
},
{
accessorKey: 'tasks',
header: 'Tasks',
},
{
header: 'Tokens',
cell: ({ row }) => formatNumber(row.original.tokens),
},
{
header: 'Cost (USD)',
cell: ({ row }) => formatCurrency(row.original.cost),
},
];
const cols: ColumnDef<ModelUsage>[] = useMemo(
() => [
{
header: 'Model',
cell: ({ row: { original: model } }) => (
<Button
variant="link"
onClick={() =>
onFilter({
type: 'model',
value: model.model,
label: model.model,
})
}
className="px-0"
>
{model.model}
</Button>
),
},
{
accessorKey: 'provider',
header: 'Provider',
},
{
accessorKey: 'tasks',
header: 'Tasks',
},
{
header: 'Tokens',
cell: ({ row }) => formatNumber(row.original.tokens),
},
{
header: 'Cost (USD)',
cell: ({ row }) => formatCurrency(row.original.cost),
},
],
[onFilter],
);
if (isPending) {
return <Loader />;
}
const columns = useMemo(
() =>
isPending
? cols.map((col) => ({
...col,
cell: () => <Skeleton className="h-9 w-full" />,
}))
: cols,
[isPending, cols],
);
return <DataTable columns={columns} data={data} />;
};

View file

@ -5,11 +5,10 @@ import { useQuery } from '@tanstack/react-query';
import { type Task, getTasks } from '@/actions/analytics';
import { formatNumber, formatCurrency } from '@/lib/formatters';
import { Button } from '@/components/ui';
import { Button, Skeleton } from '@/components/ui';
import { DataTable } from '@/components/layout/DataTable';
import type { Filter } from './types';
import { Loader } from './Loader';
import { Status } from './Status';
export const Tasks = ({
@ -23,7 +22,7 @@ export const Tasks = ({
}) => {
const { orgId } = useAuth();
const { data = [], isPending } = useQuery({
const { data = Array(3).fill({}), isPending } = useQuery({
queryKey: ['getTasks', orgId],
queryFn: () => getTasks({ orgId }),
enabled: !!orgId,
@ -41,77 +40,87 @@ export const Tasks = ({
);
}, [filter, data]);
const columns: ColumnDef<Task>[] = [
{
header: 'Task ID',
cell: ({ row: { original: task } }) => (
<Button
variant="link"
onClick={() => onTaskSelected(task)}
className="px-0"
>
{task.taskId.slice(0, 8)}
</Button>
),
},
{
header: 'Developer',
cell: ({ row }) => (
<Button
variant="link"
onClick={() =>
onFilter({
type: 'userId',
value: row.original.userId,
label: row.original.user.name,
})
}
className="px-0"
>
{row.original.user.name}
</Button>
),
},
{
header: 'Model',
cell: ({ row }) => (
<Button
variant="link"
onClick={() =>
onFilter({
type: 'model',
value: row.original.model,
label: row.original.model,
})
}
className="px-0"
>
{row.original.model}
</Button>
),
},
{
header: 'Tokens',
cell: ({ row }) => formatNumber(row.original.tokens),
},
{
header: 'Cost (USD)',
cell: ({ row }) => formatCurrency(row.original.cost),
},
{
header: 'Date',
cell: ({ row }) =>
new Date(1000 * row.original.timestamp).toLocaleString(),
},
{
header: 'Status',
cell: ({ row }) => <Status completed={row.original.completed} />,
},
];
const cols: ColumnDef<Task>[] = useMemo(
() => [
{
header: 'Task ID',
cell: ({ row: { original: task } }) => (
<Button
variant="link"
onClick={() => onTaskSelected(task)}
className="px-0"
>
{task.taskId.slice(0, 8)}
</Button>
),
},
{
header: 'Developer',
cell: ({ row }) => (
<Button
variant="link"
onClick={() =>
onFilter({
type: 'userId',
value: row.original.userId,
label: row.original.user.name,
})
}
className="px-0"
>
{row.original.user.name}
</Button>
),
},
{
header: 'Model',
cell: ({ row }) => (
<Button
variant="link"
onClick={() =>
onFilter({
type: 'model',
value: row.original.model,
label: row.original.model,
})
}
className="px-0"
>
{row.original.model}
</Button>
),
},
{
header: 'Tokens',
cell: ({ row }) => formatNumber(row.original.tokens),
},
{
header: 'Cost (USD)',
cell: ({ row }) => formatCurrency(row.original.cost),
},
{
header: 'Date',
cell: ({ row }) =>
new Date(1000 * row.original.timestamp).toLocaleString(),
},
{
header: 'Status',
cell: ({ row }) => <Status completed={row.original.completed} />,
},
],
[onFilter, onTaskSelected],
);
if (isPending) {
return <Loader />;
}
const columns = useMemo(
() =>
isPending
? cols.map((col) => ({
...col,
cell: () => <Skeleton className="h-9 w-full" />,
}))
: cols,
[isPending, cols],
);
return <DataTable columns={columns} data={tasks} />;
};