270 lines
8.6 KiB
TypeScript
270 lines
8.6 KiB
TypeScript
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from '@/components/ui/card';
|
|
import { ContainerHeader } from '@/components/ui/container-header';
|
|
import { DetailCardSkeleton } from '@/components/ui/detail-card-skeleton';
|
|
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
|
import { Label } from '@/components/ui/label';
|
|
import { QueryErrorView } from '@/components/ui/query-error-view';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import { GET_TASKS, RETRY_TASKS } from '@/domains/recorder/schema/tasks';
|
|
import { getApolloQueryError } from '@/infra/errors/apollo';
|
|
import { apolloErrorToMessage } from '@/infra/errors/apollo';
|
|
import {
|
|
type GetTasksQuery,
|
|
type GetTasksQueryVariables,
|
|
type RetryTasksMutation,
|
|
type RetryTasksMutationVariables,
|
|
SubscriberTaskStatusEnum,
|
|
} from '@/infra/graphql/gql/graphql';
|
|
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
|
import { useMutation, useQuery } from '@apollo/client';
|
|
import { createFileRoute } from '@tanstack/react-router';
|
|
import { format } from 'date-fns';
|
|
import { RefreshCw } from 'lucide-react';
|
|
import { useMemo } from 'react';
|
|
import { toast } from 'sonner';
|
|
import { prettyTaskType } from './-pretty-task-type';
|
|
import { getStatusBadge } from './-status-badge';
|
|
|
|
export const Route = createFileRoute('/_app/tasks/detail/$id')({
|
|
component: TaskDetailRouteComponent,
|
|
staticData: {
|
|
breadcrumb: { label: 'Detail' },
|
|
} satisfies RouteStateDataOption,
|
|
});
|
|
|
|
function TaskDetailRouteComponent() {
|
|
const { id } = Route.useParams();
|
|
|
|
const { data, loading, error, refetch } = useQuery<
|
|
GetTasksQuery,
|
|
GetTasksQueryVariables
|
|
>(GET_TASKS, {
|
|
variables: {
|
|
filter: {
|
|
id: {
|
|
eq: id,
|
|
},
|
|
},
|
|
pagination: {
|
|
page: {
|
|
page: 0,
|
|
limit: 1,
|
|
},
|
|
},
|
|
orderBy: {},
|
|
},
|
|
pollInterval: 5000, // Auto-refresh every 5 seconds for running tasks
|
|
});
|
|
|
|
const task = data?.subscriberTasks?.nodes?.[0];
|
|
|
|
const [retryTasks] = useMutation<
|
|
RetryTasksMutation,
|
|
RetryTasksMutationVariables
|
|
>(RETRY_TASKS, {
|
|
onCompleted: async () => {
|
|
const refetchResult = await refetch();
|
|
const error = getApolloQueryError(refetchResult);
|
|
if (error) {
|
|
toast.error('Failed to retry task', {
|
|
description: apolloErrorToMessage(error),
|
|
});
|
|
return;
|
|
}
|
|
toast.success('Task retried successfully');
|
|
},
|
|
onError: (error) => {
|
|
toast.error('Failed to retry task', {
|
|
description: apolloErrorToMessage(error),
|
|
});
|
|
},
|
|
});
|
|
|
|
const job = useMemo(() => {
|
|
if (!task) {
|
|
return null;
|
|
}
|
|
return {
|
|
...task.job,
|
|
subscription: task.subscription,
|
|
};
|
|
}, [task]);
|
|
|
|
if (loading) {
|
|
return <DetailCardSkeleton />;
|
|
}
|
|
|
|
if (error) {
|
|
return <QueryErrorView message={error.message} onRetry={refetch} />;
|
|
}
|
|
|
|
if (!task) {
|
|
return <DetailEmptyView message="Task not found" />;
|
|
}
|
|
|
|
return (
|
|
<div className="container mx-auto max-w-4xl py-6">
|
|
<ContainerHeader
|
|
title="Task Detail"
|
|
description={`View task #${task.id}`}
|
|
defaultBackTo="/tasks/manage"
|
|
actions={
|
|
<Button variant="outline" size="sm" onClick={() => refetch()}>
|
|
<RefreshCw className="h-4 w-4" />
|
|
Refresh
|
|
</Button>
|
|
}
|
|
/>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<CardTitle>Task Information</CardTitle>
|
|
<CardDescription className="mt-2">
|
|
View task execution details
|
|
</CardDescription>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{getStatusBadge(task.status)}
|
|
{task.status ===
|
|
(SubscriberTaskStatusEnum.Killed ||
|
|
SubscriberTaskStatusEnum.Failed) && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() =>
|
|
retryTasks({
|
|
variables: { filter: { id: { eq: task.id } } },
|
|
})
|
|
}
|
|
>
|
|
Retry
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-6">
|
|
{/* Basic Information */}
|
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
|
<div className="space-y-2">
|
|
<Label className="font-medium text-sm">Task ID</Label>
|
|
<div className="rounded-md bg-muted p-3">
|
|
<code className="text-sm">{task.id}</code>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="font-medium text-sm">Task Type</Label>
|
|
<div className="rounded-md bg-muted p-3">
|
|
<Badge variant="secondary" className="capitalize">
|
|
{prettyTaskType(task.taskType)}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="font-medium text-sm">Priority</Label>
|
|
<div className="rounded-md bg-muted p-3">
|
|
<span className="text-sm">{task.priority}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="font-medium text-sm">Attempts</Label>
|
|
<div className="rounded-md bg-muted p-3">
|
|
<span className="text-sm">
|
|
{task.attempts} / {task.maxAttempts}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="font-medium text-sm">
|
|
Scheduled Run Time
|
|
</Label>
|
|
<div className="rounded-md bg-muted p-3">
|
|
<span className="text-sm">
|
|
{format(new Date(task.runAt), 'yyyy-MM-dd HH:mm:ss')}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="font-medium text-sm">Done Time</Label>
|
|
<div className="rounded-md bg-muted p-3">
|
|
<span className="text-sm">
|
|
{task.doneAt
|
|
? format(new Date(task.doneAt), 'yyyy-MM-dd HH:mm:ss')
|
|
: '-'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="font-medium text-sm">Lock Time</Label>
|
|
<div className="rounded-md bg-muted p-3">
|
|
<span className="text-sm">
|
|
{task.lockAt
|
|
? format(new Date(task.lockAt), 'yyyy-MM-dd HH:mm:ss')
|
|
: '-'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="font-medium text-sm">Lock By</Label>
|
|
<div className="rounded-md bg-muted p-3">
|
|
<code className="text-sm">{task.lockBy || '-'}</code>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Job Details */}
|
|
{job && (
|
|
<>
|
|
<Separator />
|
|
<div className="space-y-2">
|
|
<Label className="font-medium text-sm">Job Details</Label>
|
|
<div className="rounded-md bg-muted p-3">
|
|
<pre className="overflow-x-auto whitespace-pre-wrap text-sm">
|
|
<code>{JSON.stringify(job, null, 2)}</code>
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Error Information */}
|
|
{(task.status === SubscriberTaskStatusEnum.Failed ||
|
|
task.status === SubscriberTaskStatusEnum.Killed) &&
|
|
task.lastError && (
|
|
<>
|
|
<Separator />
|
|
<div className="space-y-2">
|
|
<Label className="font-medium text-sm">Last Error</Label>
|
|
<div className="rounded-md bg-destructive/10 p-3">
|
|
<p className="text-destructive text-sm">
|
|
{task.lastError}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|