konobangu/apps/webui/src/presentation/routes/_app/tasks/detail.$id.tsx

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