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 { 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);

View File

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

View File

@@ -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",

View File

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

View File

@@ -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"

View File

@@ -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,

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",
"./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": {
"*": {

View File

@@ -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>
);
}

View File

@@ -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

File diff suppressed because it is too large Load Diff