Added accounts table page in Admin
This commit is contained in:
@@ -1,13 +1,70 @@
|
||||
import { ServerDataLoader } from '@makerkit/data-loader-supabase-nextjs';
|
||||
|
||||
import { AccountsTable } from '@kit/admin/components/accounts-table';
|
||||
import { AdminGuard } from '@kit/admin/components/admin-guard';
|
||||
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||
|
||||
function AccountsPage() {
|
||||
interface SearchParams {
|
||||
page?: string;
|
||||
account_type?: 'all' | 'team' | 'personal';
|
||||
}
|
||||
|
||||
function AccountsPage({ searchParams }: { searchParams: SearchParams }) {
|
||||
const client = getSupabaseServerComponentClient({
|
||||
admin: true,
|
||||
});
|
||||
|
||||
const page = searchParams.page ? parseInt(searchParams.page) : 1;
|
||||
const filters = getFilters(searchParams);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title={'Accounts'} />
|
||||
<PageBody></PageBody>;
|
||||
<PageHeader
|
||||
title={'Accounts'}
|
||||
description={`Manage your accounts, view their details, and more.`}
|
||||
/>
|
||||
|
||||
<PageBody>
|
||||
<ServerDataLoader
|
||||
table={'accounts'}
|
||||
client={client}
|
||||
page={page}
|
||||
where={filters}
|
||||
>
|
||||
{({ data, page, pageSize, pageCount }) => {
|
||||
return (
|
||||
<AccountsTable
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
pageCount={pageCount}
|
||||
data={data}
|
||||
filters={{
|
||||
type: searchParams.account_type ?? 'all',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</ServerDataLoader>
|
||||
</PageBody>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function getFilters(params: SearchParams) {
|
||||
const filters: {
|
||||
[key: string]: {
|
||||
eq: boolean;
|
||||
};
|
||||
} = {};
|
||||
|
||||
if (params.account_type && params.account_type !== 'all') {
|
||||
filters.is_personal_account = {
|
||||
eq: params.account_type === 'personal',
|
||||
};
|
||||
}
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
export default AdminGuard(AccountsPage);
|
||||
|
||||
@@ -7,6 +7,7 @@ const INTERNAL_PACKAGES = [
|
||||
'@kit/ui',
|
||||
'@kit/auth',
|
||||
'@kit/accounts',
|
||||
'@kit/admin',
|
||||
'@kit/team-accounts',
|
||||
'@kit/shared',
|
||||
'@kit/supabase',
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true pnpm run build",
|
||||
"build": "pnpm with-env next build",
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"skip": "Skip",
|
||||
"signedInAs": "Signed in as",
|
||||
"pageOfPages": "Page {{page}} of {{total}}",
|
||||
"noData": "No data available",
|
||||
"roles": {
|
||||
"owner": {
|
||||
"label": "Owner",
|
||||
|
||||
@@ -32,9 +32,9 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@manypkg/cli": "^0.21.3",
|
||||
"@turbo/gen": "^1.13.0",
|
||||
"@turbo/gen": "^1.13.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"pnpm": "^8.15.5",
|
||||
"pnpm": "^8.15.6",
|
||||
"prettier": "^3.2.5",
|
||||
"turbo": "^1.13.2",
|
||||
"yarn": "^1.22.22"
|
||||
|
||||
@@ -10,11 +10,17 @@
|
||||
},
|
||||
"prettier": "@kit/prettier-config",
|
||||
"peerDependencies": {
|
||||
"@kit/ui": "0.1.0",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@kit/supabase": "workspace:*",
|
||||
"@kit/ui": "workspace:*",
|
||||
"@makerkit/data-loader-supabase-core": "0.0.5",
|
||||
"@makerkit/data-loader-supabase-nextjs": "^0.0.7"
|
||||
"@makerkit/data-loader-supabase-nextjs": "^0.0.7",
|
||||
"lucide-react": "^0.363.0",
|
||||
"react-hook-form": "^7.51.2",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@kit/eslint-config": "workspace:*",
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
"@kit/supabase": "workspace:^",
|
||||
@@ -24,11 +30,13 @@
|
||||
"@makerkit/data-loader-supabase-core": "0.0.5",
|
||||
"@makerkit/data-loader-supabase-nextjs": "^0.0.7",
|
||||
"@supabase/supabase-js": "^2.42.0",
|
||||
"lucide-react": "^0.363.0"
|
||||
"lucide-react": "^0.363.0",
|
||||
"react-hook-form": "^7.51.2",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./components/*": "./src/components/*"
|
||||
"./components/*": "./src/components/*.tsx"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
|
||||
178
packages/features/admin/src/components/accounts-table.tsx
Normal file
178
packages/features/admin/src/components/accounts-table.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { EllipsisVertical } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from '@kit/ui/dropdown-menu';
|
||||
import { DataTable } from '@kit/ui/enhanced-data-table';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from '@kit/ui/select';
|
||||
|
||||
type Account = Database['public']['Tables']['accounts']['Row'];
|
||||
|
||||
const FiltersSchema = z.object({
|
||||
type: z.enum(['all', 'team', 'personal']),
|
||||
});
|
||||
|
||||
export function AccountsTable(
|
||||
props: React.PropsWithChildren<{
|
||||
data: Account[];
|
||||
pageCount: number;
|
||||
pageSize: number;
|
||||
page: number;
|
||||
filters: {
|
||||
type: 'all' | 'team' | 'personal';
|
||||
};
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<AccountsTableFilters filters={props.filters} />
|
||||
|
||||
<DataTable
|
||||
pageSize={props.pageSize}
|
||||
pageIndex={props.page - 1}
|
||||
pageCount={props.pageCount}
|
||||
data={props.data}
|
||||
columns={getColumns()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AccountsTableFilters(props: {
|
||||
filters: z.infer<typeof FiltersSchema>;
|
||||
}) {
|
||||
const form = useForm({
|
||||
resolver: zodResolver(FiltersSchema),
|
||||
defaultValues: {
|
||||
type: props.filters?.type ?? 'all',
|
||||
},
|
||||
mode: 'onChange',
|
||||
reValidateMode: 'onChange',
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const pathName = usePathname();
|
||||
|
||||
const onSubmit = ({ type }: z.infer<typeof FiltersSchema>) => {
|
||||
const params = new URLSearchParams({
|
||||
account_type: type,
|
||||
});
|
||||
|
||||
const url = `${pathName}?${params.toString()}`;
|
||||
|
||||
router.push(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={'flex space-x-4'}>
|
||||
<form onSubmit={form.handleSubmit((data) => onSubmit(data))}>
|
||||
<Select
|
||||
value={form.watch('type')}
|
||||
onValueChange={(value) => {
|
||||
form.setValue(
|
||||
'type',
|
||||
value as z.infer<typeof FiltersSchema>['type'],
|
||||
{
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
},
|
||||
);
|
||||
|
||||
return onSubmit(form.getValues());
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<span>Account Type</span>
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value={'all'}>All</SelectItem>
|
||||
<SelectItem value={'team'}>Team</SelectItem>
|
||||
<SelectItem value={'personal'}>Personal</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getColumns(): ColumnDef<Account>[] {
|
||||
return [
|
||||
{
|
||||
id: 'name',
|
||||
header: 'Name',
|
||||
accessorKey: 'name',
|
||||
},
|
||||
{
|
||||
id: 'email',
|
||||
header: 'Email',
|
||||
accessorKey: 'email',
|
||||
},
|
||||
{
|
||||
id: 'type',
|
||||
header: 'Type',
|
||||
cell: ({ row }) => {
|
||||
return row.original.is_personal_account ? 'Personal' : 'Team';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'created_at',
|
||||
header: 'Created At',
|
||||
accessorKey: 'created_at',
|
||||
},
|
||||
{
|
||||
id: 'updated_at',
|
||||
header: 'Updated At',
|
||||
accessorKey: 'updated_at',
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant={'ghost'}>
|
||||
<EllipsisVertical className={'h-4'} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align={'end'}>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
|
||||
<DropdownMenuItem>
|
||||
<Link href={`/accounts/${row.original.id}`}>View</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -120,7 +120,8 @@
|
||||
"./loading-overlay": "./src/makerkit/loading-overlay.tsx",
|
||||
"./profile-avatar": "./src/makerkit/profile-avatar.tsx",
|
||||
"./mdx-components": "./src/makerkit/mdx-components.tsx",
|
||||
"./mode-toggle": "./src/makerkit/mode-toggle.tsx"
|
||||
"./mode-toggle": "./src/makerkit/mode-toggle.tsx",
|
||||
"./enhanced-data-table": "./src/makerkit/data-table.tsx"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Fragment, useCallback, useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '../shadcn/table';
|
||||
import { cn } from '../utils';
|
||||
import { Trans } from './trans';
|
||||
|
||||
interface ReactTableProps<T extends object> {
|
||||
@@ -53,7 +52,6 @@ interface ReactTableProps<T extends object> {
|
||||
export function DataTable<T extends object>({
|
||||
data,
|
||||
columns,
|
||||
renderSubComponent,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
pageCount,
|
||||
@@ -117,9 +115,7 @@ export function DataTable<T extends object>({
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={'dark:border-dark-800 rounded-md border border-gray-50 p-1'}
|
||||
>
|
||||
<div className={'rounded-lg border'}>
|
||||
<Table {...tableProps}>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
@@ -145,38 +141,29 @@ export function DataTable<T extends object>({
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<Fragment key={row.id}>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
className={cn({
|
||||
'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted':
|
||||
row.getIsExpanded(),
|
||||
})}
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && 'selected'}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
style={{
|
||||
width: cell.column.getSize(),
|
||||
}}
|
||||
key={cell.id}
|
||||
>
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
|
||||
{renderSubComponent ? (
|
||||
<TableRow key={row.id + '-expanded'}>
|
||||
<TableCell colSpan={columns.length}>
|
||||
{renderSubComponent({ row })}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
</Fragment>
|
||||
))}
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
<Trans i18nKey={'common:noData'} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
|
||||
<TableFooter>
|
||||
<TableFooter className={'bg-background'}>
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length}>
|
||||
<Pagination table={table} />
|
||||
@@ -194,39 +181,7 @@ function Pagination<T>({
|
||||
table: ReactTable<T>;
|
||||
}>) {
|
||||
return (
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<Button
|
||||
size={'icon'}
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<ChevronsLeft className={'h-4'} />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size={'icon'}
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<ChevronLeft className={'h-4'} />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size={'icon'}
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<ChevronRight className={'h-4'} />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size={'icon'}
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<ChevronsRight className={'h-4'} />
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center justify-end space-x-4">
|
||||
<span className="flex items-center text-sm">
|
||||
<Trans
|
||||
i18nKey={'common:pageOfPages'}
|
||||
@@ -236,6 +191,44 @@ function Pagination<T>({
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<div className="flex items-center justify-end space-x-1">
|
||||
<Button
|
||||
size={'icon'}
|
||||
variant={'ghost'}
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<ChevronsLeft className={'h-4'} />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size={'icon'}
|
||||
variant={'ghost'}
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<ChevronLeft className={'h-4'} />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size={'icon'}
|
||||
variant={'ghost'}
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<ChevronRight className={'h-4'} />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size={'icon'}
|
||||
variant={'ghost'}
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<ChevronsRight className={'h-4'} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
|
||||
import { Trans } from '../makerkit/trans';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -52,6 +53,7 @@ export function DataTable<TData, TValue>({
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
@@ -69,7 +71,7 @@ export function DataTable<TData, TValue>({
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
<Trans i18nKey={'common:noData'} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
1313
pnpm-lock.yaml
generated
1313
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user