376 lines
11 KiB
TypeScript
376 lines
11 KiB
TypeScript
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { DataTablePagination } from '@/components/ui/data-table-pagination';
|
|
import { DataTableRowActions } from '@/components/ui/data-table-row-actions';
|
|
import { DataTableViewOptions } from '@/components/ui/data-table-view-options';
|
|
import { QueryErrorView } from '@/components/ui/query-error-view';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table';
|
|
import {
|
|
type Credential3rdQueryDto,
|
|
DELETE_CREDENTIAL_3RD,
|
|
GET_CREDENTIAL_3RD,
|
|
} from '@/domains/recorder/schema/credential3rd';
|
|
import {
|
|
apolloErrorToMessage,
|
|
getApolloQueryError,
|
|
} from '@/infra/errors/apollo';
|
|
import type { GetCredential3rdQuery } from '@/infra/graphql/gql/graphql';
|
|
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
|
import { useDebouncedSkeleton } from '@/presentation/hooks/use-debounded-skeleton';
|
|
import { useEvent } from '@/presentation/hooks/use-event';
|
|
import { cn } from '@/presentation/utils';
|
|
import { useMutation, useQuery } from '@apollo/client';
|
|
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
|
import {
|
|
type ColumnDef,
|
|
type PaginationState,
|
|
type Row,
|
|
type SortingState,
|
|
type VisibilityState,
|
|
flexRender,
|
|
getCoreRowModel,
|
|
getPaginationRowModel,
|
|
useReactTable,
|
|
} from '@tanstack/react-table';
|
|
import { format } from 'date-fns';
|
|
import { Eye, EyeOff, Plus } from 'lucide-react';
|
|
import { useMemo, useState } from 'react';
|
|
import { toast } from 'sonner';
|
|
|
|
export const Route = createFileRoute('/_app/credential3rd/manage')({
|
|
component: CredentialManageRouteComponent,
|
|
staticData: {
|
|
breadcrumb: { label: 'Manage' },
|
|
} satisfies RouteStateDataOption,
|
|
});
|
|
|
|
function CredentialManageRouteComponent() {
|
|
const navigate = useNavigate();
|
|
|
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
|
|
createdAt: false,
|
|
updatedAt: false,
|
|
});
|
|
const [sorting, setSorting] = useState<SortingState>([]);
|
|
const [pagination, setPagination] = useState<PaginationState>({
|
|
pageIndex: 0,
|
|
pageSize: 10,
|
|
});
|
|
const [showPasswords, setShowPasswords] = useState<Record<number, boolean>>(
|
|
{}
|
|
);
|
|
|
|
const { loading, error, data, refetch } = useQuery<GetCredential3rdQuery>(
|
|
GET_CREDENTIAL_3RD,
|
|
{
|
|
variables: {
|
|
filters: {},
|
|
orderBy: {
|
|
createdAt: 'DESC',
|
|
},
|
|
pagination: {
|
|
page: {
|
|
page: pagination.pageIndex,
|
|
limit: pagination.pageSize,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
);
|
|
|
|
const [deleteCredential] = useMutation(DELETE_CREDENTIAL_3RD, {
|
|
onCompleted: async () => {
|
|
const refetchResult = await refetch();
|
|
const error = getApolloQueryError(refetchResult);
|
|
if (error) {
|
|
toast.error('Failed to delete credential', {
|
|
description: apolloErrorToMessage(error),
|
|
});
|
|
return;
|
|
}
|
|
toast.success('Credential deleted');
|
|
},
|
|
onError: (error) => {
|
|
toast.error('Failed to delete credential', {
|
|
description: error.message,
|
|
});
|
|
},
|
|
});
|
|
const { showSkeleton } = useDebouncedSkeleton({ loading });
|
|
|
|
const credentials = data?.credential3rd;
|
|
|
|
const handleDeleteRecord = useEvent(
|
|
(row: Row<Credential3rdQueryDto>) => async () => {
|
|
await deleteCredential({
|
|
variables: {
|
|
filters: {
|
|
id: { eq: row.original.id },
|
|
},
|
|
},
|
|
});
|
|
}
|
|
);
|
|
|
|
const togglePasswordVisibility = useEvent((id: number) => {
|
|
setShowPasswords((prev) => ({
|
|
...prev,
|
|
[id]: !prev[id],
|
|
}));
|
|
});
|
|
|
|
const columns = useMemo(() => {
|
|
const cs: ColumnDef<Credential3rdQueryDto>[] = [
|
|
{
|
|
header: 'ID',
|
|
accessorKey: 'id',
|
|
cell: ({ row }) => {
|
|
return <div className="font-mono text-sm">{row.original.id}</div>;
|
|
},
|
|
},
|
|
{
|
|
header: 'Credential Type',
|
|
accessorKey: 'credentialType',
|
|
cell: ({ row }) => {
|
|
const type = row.original.credentialType;
|
|
return (
|
|
<Badge variant="secondary" className="capitalize">
|
|
{type}
|
|
</Badge>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
header: 'Username',
|
|
accessorKey: 'username',
|
|
cell: ({ row }) => {
|
|
const username = row.original.username;
|
|
return (
|
|
<div className="whitespace-normal break-words">
|
|
{username || '-'}
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
header: 'Password',
|
|
accessorKey: 'password',
|
|
cell: ({ row }) => {
|
|
const password = row.original.password;
|
|
const isVisible = showPasswords[row.original.id];
|
|
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<div className="font-mono text-sm">
|
|
{isVisible ? password || '-' : '••••••••'}
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-6 w-6 p-0"
|
|
onClick={() => togglePasswordVisibility(row.original.id)}
|
|
>
|
|
{isVisible ? (
|
|
<EyeOff className="h-3 w-3" />
|
|
) : (
|
|
<Eye className="h-3 w-3" />
|
|
)}
|
|
</Button>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
header: 'User Agent',
|
|
accessorKey: 'userAgent',
|
|
cell: ({ row }) => {
|
|
const userAgent = row.original.userAgent;
|
|
return (
|
|
<div className="max-w-xs truncate" title={userAgent || undefined}>
|
|
{userAgent || '-'}
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
header: 'Created At',
|
|
accessorKey: 'createdAt',
|
|
cell: ({ row }) => {
|
|
const createdAt = row.original.createdAt;
|
|
return (
|
|
<div className="text-sm">
|
|
{format(new Date(createdAt), 'yyyy-MM-dd HH:mm:ss')}
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
header: 'Updated At',
|
|
accessorKey: 'updatedAt',
|
|
cell: ({ row }) => {
|
|
const updatedAt = row.original.updatedAt;
|
|
return (
|
|
<div className="text-sm">
|
|
{format(new Date(updatedAt), 'yyyy-MM-dd HH:mm:ss')}
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: 'actions',
|
|
cell: ({ row }) => (
|
|
<DataTableRowActions
|
|
row={row}
|
|
getId={(row) => row.original.id}
|
|
showEdit
|
|
showDelete
|
|
showDetail
|
|
onDetail={() => {
|
|
navigate({
|
|
to: '/credential3rd/detail/$id',
|
|
params: { id: `${row.original.id}` },
|
|
});
|
|
}}
|
|
onEdit={() => {
|
|
navigate({
|
|
to: '/credential3rd/edit/$id',
|
|
params: { id: `${row.original.id}` },
|
|
});
|
|
}}
|
|
onDelete={handleDeleteRecord(row)}
|
|
/>
|
|
),
|
|
},
|
|
];
|
|
return cs;
|
|
}, [handleDeleteRecord, navigate, showPasswords, togglePasswordVisibility]);
|
|
|
|
const table = useReactTable({
|
|
data: data?.credential3rd?.nodes ?? [],
|
|
columns,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
getPaginationRowModel: getPaginationRowModel(),
|
|
onPaginationChange: setPagination,
|
|
onSortingChange: setSorting,
|
|
onColumnVisibilityChange: setColumnVisibility,
|
|
pageCount: credentials?.paginationInfo?.pages,
|
|
rowCount: credentials?.paginationInfo?.total,
|
|
state: {
|
|
pagination,
|
|
sorting,
|
|
columnVisibility,
|
|
},
|
|
enableColumnPinning: true,
|
|
initialState: {
|
|
columnPinning: {
|
|
left: [],
|
|
right: ['actions'],
|
|
},
|
|
},
|
|
});
|
|
|
|
if (error) {
|
|
return <QueryErrorView message={error.message} onRetry={refetch} />;
|
|
}
|
|
|
|
return (
|
|
<div className="container mx-auto space-y-4 rounded-md">
|
|
<div className="flex items-center justify-between pt-4">
|
|
<div>
|
|
<h1 className="font-bold text-2xl">Credential 3rd Management</h1>
|
|
<p className="text-muted-foreground">
|
|
Manage your third-party platform login credentials
|
|
</p>
|
|
</div>
|
|
<Button onClick={() => navigate({ to: '/credential3rd/create' })}>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Add Credential
|
|
</Button>
|
|
</div>
|
|
<div className="flex items-center py-2">
|
|
<DataTableViewOptions table={table} />
|
|
</div>
|
|
<div className="rounded-md border">
|
|
<Table>
|
|
<TableHeader>
|
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
<TableRow key={headerGroup.id}>
|
|
{headerGroup.headers.map((header) => {
|
|
return (
|
|
<TableHead key={header.id}>
|
|
{header.isPlaceholder
|
|
? null
|
|
: flexRender(
|
|
header.column.columnDef.header,
|
|
header.getContext()
|
|
)}
|
|
</TableHead>
|
|
);
|
|
})}
|
|
</TableRow>
|
|
))}
|
|
</TableHeader>
|
|
<TableBody>
|
|
{showSkeleton &&
|
|
Array.from(new Array(pagination.pageSize)).map((_, index) => (
|
|
<TableRow key={index}>
|
|
{table.getVisibleLeafColumns().map((column) => (
|
|
<TableCell key={column.id}>
|
|
<Skeleton className="h-8" />
|
|
</TableCell>
|
|
))}
|
|
</TableRow>
|
|
))}
|
|
{!showSkeleton &&
|
|
(table.getRowModel().rows?.length ? (
|
|
table.getRowModel().rows.map((row) => (
|
|
<TableRow
|
|
key={row.id}
|
|
data-state={row.getIsSelected() && 'selected'}
|
|
>
|
|
{row.getVisibleCells().map((cell) => {
|
|
const isPinned = cell.column.getIsPinned();
|
|
return (
|
|
<TableCell
|
|
key={cell.id}
|
|
className={cn({
|
|
'sticky z-1 bg-background shadow-xs': isPinned,
|
|
'right-0': isPinned === 'right',
|
|
'left-0': isPinned === 'left',
|
|
})}
|
|
>
|
|
{flexRender(
|
|
cell.column.columnDef.cell,
|
|
cell.getContext()
|
|
)}
|
|
</TableCell>
|
|
);
|
|
})}
|
|
</TableRow>
|
|
))
|
|
) : (
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={columns.length}
|
|
className="h-24 text-center"
|
|
>
|
|
No Results
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
<DataTablePagination table={table} showSelectedRowCount={false} />
|
|
</div>
|
|
);
|
|
}
|