|
@@ -1,313 +1,181 @@
|
|
|
"use client";
|
|
"use client";
|
|
|
|
|
|
|
|
-import { useState, useEffect, useCallback } from "react";
|
|
|
|
|
|
|
+import { useState, useEffect, useRef, useCallback } from "react";
|
|
|
import { Header } from "@/components/layout";
|
|
import { Header } from "@/components/layout";
|
|
|
import { AppLayout } from "@/components/layout";
|
|
import { AppLayout } from "@/components/layout";
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { Button } from "@/components/ui/button";
|
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
|
-import { Badge } from "@/components/ui/badge";
|
|
|
|
|
-import { Progress } from "@/components/ui/progress";
|
|
|
|
|
-import { apiClient, type JobInfo, type QueueStatus } from "@/lib/api";
|
|
|
|
|
|
|
+import { Loader2, RefreshCw, AlertCircle } from "lucide-react";
|
|
|
|
|
+import { EnhancedQueueList } from "@/components/features/queue/enhanced-queue-list";
|
|
|
import {
|
|
import {
|
|
|
- Loader2,
|
|
|
|
|
- RefreshCw,
|
|
|
|
|
- X,
|
|
|
|
|
- Clock,
|
|
|
|
|
- CheckCircle,
|
|
|
|
|
- XCircle,
|
|
|
|
|
- AlertCircle,
|
|
|
|
|
- Play,
|
|
|
|
|
- Pause,
|
|
|
|
|
-} from "lucide-react";
|
|
|
|
|
|
|
+ apiClient,
|
|
|
|
|
+ type QueueStatus,
|
|
|
|
|
+ type JobInfo,
|
|
|
|
|
+} from "@/lib/api";
|
|
|
|
|
|
|
|
-function QueuePage() {
|
|
|
|
|
|
|
+export default function QueuePage() {
|
|
|
const [queueStatus, setQueueStatus] = useState<QueueStatus | null>(null);
|
|
const [queueStatus, setQueueStatus] = useState<QueueStatus | null>(null);
|
|
|
const [loading, setLoading] = useState(true);
|
|
const [loading, setLoading] = useState(true);
|
|
|
- const [isInitialLoad, setIsInitialLoad] = useState(true);
|
|
|
|
|
|
|
+ const [actionLoading, setActionLoading] = useState(false);
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
+ const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
|
+ const isMountedRef = useRef(true);
|
|
|
|
|
|
|
|
- const loadQueueStatus = useCallback(async () => {
|
|
|
|
|
|
|
+ // Fetch queue status from the API
|
|
|
|
|
+ const fetchQueueStatus = useCallback(async () => {
|
|
|
|
|
+ if (!isMountedRef.current) return;
|
|
|
|
|
+
|
|
|
try {
|
|
try {
|
|
|
- setLoading(true);
|
|
|
|
|
- setError(null);
|
|
|
|
|
const status = await apiClient.getQueueStatus();
|
|
const status = await apiClient.getQueueStatus();
|
|
|
- setQueueStatus(status);
|
|
|
|
|
|
|
+ if (isMountedRef.current) {
|
|
|
|
|
+ setQueueStatus(status);
|
|
|
|
|
+ setError(null);
|
|
|
|
|
+ }
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
- console.error("Failed to load queue status:", err);
|
|
|
|
|
- setError(err instanceof Error ? err.message : "Failed to load queue status");
|
|
|
|
|
|
|
+ if (isMountedRef.current) {
|
|
|
|
|
+ const errorMessage = err instanceof Error ? err.message : "Failed to fetch queue status";
|
|
|
|
|
+ setError(errorMessage);
|
|
|
|
|
+ console.error("Error fetching queue status:", err);
|
|
|
|
|
+ }
|
|
|
} finally {
|
|
} finally {
|
|
|
- setLoading(false);
|
|
|
|
|
- setIsInitialLoad(false);
|
|
|
|
|
|
|
+ if (isMountedRef.current) {
|
|
|
|
|
+ setLoading(false);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}, []);
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
+ // Set up auto-refresh with proper cleanup
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
- loadQueueStatus();
|
|
|
|
|
-
|
|
|
|
|
- // Set up polling for active jobs
|
|
|
|
|
- const interval = setInterval(() => {
|
|
|
|
|
- loadQueueStatus();
|
|
|
|
|
- }, 2000); // Poll every 2 seconds
|
|
|
|
|
|
|
+ // Initial fetch
|
|
|
|
|
+ fetchQueueStatus();
|
|
|
|
|
+
|
|
|
|
|
+ // Set up interval for auto-refresh (every 3 seconds)
|
|
|
|
|
+ intervalRef.current = setInterval(() => {
|
|
|
|
|
+ fetchQueueStatus();
|
|
|
|
|
+ }, 3000);
|
|
|
|
|
+
|
|
|
|
|
+ // Cleanup function
|
|
|
|
|
+ return () => {
|
|
|
|
|
+ isMountedRef.current = false;
|
|
|
|
|
+ if (intervalRef.current) {
|
|
|
|
|
+ clearInterval(intervalRef.current);
|
|
|
|
|
+ intervalRef.current = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ }, [fetchQueueStatus]);
|
|
|
|
|
|
|
|
- return () => clearInterval(interval);
|
|
|
|
|
- }, [loadQueueStatus]);
|
|
|
|
|
|
|
+ // Handle manual refresh
|
|
|
|
|
+ const handleRefresh = useCallback(() => {
|
|
|
|
|
+ setLoading(true);
|
|
|
|
|
+ fetchQueueStatus();
|
|
|
|
|
+ }, [fetchQueueStatus]);
|
|
|
|
|
|
|
|
- const handleCancelJob = async (jobId: string) => {
|
|
|
|
|
|
|
+ // Handle job cancellation
|
|
|
|
|
+ const handleCancelJob = useCallback(async (jobId: string) => {
|
|
|
|
|
+ setActionLoading(true);
|
|
|
try {
|
|
try {
|
|
|
await apiClient.cancelJob(jobId);
|
|
await apiClient.cancelJob(jobId);
|
|
|
|
|
+ console.log(`Job ${jobId} cancelled successfully`);
|
|
|
// Refresh queue status after cancellation
|
|
// Refresh queue status after cancellation
|
|
|
- loadQueueStatus();
|
|
|
|
|
|
|
+ fetchQueueStatus();
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
- console.error("Failed to cancel job:", err);
|
|
|
|
|
- setError(err instanceof Error ? err.message : "Failed to cancel job");
|
|
|
|
|
|
|
+ const errorMessage = err instanceof Error ? err.message : "Failed to cancel job";
|
|
|
|
|
+ console.error(`Failed to cancel job: ${errorMessage}`, err);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setActionLoading(false);
|
|
|
}
|
|
}
|
|
|
- };
|
|
|
|
|
|
|
+ }, [fetchQueueStatus]);
|
|
|
|
|
|
|
|
- const handleClearQueue = async () => {
|
|
|
|
|
- if (!confirm("Are you sure you want to clear the entire queue? This will remove all jobs.")) {
|
|
|
|
|
|
|
+ // Handle queue clearing
|
|
|
|
|
+ const handleClearQueue = useCallback(async () => {
|
|
|
|
|
+ if (!queueStatus?.jobs.length) return;
|
|
|
|
|
+
|
|
|
|
|
+ if (!confirm("Are you sure you want to clear all jobs from the queue? This action cannot be undone.")) {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ setActionLoading(true);
|
|
|
try {
|
|
try {
|
|
|
await apiClient.clearQueue();
|
|
await apiClient.clearQueue();
|
|
|
- loadQueueStatus();
|
|
|
|
|
|
|
+ console.log("Queue cleared successfully");
|
|
|
|
|
+ // Refresh queue status after clearing
|
|
|
|
|
+ fetchQueueStatus();
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
- console.error("Failed to clear queue:", err);
|
|
|
|
|
- setError(err instanceof Error ? err.message : "Failed to clear queue");
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const getStatusIcon = (status: string) => {
|
|
|
|
|
- switch (status) {
|
|
|
|
|
- case "completed":
|
|
|
|
|
- return <CheckCircle className="h-4 w-4 text-green-500" />;
|
|
|
|
|
- case "failed":
|
|
|
|
|
- return <XCircle className="h-4 w-4 text-red-500" />;
|
|
|
|
|
- case "cancelled":
|
|
|
|
|
- return <X className="h-4 w-4 text-gray-500" />;
|
|
|
|
|
- case "processing":
|
|
|
|
|
- return <Play className="h-4 w-4 text-blue-500" />;
|
|
|
|
|
- case "queued":
|
|
|
|
|
- case "pending":
|
|
|
|
|
- return <Clock className="h-4 w-4 text-yellow-500" />;
|
|
|
|
|
- default:
|
|
|
|
|
- return <AlertCircle className="h-4 w-4 text-gray-500" />;
|
|
|
|
|
|
|
+ const errorMessage = err instanceof Error ? err.message : "Failed to clear queue";
|
|
|
|
|
+ console.error(`Failed to clear queue: ${errorMessage}`, err);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setActionLoading(false);
|
|
|
}
|
|
}
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const getStatusBadge = (status: string) => {
|
|
|
|
|
- const variants: Record<string, "default" | "secondary" | "destructive" | "outline"> = {
|
|
|
|
|
- completed: "default",
|
|
|
|
|
- failed: "destructive",
|
|
|
|
|
- cancelled: "secondary",
|
|
|
|
|
- processing: "default",
|
|
|
|
|
- queued: "secondary",
|
|
|
|
|
- pending: "secondary",
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- return (
|
|
|
|
|
- <Badge variant={variants[status] || "secondary"} className="capitalize">
|
|
|
|
|
- {status}
|
|
|
|
|
- </Badge>
|
|
|
|
|
- );
|
|
|
|
|
- };
|
|
|
|
|
|
|
+ }, [queueStatus?.jobs.length, fetchQueueStatus]);
|
|
|
|
|
|
|
|
- const getProgressValue = (job: JobInfo): number => {
|
|
|
|
|
- // For completed jobs, ensure progress is 100%
|
|
|
|
|
- if (job.status === "completed") {
|
|
|
|
|
- return 100;
|
|
|
|
|
- }
|
|
|
|
|
- // For failed jobs, show 0% progress
|
|
|
|
|
- if (job.status === "failed" || job.status === "cancelled") {
|
|
|
|
|
- return 0;
|
|
|
|
|
|
|
+ // Handle copying job parameters
|
|
|
|
|
+ const handleCopyParameters = useCallback((job: JobInfo) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const params = {
|
|
|
|
|
+ prompt: job.prompt || "",
|
|
|
|
|
+ negative_prompt: job.message || "",
|
|
|
|
|
+ // Add other relevant parameters as needed
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const paramsText = JSON.stringify(params, null, 2);
|
|
|
|
|
+ navigator.clipboard.writeText(paramsText);
|
|
|
|
|
+ console.log("Job parameters copied to clipboard");
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error("Failed to copy parameters:", err);
|
|
|
}
|
|
}
|
|
|
- // For other statuses, use the progress value from the API
|
|
|
|
|
- return job.progress || 0;
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const formatDate = (dateString?: string) => {
|
|
|
|
|
- if (!dateString) return "N/A";
|
|
|
|
|
- const date = new Date(dateString);
|
|
|
|
|
- const year = date.getFullYear();
|
|
|
|
|
- const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
|
|
|
- const day = String(date.getDate()).padStart(2, '0');
|
|
|
|
|
- const hours = String(date.getHours()).padStart(2, '0');
|
|
|
|
|
- const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
|
|
|
- const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
|
|
|
- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- if (loading && isInitialLoad) {
|
|
|
|
|
- return (
|
|
|
|
|
- <AppLayout>
|
|
|
|
|
- <Header title="Queue" description="Monitor and manage generation jobs" />
|
|
|
|
|
- <div className="container mx-auto p-6">
|
|
|
|
|
- <div className="flex items-center justify-center h-96">
|
|
|
|
|
- <div className="flex items-center gap-2">
|
|
|
|
|
- <Loader2 className="h-6 w-6 animate-spin" />
|
|
|
|
|
- <span>Loading queue...</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </AppLayout>
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (error) {
|
|
|
|
|
- return (
|
|
|
|
|
- <AppLayout>
|
|
|
|
|
- <Header title="Queue" description="Monitor and manage generation jobs" />
|
|
|
|
|
- <div className="container mx-auto p-6">
|
|
|
|
|
- <div className="flex flex-col items-center justify-center h-96 gap-4">
|
|
|
|
|
- <div className="text-destructive text-center">
|
|
|
|
|
- <p className="text-lg font-medium">Error loading queue</p>
|
|
|
|
|
- <p className="text-sm">{error}</p>
|
|
|
|
|
- </div>
|
|
|
|
|
- <Button onClick={loadQueueStatus} variant="outline">
|
|
|
|
|
- <RefreshCw className="h-4 w-4 mr-2" />
|
|
|
|
|
- Try Again
|
|
|
|
|
- </Button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </AppLayout>
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ }, []);
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
<AppLayout>
|
|
<AppLayout>
|
|
|
- <Header title="Queue" description="Monitor and manage generation jobs" />
|
|
|
|
|
|
|
+ <Header
|
|
|
|
|
+ title="Queue Management"
|
|
|
|
|
+ description="Monitor and manage generation jobs in the queue"
|
|
|
|
|
+ />
|
|
|
<div className="container mx-auto p-6">
|
|
<div className="container mx-auto p-6">
|
|
|
- {/* Queue Controls */}
|
|
|
|
|
- <div className="flex items-center justify-between mb-6">
|
|
|
|
|
- <div className="flex items-center gap-4">
|
|
|
|
|
- <h2 className="text-2xl font-bold">
|
|
|
|
|
- {queueStatus?.size || 0} Job{(queueStatus?.size || 0) !== 1 ? "s" : ""}
|
|
|
|
|
- </h2>
|
|
|
|
|
- {queueStatus?.active_generations ? (
|
|
|
|
|
- <Badge variant="secondary">
|
|
|
|
|
- {queueStatus.active_generations} Active
|
|
|
|
|
- </Badge>
|
|
|
|
|
- ) : null}
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div className="flex items-center gap-2">
|
|
|
|
|
- <Button onClick={loadQueueStatus} variant="outline" size="sm">
|
|
|
|
|
- <RefreshCw className="h-4 w-4 mr-2" />
|
|
|
|
|
- Refresh
|
|
|
|
|
- </Button>
|
|
|
|
|
- {queueStatus && queueStatus.size > 0 && (
|
|
|
|
|
- <Button onClick={handleClearQueue} variant="destructive" size="sm">
|
|
|
|
|
- Clear Queue
|
|
|
|
|
- </Button>
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ {error && (
|
|
|
|
|
+ <Card className="mb-6 border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950/20">
|
|
|
|
|
+ <CardContent className="pt-6">
|
|
|
|
|
+ <div className="flex items-center gap-3 text-red-700 dark:text-red-300">
|
|
|
|
|
+ <AlertCircle className="h-5 w-5" />
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <h3 className="font-medium">Error loading queue</h3>
|
|
|
|
|
+ <p className="text-sm">{error}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="outline"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ onClick={handleRefresh}
|
|
|
|
|
+ className="ml-auto"
|
|
|
|
|
+ >
|
|
|
|
|
+ <RefreshCw className="h-4 w-4 mr-2" />
|
|
|
|
|
+ Retry
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ )}
|
|
|
|
|
|
|
|
- {/* Queue Status */}
|
|
|
|
|
- {!queueStatus || queueStatus.jobs.length === 0 ? (
|
|
|
|
|
- <div className="flex flex-col items-center justify-center h-96 border-2 border-dashed border-border rounded-lg">
|
|
|
|
|
- <div className="text-center">
|
|
|
|
|
- <h3 className="text-lg font-medium text-muted-foreground mb-2">
|
|
|
|
|
- No jobs in queue
|
|
|
|
|
- </h3>
|
|
|
|
|
- <p className="text-sm text-muted-foreground">
|
|
|
|
|
- Generate some images using the Text to Image, Image to Image, or Inpainting tools.
|
|
|
|
|
- </p>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ {loading && !queueStatus ? (
|
|
|
|
|
+ <Card>
|
|
|
|
|
+ <CardContent className="pt-6">
|
|
|
|
|
+ <div className="flex items-center justify-center py-12">
|
|
|
|
|
+ <Loader2 className="h-8 w-8 animate-spin mr-3" />
|
|
|
|
|
+ <span>Loading queue status...</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
) : (
|
|
) : (
|
|
|
- <div className="space-y-4">
|
|
|
|
|
- {queueStatus.jobs.map((job, index) => (
|
|
|
|
|
- <Card key={job.id || job.request_id || index} className="overflow-hidden">
|
|
|
|
|
- <CardContent className="p-6">
|
|
|
|
|
- <div className="flex items-start justify-between">
|
|
|
|
|
- {/* Job Info */}
|
|
|
|
|
- <div className="flex-1 space-y-2">
|
|
|
|
|
- <div className="flex items-center gap-3">
|
|
|
|
|
- {getStatusIcon(job.status)}
|
|
|
|
|
- <h3 className="text-lg font-semibold">
|
|
|
|
|
- Job {job.id || job.request_id}
|
|
|
|
|
- </h3>
|
|
|
|
|
- {getStatusBadge(job.status)}
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 text-sm">
|
|
|
|
|
- <div>
|
|
|
|
|
- <span className="font-medium">Type:</span>
|
|
|
|
|
- <p className="text-muted-foreground">
|
|
|
|
|
- {job.prompt ? "Text to Image" : "Unknown"}
|
|
|
|
|
- </p>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div>
|
|
|
|
|
- <span className="font-medium">Created:</span>
|
|
|
|
|
- <p className="text-muted-foreground">
|
|
|
|
|
- {formatDate(job.created_at)}
|
|
|
|
|
- </p>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div>
|
|
|
|
|
- <span className="font-medium">Position:</span>
|
|
|
|
|
- <p className="text-muted-foreground">
|
|
|
|
|
- {job.position !== undefined ? job.position + 1 : "N/A"}
|
|
|
|
|
- </p>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- {/* Progress Bar */}
|
|
|
|
|
- <div className="space-y-2">
|
|
|
|
|
- <div className="flex items-center justify-between">
|
|
|
|
|
- <span className="font-medium">Progress:</span>
|
|
|
|
|
- <span className="text-sm text-muted-foreground">
|
|
|
|
|
- {getProgressValue(job)}%
|
|
|
|
|
- </span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <Progress value={getProgressValue(job)} className="h-2" />
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- {/* Prompt Preview */}
|
|
|
|
|
- {job.prompt && (
|
|
|
|
|
- <div>
|
|
|
|
|
- <span className="font-medium">Prompt:</span>
|
|
|
|
|
- <p className="text-sm text-muted-foreground line-clamp-2">
|
|
|
|
|
- {job.prompt}
|
|
|
|
|
- </p>
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {/* Error Message */}
|
|
|
|
|
- {job.error && (
|
|
|
|
|
- <div>
|
|
|
|
|
- <span className="font-medium text-red-500">Error:</span>
|
|
|
|
|
- <p className="text-sm text-red-500">
|
|
|
|
|
- {job.error}
|
|
|
|
|
- </p>
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- {/* Actions */}
|
|
|
|
|
- <div className="flex flex-col gap-2 ml-4">
|
|
|
|
|
- {(job.status === "queued" || job.status === "processing" || job.status === "pending") && (
|
|
|
|
|
- <Button
|
|
|
|
|
- onClick={() => handleCancelJob(job.id || job.request_id || "")}
|
|
|
|
|
- variant="destructive"
|
|
|
|
|
- size="sm"
|
|
|
|
|
- >
|
|
|
|
|
- <X className="h-4 w-4 mr-2" />
|
|
|
|
|
- Cancel
|
|
|
|
|
- </Button>
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </CardContent>
|
|
|
|
|
- </Card>
|
|
|
|
|
- ))}
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <EnhancedQueueList
|
|
|
|
|
+ queueStatus={queueStatus}
|
|
|
|
|
+ loading={loading}
|
|
|
|
|
+ onRefresh={handleRefresh}
|
|
|
|
|
+ onCancelJob={handleCancelJob}
|
|
|
|
|
+ onClearQueue={handleClearQueue}
|
|
|
|
|
+ actionLoading={actionLoading}
|
|
|
|
|
+ onCopyParameters={handleCopyParameters}
|
|
|
|
|
+ />
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
|
</AppLayout>
|
|
</AppLayout>
|
|
|
);
|
|
);
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-export default QueuePage;
|
|
|
|
|
|
|
+}
|