Added accounts table page in Admin

This commit is contained in:
giancarlo
2024-04-08 16:32:22 +08:00
parent 45417fe2c5
commit 767e2f21b5
11 changed files with 1481 additions and 220 deletions

View File

@@ -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 { AdminGuard } from '@kit/admin/components/admin-guard';
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
import { PageBody, PageHeader } from '@kit/ui/page'; 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 ( return (
<> <>
<PageHeader title={'Accounts'} /> <PageHeader
<PageBody></PageBody>; 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); export default AdminGuard(AccountsPage);

View File

@@ -7,6 +7,7 @@ const INTERNAL_PACKAGES = [
'@kit/ui', '@kit/ui',
'@kit/auth', '@kit/auth',
'@kit/accounts', '@kit/accounts',
'@kit/admin',
'@kit/team-accounts', '@kit/team-accounts',
'@kit/shared', '@kit/shared',
'@kit/supabase', '@kit/supabase',

View File

@@ -3,6 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"sideEffects": false, "sideEffects": false,
"type": "module",
"scripts": { "scripts": {
"analyze": "ANALYZE=true pnpm run build", "analyze": "ANALYZE=true pnpm run build",
"build": "pnpm with-env next build", "build": "pnpm with-env next build",

View File

@@ -45,6 +45,7 @@
"skip": "Skip", "skip": "Skip",
"signedInAs": "Signed in as", "signedInAs": "Signed in as",
"pageOfPages": "Page {{page}} of {{total}}", "pageOfPages": "Page {{page}} of {{total}}",
"noData": "No data available",
"roles": { "roles": {
"owner": { "owner": {
"label": "Owner", "label": "Owner",

View File

@@ -32,9 +32,9 @@
], ],
"dependencies": { "dependencies": {
"@manypkg/cli": "^0.21.3", "@manypkg/cli": "^0.21.3",
"@turbo/gen": "^1.13.0", "@turbo/gen": "^1.13.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"pnpm": "^8.15.5", "pnpm": "^8.15.6",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"turbo": "^1.13.2", "turbo": "^1.13.2",
"yarn": "^1.22.22" "yarn": "^1.22.22"

View File

@@ -10,11 +10,17 @@
}, },
"prettier": "@kit/prettier-config", "prettier": "@kit/prettier-config",
"peerDependencies": { "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-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": { "devDependencies": {
"@hookform/resolvers": "^3.3.4",
"@kit/eslint-config": "workspace:*", "@kit/eslint-config": "workspace:*",
"@kit/prettier-config": "workspace:*", "@kit/prettier-config": "workspace:*",
"@kit/supabase": "workspace:^", "@kit/supabase": "workspace:^",
@@ -24,11 +30,13 @@
"@makerkit/data-loader-supabase-core": "0.0.5", "@makerkit/data-loader-supabase-core": "0.0.5",
"@makerkit/data-loader-supabase-nextjs": "^0.0.7", "@makerkit/data-loader-supabase-nextjs": "^0.0.7",
"@supabase/supabase-js": "^2.42.0", "@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": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
"./components/*": "./src/components/*" "./components/*": "./src/components/*.tsx"
}, },
"eslintConfig": { "eslintConfig": {
"root": true, "root": true,

View 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>
);
},
},
];
}

View File

@@ -120,7 +120,8 @@
"./loading-overlay": "./src/makerkit/loading-overlay.tsx", "./loading-overlay": "./src/makerkit/loading-overlay.tsx",
"./profile-avatar": "./src/makerkit/profile-avatar.tsx", "./profile-avatar": "./src/makerkit/profile-avatar.tsx",
"./mdx-components": "./src/makerkit/mdx-components.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": { "typesVersions": {
"*": { "*": {

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import { Fragment, useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -35,7 +35,6 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from '../shadcn/table'; } from '../shadcn/table';
import { cn } from '../utils';
import { Trans } from './trans'; import { Trans } from './trans';
interface ReactTableProps<T extends object> { interface ReactTableProps<T extends object> {
@@ -53,7 +52,6 @@ interface ReactTableProps<T extends object> {
export function DataTable<T extends object>({ export function DataTable<T extends object>({
data, data,
columns, columns,
renderSubComponent,
pageIndex, pageIndex,
pageSize, pageSize,
pageCount, pageCount,
@@ -117,9 +115,7 @@ export function DataTable<T extends object>({
}); });
return ( return (
<div <div className={'rounded-lg border'}>
className={'dark:border-dark-800 rounded-md border border-gray-50 p-1'}
>
<Table {...tableProps}> <Table {...tableProps}>
<TableHeader> <TableHeader>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
@@ -145,38 +141,29 @@ export function DataTable<T extends object>({
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{table.getRowModel().rows.map((row) => ( {table.getRowModel().rows?.length ? (
<Fragment key={row.id}> table.getRowModel().rows.map((row) => (
<TableRow <TableRow
className={cn({ key={row.id}
'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted': data-state={row.getIsSelected() && 'selected'}
row.getIsExpanded(),
})}
> >
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<TableCell <TableCell key={cell.id}>
style={{
width: cell.column.getSize(),
}}
key={cell.id}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())} {flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell> </TableCell>
))} ))}
</TableRow> </TableRow>
))
{renderSubComponent ? ( ) : (
<TableRow key={row.id + '-expanded'}> <TableRow>
<TableCell colSpan={columns.length}> <TableCell colSpan={columns.length} className="h-24 text-center">
{renderSubComponent({ row })} <Trans i18nKey={'common:noData'} />
</TableCell> </TableCell>
</TableRow> </TableRow>
) : null} )}
</Fragment>
))}
</TableBody> </TableBody>
<TableFooter> <TableFooter className={'bg-background'}>
<TableRow> <TableRow>
<TableCell colSpan={columns.length}> <TableCell colSpan={columns.length}>
<Pagination table={table} /> <Pagination table={table} />
@@ -194,39 +181,7 @@ function Pagination<T>({
table: ReactTable<T>; table: ReactTable<T>;
}>) { }>) {
return ( return (
<div className="flex w-full items-center gap-2"> <div className="flex items-center justify-end space-x-4">
<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>
<span className="flex items-center text-sm"> <span className="flex items-center text-sm">
<Trans <Trans
i18nKey={'common:pageOfPages'} i18nKey={'common:pageOfPages'}
@@ -236,6 +191,44 @@ function Pagination<T>({
}} }}
/> />
</span> </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> </div>
); );
} }

View File

@@ -7,6 +7,7 @@ import {
useReactTable, useReactTable,
} from '@tanstack/react-table'; } from '@tanstack/react-table';
import { Trans } from '../makerkit/trans';
import { import {
Table, Table,
TableBody, TableBody,
@@ -52,6 +53,7 @@ export function DataTable<TData, TValue>({
</TableRow> </TableRow>
))} ))}
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{table.getRowModel().rows?.length ? ( {table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => (
@@ -69,7 +71,7 @@ export function DataTable<TData, TValue>({
) : ( ) : (
<TableRow> <TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center"> <TableCell colSpan={columns.length} className="h-24 text-center">
No results. <Trans i18nKey={'common:noData'} />
</TableCell> </TableCell>
</TableRow> </TableRow>
)} )}

1313
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff