|
|
@@ -1,13 +1,32 @@
|
|
|
import { Badge } from '@/vdb/components/ui/badge.js';
|
|
|
import { Button } from '@/vdb/components/ui/button.js';
|
|
|
+import {
|
|
|
+ DropdownMenu,
|
|
|
+ DropdownMenuContent,
|
|
|
+ DropdownMenuItem,
|
|
|
+ DropdownMenuTrigger,
|
|
|
+} from '@/vdb/components/ui/dropdown-menu.js';
|
|
|
+import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
|
|
|
import { ListPage } from '@/vdb/framework/page/list-page.js';
|
|
|
import { api } from '@/vdb/graphql/api.js';
|
|
|
import { Trans } from '@/vdb/lib/trans.js';
|
|
|
+import { useMutation } from '@tanstack/react-query';
|
|
|
import { createFileRoute } from '@tanstack/react-router';
|
|
|
import { formatRelative } from 'date-fns';
|
|
|
-import { Ban, CheckCircle2Icon, CircleXIcon, ClockIcon, LoaderIcon, RotateCcw } from 'lucide-react';
|
|
|
+import {
|
|
|
+ Ban,
|
|
|
+ CheckCircle2Icon,
|
|
|
+ ChevronDown,
|
|
|
+ CircleXIcon,
|
|
|
+ ClockIcon,
|
|
|
+ LoaderIcon,
|
|
|
+ MoreVertical,
|
|
|
+ RefreshCw,
|
|
|
+ RotateCcw,
|
|
|
+} from 'lucide-react';
|
|
|
+import { useEffect, useRef, useState } from 'react';
|
|
|
import { PayloadDialog } from './components/payload-dialog.js';
|
|
|
-import { jobListDocument, jobQueueListDocument } from './job-queue.graphql.js';
|
|
|
+import { cancelJobDocument, jobListDocument, jobQueueListDocument } from './job-queue.graphql.js';
|
|
|
|
|
|
export const Route = createFileRoute('/_authenticated/_system/job-queue')({
|
|
|
component: JobQueuePage,
|
|
|
@@ -47,7 +66,30 @@ const STATES = [
|
|
|
},
|
|
|
];
|
|
|
|
|
|
+const REFRESH_INTERVALS = [
|
|
|
+ { label: <Trans>Off</Trans>, value: 0 },
|
|
|
+ { label: <Trans>Every 5 seconds</Trans>, value: 5000 },
|
|
|
+ { label: <Trans>Every 10 seconds</Trans>, value: 10000 },
|
|
|
+ { label: <Trans>Every 30 seconds</Trans>, value: 30000 },
|
|
|
+ { label: <Trans>Every 60 seconds</Trans>, value: 60000 },
|
|
|
+];
|
|
|
+
|
|
|
function JobQueuePage() {
|
|
|
+ const refreshRef = useRef<() => void>(() => {});
|
|
|
+ const [refreshInterval, setRefreshInterval] = useState(10000);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (refreshInterval === 0) return;
|
|
|
+
|
|
|
+ const interval = setInterval(() => {
|
|
|
+ refreshRef.current();
|
|
|
+ }, refreshInterval);
|
|
|
+
|
|
|
+ return () => clearInterval(interval);
|
|
|
+ }, [refreshInterval]);
|
|
|
+
|
|
|
+ const currentInterval = REFRESH_INTERVALS.find(i => i.value === refreshInterval);
|
|
|
+
|
|
|
return (
|
|
|
<ListPage
|
|
|
pageId="job-queue-list"
|
|
|
@@ -105,9 +147,14 @@ function JobQueuePage() {
|
|
|
},
|
|
|
state: {
|
|
|
header: 'State',
|
|
|
- cell: ({ row }) => {
|
|
|
+ cell: ({ row, table }) => {
|
|
|
+ const cancelJobMutation = useMutation({
|
|
|
+ mutationFn: (jobId: string) => api.mutate(cancelJobDocument, { jobId }),
|
|
|
+ onSuccess: () => {
|
|
|
+ refreshRef.current();
|
|
|
+ },
|
|
|
+ });
|
|
|
const state = STATES.find(s => s.value === row.original.state);
|
|
|
-
|
|
|
return (
|
|
|
<Badge
|
|
|
variant={
|
|
|
@@ -122,6 +169,27 @@ function JobQueuePage() {
|
|
|
>
|
|
|
{state && <state.icon />}
|
|
|
{row.original.state}
|
|
|
+ {row.original.state === 'RUNNING' ? (
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <DropdownMenu>
|
|
|
+ <DropdownMenuTrigger asChild>
|
|
|
+ <Button variant="ghost" size="sm" className="h-6 w-6 p-0">
|
|
|
+ <MoreVertical className="h-4 w-4" />
|
|
|
+ </Button>
|
|
|
+ </DropdownMenuTrigger>
|
|
|
+ <DropdownMenuContent align="end">
|
|
|
+ <DropdownMenuItem
|
|
|
+ onClick={() => cancelJobMutation.mutate(row.original.id)}
|
|
|
+ disabled={cancelJobMutation.isPending}
|
|
|
+ className="text-destructive focus:text-destructive"
|
|
|
+ >
|
|
|
+ <Ban className="mr-2 h-4 w-4" />
|
|
|
+ <Trans>Cancel Job</Trans>
|
|
|
+ </DropdownMenuItem>
|
|
|
+ </DropdownMenuContent>
|
|
|
+ </DropdownMenu>
|
|
|
+ </div>
|
|
|
+ ) : null}
|
|
|
</Badge>
|
|
|
);
|
|
|
},
|
|
|
@@ -159,6 +227,32 @@ function JobQueuePage() {
|
|
|
options: STATES,
|
|
|
},
|
|
|
}}
|
|
|
- ></ListPage>
|
|
|
+ registerRefresher={refresher => {
|
|
|
+ refreshRef.current = refresher;
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <PageActionBarRight>
|
|
|
+ <DropdownMenu>
|
|
|
+ <DropdownMenuTrigger asChild>
|
|
|
+ <Button variant="outline" size="sm" className="gap-2">
|
|
|
+ <RefreshCw className="h-4 w-4" />
|
|
|
+ <span>Auto refresh: {currentInterval?.label}</span>
|
|
|
+ <ChevronDown className="h-4 w-4" />
|
|
|
+ </Button>
|
|
|
+ </DropdownMenuTrigger>
|
|
|
+ <DropdownMenuContent align="end">
|
|
|
+ {REFRESH_INTERVALS.map(interval => (
|
|
|
+ <DropdownMenuItem
|
|
|
+ key={interval.value}
|
|
|
+ onClick={() => setRefreshInterval(interval.value)}
|
|
|
+ className={refreshInterval === interval.value ? 'bg-accent' : ''}
|
|
|
+ >
|
|
|
+ {interval.label}
|
|
|
+ </DropdownMenuItem>
|
|
|
+ ))}
|
|
|
+ </DropdownMenuContent>
|
|
|
+ </DropdownMenu>
|
|
|
+ </PageActionBarRight>
|
|
|
+ </ListPage>
|
|
|
);
|
|
|
}
|