Browse Source

feat(dashboard): Job list improvements

Can cancel jobs, can set refresh interval, style improvements
Michael Bromley 5 months ago
parent
commit
d5c39cf444

+ 1 - 1
packages/dashboard/src/app/routes/_authenticated/_system/components/payload-dialog.tsx

@@ -26,7 +26,7 @@ export function PayloadDialog({ payload, trigger, title, description }: Readonly
                     <DialogDescription>{description}</DialogDescription>
                 </DialogHeader>
                 <ScrollArea className="max-h-[600px]">
-                    <JsonEditor viewOnly data={payload} collapse />
+                    <JsonEditor viewOnly data={payload} collapse={1} rootFontSize={12} />
                 </ScrollArea>
             </DialogContent>
         </Dialog>

+ 11 - 0
packages/dashboard/src/app/routes/_authenticated/_system/job-queue.graphql.ts

@@ -41,3 +41,14 @@ export const jobQueueListDocument = graphql(`
         }
     }
 `);
+
+export const cancelJobDocument = graphql(
+    `
+        mutation CancelJob($jobId: ID!) {
+            cancelJob(jobId: $jobId) {
+                ...JobInfo
+            }
+        }
+    `,
+    [jobInfoFragment],
+);

+ 99 - 5
packages/dashboard/src/app/routes/_authenticated/_system/job-queue.tsx

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

+ 9 - 0
packages/dashboard/src/lib/framework/page/list-page.tsx

@@ -7,6 +7,7 @@ import {
     ListQueryOptionsShape,
     ListQueryShape,
     PaginatedListDataTable,
+    PaginatedListRefresherRegisterFn,
     RowAction,
 } from '@/vdb/components/shared/paginated-list-data-table.js';
 import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
@@ -53,6 +54,12 @@ export interface ListPageProps<
     transformData?: (data: any[]) => any[];
     setTableOptions?: (table: TableOptions<any>) => TableOptions<any>;
     bulkActions?: BulkAction[];
+    /**
+     * Register a function that allows you to assign a refresh function for
+     * this list. The function can be assigned to a ref and then called when
+     * the list needs to be refreshed.
+     */
+    registerRefresher?: PaginatedListRefresherRegisterFn;
 }
 
 /**
@@ -90,6 +97,7 @@ export function ListPage<
     transformData,
     setTableOptions,
     bulkActions,
+    registerRefresher,
 }: Readonly<ListPageProps<T, U, V, AC>>) {
     const route = typeof routeOrFn === 'function' ? routeOrFn() : routeOrFn;
     const routeSearch = route.useSearch();
@@ -191,6 +199,7 @@ export function ListPage<
                         bulkActions={bulkActions}
                         setTableOptions={setTableOptions}
                         transformData={transformData}
+                        registerRefresher={registerRefresher}
                     />
                 </FullWidthPageBlock>
             </PageLayout>