325 lines
10 KiB
TypeScript
325 lines
10 KiB
TypeScript
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { DataTablePagination } from '@/components/ui/data-table-pagination';
|
|
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
|
import { DropdownMenuActions } from '@/components/ui/dropdown-menu-actions';
|
|
import { QueryErrorView } from '@/components/ui/query-error-view';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import {
|
|
DELETE_TASKS,
|
|
GET_TASKS,
|
|
RETRY_TASKS,
|
|
type TaskDto,
|
|
} from '@/domains/recorder/schema/tasks';
|
|
import {
|
|
type DeleteTasksMutation,
|
|
type DeleteTasksMutationVariables,
|
|
type GetTasksQuery,
|
|
type RetryTasksMutation,
|
|
type RetryTasksMutationVariables,
|
|
SubscriberTaskStatusEnum,
|
|
} from '@/infra/graphql/gql/graphql';
|
|
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
|
import { useDebouncedSkeleton } from '@/presentation/hooks/use-debounded-skeleton';
|
|
import { useMutation, useQuery } from '@apollo/client';
|
|
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
|
import {
|
|
type ColumnDef,
|
|
type PaginationState,
|
|
type SortingState,
|
|
type VisibilityState,
|
|
getCoreRowModel,
|
|
getPaginationRowModel,
|
|
useReactTable,
|
|
} from '@tanstack/react-table';
|
|
import { format } from 'date-fns';
|
|
import { RefreshCw } from 'lucide-react';
|
|
|
|
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
|
import {
|
|
apolloErrorToMessage,
|
|
getApolloQueryError,
|
|
} from '@/infra/errors/apollo';
|
|
import { useMemo, useState } from 'react';
|
|
import { toast } from 'sonner';
|
|
import { getStatusBadge } from './-status-badge';
|
|
|
|
export const Route = createFileRoute('/_app/tasks/manage')({
|
|
component: TaskManageRouteComponent,
|
|
staticData: {
|
|
breadcrumb: { label: 'Manage' },
|
|
} satisfies RouteStateDataOption,
|
|
});
|
|
|
|
function TaskManageRouteComponent() {
|
|
const navigate = useNavigate();
|
|
|
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
|
|
lockAt: false,
|
|
lockBy: false,
|
|
attempts: false,
|
|
});
|
|
const [sorting, setSorting] = useState<SortingState>([]);
|
|
const [pagination, setPagination] = useState<PaginationState>({
|
|
pageIndex: 0,
|
|
pageSize: 10,
|
|
});
|
|
|
|
const { loading, error, data, refetch } = useQuery<GetTasksQuery>(GET_TASKS, {
|
|
variables: {
|
|
pagination: {
|
|
page: {
|
|
page: pagination.pageIndex,
|
|
limit: pagination.pageSize,
|
|
},
|
|
},
|
|
filters: {},
|
|
orderBy: {
|
|
runAt: 'DESC',
|
|
},
|
|
},
|
|
pollInterval: 5000, // Auto-refresh every 5 seconds
|
|
});
|
|
|
|
const { showSkeleton } = useDebouncedSkeleton({ loading });
|
|
|
|
const tasks = data?.subscriberTasks;
|
|
|
|
const [deleteTasks] = useMutation<
|
|
DeleteTasksMutation,
|
|
DeleteTasksMutationVariables
|
|
>(DELETE_TASKS, {
|
|
onCompleted: async () => {
|
|
const refetchResult = await refetch();
|
|
const error = getApolloQueryError(refetchResult);
|
|
if (error) {
|
|
toast.error('Failed to delete tasks', {
|
|
description: apolloErrorToMessage(error),
|
|
});
|
|
return;
|
|
}
|
|
toast.success('Tasks deleted');
|
|
},
|
|
onError: (error) => {
|
|
toast.error('Failed to delete tasks', {
|
|
description: error.message,
|
|
});
|
|
},
|
|
});
|
|
|
|
const [retryTasks] = useMutation<
|
|
RetryTasksMutation,
|
|
RetryTasksMutationVariables
|
|
>(RETRY_TASKS, {
|
|
onCompleted: () => {
|
|
toast.success('Tasks retried');
|
|
},
|
|
onError: (error) => {
|
|
toast.error('Failed to retry tasks', {
|
|
description: error.message,
|
|
});
|
|
},
|
|
});
|
|
|
|
const columns = useMemo(() => {
|
|
const cs: ColumnDef<TaskDto>[] = [
|
|
{
|
|
header: 'ID',
|
|
accessorKey: 'id',
|
|
cell: ({ row }) => {
|
|
return (
|
|
<div
|
|
className="max-w-[200px] truncate font-mono text-sm"
|
|
title={row.original.id}
|
|
>
|
|
{row.original.id}
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
];
|
|
return cs;
|
|
}, []);
|
|
|
|
const table = useReactTable({
|
|
data: useMemo(() => (tasks?.nodes ?? []) as TaskDto[], [tasks]),
|
|
columns,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
getPaginationRowModel: getPaginationRowModel(),
|
|
onPaginationChange: setPagination,
|
|
onSortingChange: setSorting,
|
|
onColumnVisibilityChange: setColumnVisibility,
|
|
pageCount: tasks?.paginationInfo?.pages,
|
|
rowCount: tasks?.paginationInfo?.total,
|
|
enableColumnPinning: true,
|
|
autoResetPageIndex: true,
|
|
manualPagination: true,
|
|
state: {
|
|
pagination,
|
|
sorting,
|
|
columnVisibility,
|
|
},
|
|
initialState: {
|
|
columnPinning: {
|
|
right: ['actions'],
|
|
},
|
|
},
|
|
});
|
|
|
|
if (error) {
|
|
return <QueryErrorView message={error.message} onRetry={refetch} />;
|
|
}
|
|
|
|
return (
|
|
<div className="container mx-auto space-y-4 px-4">
|
|
<div className="flex items-center justify-between pt-4">
|
|
<div>
|
|
<h1 className="font-bold text-2xl">Tasks Management</h1>
|
|
<p className="text-muted-foreground">Manage your tasks</p>
|
|
</div>
|
|
<Button onClick={() => refetch()} variant="outline" size="sm">
|
|
<RefreshCw className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{showSkeleton &&
|
|
Array.from(new Array(10)).map((_, index) => (
|
|
<Skeleton key={index} className="h-32 w-full" />
|
|
))}
|
|
|
|
{!showSkeleton && table.getRowModel().rows?.length > 0 ? (
|
|
table.getRowModel().rows.map((row, index) => {
|
|
const task = row.original;
|
|
return (
|
|
<div
|
|
className="space-y-3 rounded-lg border bg-card p-4"
|
|
key={`${task.id}-${index}`}
|
|
>
|
|
{/* Header with status and priority */}
|
|
<div className="flex items-center justify-between gap-2">
|
|
<div className="font-mono text-muted-foreground text-xs">
|
|
# {task.id}
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Badge variant="outline">{task.taskType}</Badge>
|
|
</div>
|
|
</div>
|
|
<div className="mt-1 flex items-center gap-2">
|
|
{getStatusBadge(task.status)}
|
|
<Badge variant="outline">Priority: {task.priority}</Badge>
|
|
<div className="mr-0 ml-auto">
|
|
<DropdownMenuActions
|
|
id={task.id}
|
|
showDetail
|
|
onDetail={() => {
|
|
navigate({
|
|
to: '/tasks/detail/$id',
|
|
params: { id: task.id },
|
|
});
|
|
}}
|
|
showDelete
|
|
onDelete={() =>
|
|
deleteTasks({
|
|
variables: {
|
|
filters: {
|
|
id: {
|
|
eq: task.id,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
>
|
|
{task.status ===
|
|
(SubscriberTaskStatusEnum.Killed ||
|
|
SubscriberTaskStatusEnum.Failed) && (
|
|
<DropdownMenuItem
|
|
onSelect={() =>
|
|
retryTasks({
|
|
variables: {
|
|
filters: {
|
|
id: {
|
|
eq: task.id,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
>
|
|
Retry
|
|
</DropdownMenuItem>
|
|
)}
|
|
</DropdownMenuActions>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Time info */}
|
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
|
<div>
|
|
<span className="text-muted-foreground">Run at: </span>
|
|
<span>{format(new Date(task.runAt), 'MM/dd HH:mm')}</span>
|
|
</div>
|
|
|
|
<div>
|
|
<span className="text-muted-foreground">Done: </span>
|
|
<span>
|
|
{task.doneAt
|
|
? format(new Date(task.doneAt), 'MM/dd HH:mm')
|
|
: '-'}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Attempts */}
|
|
<div className="text-sm">
|
|
<span className="text-muted-foreground">Attempts: </span>
|
|
<span>
|
|
{task.attempts} / {task.maxAttempts}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Lock at */}
|
|
<div className="text-sm">
|
|
<span className="text-muted-foreground">Lock at: </span>
|
|
<span>
|
|
{task.lockAt
|
|
? format(new Date(task.lockAt), 'MM/dd HH:mm')
|
|
: '-'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{task.job && (
|
|
<div className="text-sm">
|
|
<span className="text-muted-foreground">Job: </span>
|
|
<br />
|
|
<span
|
|
className="whitespace-pre-wrap"
|
|
dangerouslySetInnerHTML={{
|
|
__html: JSON.stringify(task.job, null, 2),
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Error if exists */}
|
|
{(task.status === SubscriberTaskStatusEnum.Failed ||
|
|
task.status === SubscriberTaskStatusEnum.Killed) &&
|
|
task.lastError && (
|
|
<div className="rounded bg-destructive/10 p-2 text-destructive text-sm">
|
|
{task.lastError}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})
|
|
) : (
|
|
<DetailEmptyView message="No tasks found" fullWidth />
|
|
)}
|
|
</div>
|
|
|
|
<DataTablePagination table={table} showSelectedRowCount={false} />
|
|
</div>
|
|
);
|
|
}
|