fina autoamtiona adn billing and credits
This commit is contained in:
390
frontend/src/components/Automation/CurrentProcessingCard.tsx
Normal file
390
frontend/src/components/Automation/CurrentProcessingCard.tsx
Normal file
@@ -0,0 +1,390 @@
|
||||
/**
|
||||
* Current Processing Card Component
|
||||
* Shows real-time automation progress with pause/resume/cancel controls
|
||||
*/
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { automationService, ProcessingState, AutomationRun } from '../../services/automationService';
|
||||
import { useToast } from '../ui/toast/ToastContainer';
|
||||
import Button from '../ui/button/Button';
|
||||
import {
|
||||
PlayIcon,
|
||||
PauseIcon,
|
||||
XMarkIcon,
|
||||
ClockIcon,
|
||||
BoltIcon
|
||||
} from '../../icons';
|
||||
|
||||
interface CurrentProcessingCardProps {
|
||||
runId: string;
|
||||
siteId: number;
|
||||
currentRun: AutomationRun;
|
||||
onUpdate: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
||||
runId,
|
||||
siteId,
|
||||
currentRun,
|
||||
onUpdate,
|
||||
onClose,
|
||||
}) => {
|
||||
const [processingState, setProcessingState] = useState<ProcessingState | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isPausing, setIsPausing] = useState(false);
|
||||
const [isResuming, setIsResuming] = useState(false);
|
||||
const [isCancelling, setIsCancelling] = useState(false);
|
||||
const toast = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const fetchState = async () => {
|
||||
try {
|
||||
const state = await automationService.getCurrentProcessing(siteId, runId);
|
||||
|
||||
if (!isMounted) return;
|
||||
|
||||
setProcessingState(state);
|
||||
setError(null);
|
||||
|
||||
// If stage completed (all items processed), trigger page refresh
|
||||
if (state && state.processed_items >= state.total_items && state.total_items > 0) {
|
||||
onUpdate();
|
||||
}
|
||||
} catch (err) {
|
||||
if (!isMounted) return;
|
||||
console.error('Error fetching processing state:', err);
|
||||
setError('Failed to load processing state');
|
||||
}
|
||||
};
|
||||
|
||||
// Only fetch if status is running or paused
|
||||
if (currentRun.status === 'running' || currentRun.status === 'paused') {
|
||||
// Initial fetch
|
||||
fetchState();
|
||||
|
||||
// Poll every 3 seconds
|
||||
const interval = setInterval(fetchState, 3000);
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
clearInterval(interval);
|
||||
};
|
||||
}
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [siteId, runId, currentRun.status, onUpdate]);
|
||||
|
||||
const handlePause = async () => {
|
||||
setIsPausing(true);
|
||||
try {
|
||||
await automationService.pause(siteId, runId);
|
||||
toast?.success('Automation pausing... will complete current item');
|
||||
// Trigger update to refresh run status
|
||||
setTimeout(onUpdate, 1000);
|
||||
} catch (error: any) {
|
||||
toast?.error(error?.message || 'Failed to pause automation');
|
||||
} finally {
|
||||
setIsPausing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResume = async () => {
|
||||
setIsResuming(true);
|
||||
try {
|
||||
await automationService.resume(siteId, runId);
|
||||
toast?.success('Automation resumed');
|
||||
// Trigger update to refresh run status
|
||||
setTimeout(onUpdate, 1000);
|
||||
} catch (error: any) {
|
||||
toast?.error(error?.message || 'Failed to resume automation');
|
||||
} finally {
|
||||
setIsResuming(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = async () => {
|
||||
if (!confirm('Are you sure you want to cancel this automation run? This cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCancelling(true);
|
||||
try {
|
||||
await automationService.cancel(siteId, runId);
|
||||
toast?.success('Automation cancelling... will complete current item');
|
||||
// Trigger update to refresh run status
|
||||
setTimeout(onUpdate, 1500);
|
||||
} catch (error: any) {
|
||||
toast?.error(error?.message || 'Failed to cancel automation');
|
||||
} finally {
|
||||
setIsCancelling(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDuration = (startTime: string) => {
|
||||
const start = new Date(startTime).getTime();
|
||||
const now = Date.now();
|
||||
const diffMs = now - start;
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
|
||||
if (diffHours > 0) {
|
||||
return `${diffHours}h ${diffMins % 60}m`;
|
||||
}
|
||||
return `${diffMins}m`;
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border-2 border-red-500 rounded-lg p-4 mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-red-700 dark:text-red-300 text-sm">{error}</p>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-red-500 hover:text-red-700 dark:hover:text-red-300"
|
||||
>
|
||||
<XMarkIcon className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!processingState && currentRun.status === 'running') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const percentage = processingState?.percentage || 0;
|
||||
const isPaused = currentRun.status === 'paused';
|
||||
|
||||
return (
|
||||
<div className={`border-2 rounded-lg p-6 mb-6 ${
|
||||
isPaused
|
||||
? 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-500'
|
||||
: 'bg-blue-50 dark:bg-blue-900/20 border-blue-500'
|
||||
}`}>
|
||||
{/* Header Row with Main Info and Close */}
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
{/* Left Side - Main Info (75%) */}
|
||||
<div className="flex-1 pr-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className={isPaused ? '' : 'animate-pulse'}>
|
||||
{isPaused ? (
|
||||
<PauseIcon className="w-8 h-8 text-yellow-600 dark:text-yellow-400" />
|
||||
) : (
|
||||
<BoltIcon className="w-8 h-8 text-blue-600 dark:text-blue-400" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{isPaused ? 'Automation Paused' : 'Automation In Progress'}
|
||||
</h2>
|
||||
{processingState && (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Stage {currentRun.current_stage}: {processingState.stage_name}
|
||||
<span className={`ml-2 px-2 py-0.5 rounded text-xs ${
|
||||
isPaused
|
||||
? 'bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300'
|
||||
: 'bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300'
|
||||
}`}>
|
||||
{processingState.stage_type}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Info */}
|
||||
{processingState && (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400">
|
||||
{percentage}%
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{processingState.processed_items}/{processingState.total_items} completed
|
||||
</div>
|
||||
</div>
|
||||
{/* Progress Bar */}
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-3">
|
||||
<div
|
||||
className={`h-3 rounded-full transition-all duration-500 ${
|
||||
isPaused
|
||||
? 'bg-yellow-600 dark:bg-yellow-500'
|
||||
: 'bg-blue-600 dark:bg-blue-500'
|
||||
}`}
|
||||
style={{ width: `${Math.min(percentage, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Currently Processing and Up Next */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
{/* Currently Processing */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
||||
Currently Processing:
|
||||
</h3>
|
||||
<div className="space-y-1">
|
||||
{processingState.currently_processing.length > 0 ? (
|
||||
processingState.currently_processing.map((item, idx) => (
|
||||
<div key={idx} className="flex items-start gap-2 text-sm">
|
||||
<span className={isPaused ? 'text-yellow-600 dark:text-yellow-400 mt-1' : 'text-blue-600 dark:text-blue-400 mt-1'}>•</span>
|
||||
<span className="text-gray-800 dark:text-gray-200 font-medium line-clamp-2">
|
||||
{item.title}
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 italic">
|
||||
{isPaused ? 'Paused' : 'No items currently processing'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Up Next */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
||||
Up Next:
|
||||
</h3>
|
||||
<div className="space-y-1">
|
||||
{processingState.up_next.length > 0 ? (
|
||||
<>
|
||||
{processingState.up_next.map((item, idx) => (
|
||||
<div key={idx} className="flex items-start gap-2 text-sm">
|
||||
<span className="text-gray-400 dark:text-gray-500 mt-1">•</span>
|
||||
<span className="text-gray-600 dark:text-gray-400 line-clamp-2">
|
||||
{item.title}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{processingState.remaining_count > processingState.up_next.length + processingState.currently_processing.length && (
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
||||
+ {processingState.remaining_count - processingState.up_next.length - processingState.currently_processing.length} more in queue
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 italic">
|
||||
Queue empty
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Control Buttons */}
|
||||
<div className="flex items-center gap-3">
|
||||
{currentRun.status === 'running' ? (
|
||||
<Button
|
||||
onClick={handlePause}
|
||||
disabled={isPausing}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
>
|
||||
<PauseIcon className="w-4 h-4 mr-2" />
|
||||
{isPausing ? 'Pausing...' : 'Pause'}
|
||||
</Button>
|
||||
) : currentRun.status === 'paused' ? (
|
||||
<Button
|
||||
onClick={handleResume}
|
||||
disabled={isResuming}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
>
|
||||
<PlayIcon className="w-4 h-4 mr-2" />
|
||||
{isResuming ? 'Resuming...' : 'Resume'}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
onClick={handleCancel}
|
||||
disabled={isCancelling}
|
||||
variant="danger"
|
||||
size="sm"
|
||||
>
|
||||
<XMarkIcon className="w-4 h-4 mr-2" />
|
||||
{isCancelling ? 'Cancelling...' : 'Cancel'}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right Side - Metrics and Close (25%) */}
|
||||
<div className="w-64 flex-shrink-0">
|
||||
{/* Close Button */}
|
||||
<div className="flex justify-end mb-4">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||
title="Close (card will remain available below)"
|
||||
>
|
||||
<XMarkIcon className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Metrics Cards */}
|
||||
<div className="space-y-3">
|
||||
{/* Duration */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<ClockIcon className="w-4 h-4 text-gray-500" />
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold">
|
||||
Duration
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-gray-900 dark:text-white">
|
||||
{formatDuration(currentRun.started_at)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Credits Used */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<BoltIcon className="w-4 h-4 text-amber-500" />
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold">
|
||||
Credits Used
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-amber-600 dark:text-amber-400">
|
||||
{currentRun.total_credits_used}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current Stage */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold mb-1">
|
||||
Stage
|
||||
</div>
|
||||
<div className="text-xl font-bold text-gray-900 dark:text-white">
|
||||
{currentRun.current_stage} of 7
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold mb-1">
|
||||
Status
|
||||
</div>
|
||||
<div className={`text-sm font-semibold ${
|
||||
isPaused
|
||||
? 'text-yellow-600 dark:text-yellow-400'
|
||||
: 'text-blue-600 dark:text-blue-400'
|
||||
}`}>
|
||||
{isPaused ? 'Paused' : 'Running'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrentProcessingCard;
|
||||
Reference in New Issue
Block a user