feat: real time progress update for runs
This commit is contained in:
@@ -12,6 +12,45 @@ import { GenericModal } from "../ui/GenericModal";
|
||||
import { getUserById } from "../../api/auth";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import { remoteBrowserApiUrl } from "../../apiConfig";
|
||||
|
||||
const socketCache = new Map<string, Socket>();
|
||||
const progressCallbacks = new Map<string, Set<(data: any) => void>>();
|
||||
|
||||
function getOrCreateSocket(browserId: string): Socket {
|
||||
if (socketCache.has(browserId)) {
|
||||
return socketCache.get(browserId)!;
|
||||
}
|
||||
|
||||
const socket = io(`${remoteBrowserApiUrl}/${browserId}`, {
|
||||
transports: ["websocket"],
|
||||
rejectUnauthorized: false
|
||||
});
|
||||
|
||||
socket.on('workflowProgress', (data: any) => {
|
||||
const callbacks = progressCallbacks.get(browserId);
|
||||
if (callbacks) {
|
||||
callbacks.forEach(cb => cb(data));
|
||||
}
|
||||
});
|
||||
|
||||
socketCache.set(browserId, socket);
|
||||
return socket;
|
||||
}
|
||||
|
||||
function cleanupSocketIfUnused(browserId: string) {
|
||||
const callbacks = progressCallbacks.get(browserId);
|
||||
|
||||
if (!callbacks || callbacks.size === 0) {
|
||||
const socket = socketCache.get(browserId);
|
||||
if (socket) {
|
||||
socket.disconnect();
|
||||
socketCache.delete(browserId);
|
||||
progressCallbacks.delete(browserId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface RunTypeChipProps {
|
||||
runByUserId?: string;
|
||||
@@ -54,6 +93,53 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, onToggleExpanded, cu
|
||||
|
||||
const logEndRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const [workflowProgress, setWorkflowProgress] = useState<{
|
||||
current: number;
|
||||
total: number;
|
||||
percentage: number;
|
||||
} | null>(null);
|
||||
|
||||
// Subscribe to progress updates using module-level socket cache
|
||||
useEffect(() => {
|
||||
if (!row.browserId) return;
|
||||
|
||||
// Get or create socket (from module cache)
|
||||
getOrCreateSocket(row.browserId);
|
||||
|
||||
// Register callback
|
||||
if (!progressCallbacks.has(row.browserId)) {
|
||||
progressCallbacks.set(row.browserId, new Set());
|
||||
}
|
||||
|
||||
const callback = (data: any) => {
|
||||
setWorkflowProgress(data);
|
||||
};
|
||||
|
||||
progressCallbacks.get(row.browserId)!.add(callback);
|
||||
|
||||
// Cleanup: remove callback and cleanup socket if no callbacks remain
|
||||
return () => {
|
||||
const callbacks = progressCallbacks.get(row.browserId);
|
||||
if (callbacks) {
|
||||
callbacks.delete(callback);
|
||||
// Cleanup socket if this was the last callback
|
||||
cleanupSocketIfUnused(row.browserId);
|
||||
}
|
||||
};
|
||||
}, [row.browserId]);
|
||||
|
||||
// Clear progress UI when run completes and trigger socket cleanup
|
||||
useEffect(() => {
|
||||
if (row.status !== 'running' && row.status !== 'queued') {
|
||||
setWorkflowProgress(null);
|
||||
// Attempt to cleanup socket when run completes
|
||||
// (will only cleanup if no other callbacks exist)
|
||||
if (row.browserId) {
|
||||
cleanupSocketIfUnused(row.browserId);
|
||||
}
|
||||
}
|
||||
}, [row.status, row.browserId]);
|
||||
|
||||
const handleAbort = () => {
|
||||
abortRunHandler(row.runId, row.name, row.browserId);
|
||||
}
|
||||
@@ -185,7 +271,8 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, onToggleExpanded, cu
|
||||
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
|
||||
<Collapse in={isOpen} timeout="auto" unmountOnExit>
|
||||
<RunContent row={row} abortRunHandler={handleAbort} currentLog={currentLog}
|
||||
logEndRef={logEndRef} interpretationInProgress={runningRecordingName === row.name} />
|
||||
logEndRef={logEndRef} interpretationInProgress={runningRecordingName === row.name}
|
||||
workflowProgress={workflowProgress} />
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -23,6 +23,8 @@ import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useThemeMode } from "../../context/theme-provider";
|
||||
import { remoteBrowserApiUrl } from "../../apiConfig";
|
||||
import { io } from "socket.io-client";
|
||||
|
||||
interface RunContentProps {
|
||||
row: Data,
|
||||
@@ -30,10 +32,14 @@ interface RunContentProps {
|
||||
interpretationInProgress: boolean,
|
||||
logEndRef: React.RefObject<HTMLDivElement>,
|
||||
abortRunHandler: () => void,
|
||||
workflowProgress: {
|
||||
current: number;
|
||||
total: number;
|
||||
percentage: number;
|
||||
} | null,
|
||||
}
|
||||
|
||||
export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRef, abortRunHandler }: RunContentProps) => {
|
||||
const { t } = useTranslation();
|
||||
export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRef, abortRunHandler, workflowProgress }: RunContentProps) => { const { t } = useTranslation();
|
||||
const [tab, setTab] = React.useState<string>('output');
|
||||
const [markdownContent, setMarkdownContent] = useState<string>('');
|
||||
const [htmlContent, setHtmlContent] = useState<string>('');
|
||||
@@ -63,6 +69,15 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
|
||||
setTab(tab);
|
||||
}, [interpretationInProgress]);
|
||||
|
||||
const getProgressMessage = (percentage: number): string => {
|
||||
if (percentage === 0) return 'Initializing workflow...';
|
||||
if (percentage < 25) return 'Starting execution...';
|
||||
if (percentage < 50) return 'Processing actions...';
|
||||
if (percentage < 75) return 'Extracting data...';
|
||||
if (percentage < 100) return 'Finalizing results...';
|
||||
return 'Completing...';
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setMarkdownContent('');
|
||||
setHtmlContent('');
|
||||
@@ -810,7 +825,20 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
|
||||
{row.status === 'running' || row.status === 'queued' ? (
|
||||
<>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<CircularProgress size={22} sx={{ marginRight: '10px' }} />
|
||||
{workflowProgress ? (
|
||||
<>
|
||||
<CircularProgress
|
||||
size={22}
|
||||
sx={{ marginRight: '10px' }}
|
||||
/>
|
||||
{getProgressMessage(workflowProgress.percentage)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CircularProgress size={22} sx={{ marginRight: '10px' }} />
|
||||
{t('run_content.loading')}
|
||||
</>
|
||||
)}
|
||||
{t('run_content.loading')}
|
||||
</Box>
|
||||
<Button color="error" onClick={abortRunHandler} sx={{ mt: 1 }}>
|
||||
|
||||
Reference in New Issue
Block a user