Merge pull request #628 from getmaxun/unified-run

feat: unified run creation and execution
This commit is contained in:
Karishma Shukla
2025-06-12 21:24:46 +05:30
committed by GitHub
15 changed files with 621 additions and 287 deletions

View File

@@ -35,6 +35,19 @@ export const getActiveBrowserId = async(): Promise<string> => {
}
};
export const canCreateBrowserInState = async(state: "recording" | "run"): Promise<boolean> => {
try {
const response = await axios.get(`${apiUrl}/record/can-create/${state}`, { withCredentials: true });
if (response.status === 200) {
return response.data.canCreate;
} else {
return false;
}
} catch(error: any) {
return false;
}
};
export const interpretCurrentRecording = async(): Promise<boolean> => {
try {
const response = await axios.get(`${apiUrl}/record/interpret`);

View File

@@ -154,6 +154,27 @@ export const editRecordingFromStorage = async (browserId: string, id: string): P
}
};
export interface CreateRunResponseWithQueue extends CreateRunResponse {
queued?: boolean;
}
export const createAndRunRecording = async (id: string, settings: RunSettings): Promise<CreateRunResponseWithQueue> => {
try {
const response = await axios.put(
`${apiUrl}/storage/runs/${id}`,
{ ...settings, withCredentials: true }
);
if (response.status === 200) {
return response.data;
} else {
throw new Error(`Couldn't create and run recording ${id}`);
}
} catch (error: any) {
console.log(error);
return { browserId: '', runId: '', robotMetaId: '', queued: false };
}
}
export const createRunForStoredRecording = async (id: string, settings: RunSettings): Promise<CreateRunResponse> => {
try {
const response = await axios.put(

View File

@@ -39,7 +39,7 @@ import { useGlobalInfoStore } from "../../context/globalInfo";
import { checkRunsForRecording, deleteRecordingFromStorage, getStoredRecordings } from "../../api/storage";
import { Add } from "@mui/icons-material";
import { useNavigate } from 'react-router-dom';
import { getActiveBrowserId, stopRecording } from "../../api/recording";
import { canCreateBrowserInState, getActiveBrowserId, stopRecording } from "../../api/recording";
import { GenericModal } from '../ui/GenericModal';
declare global {
@@ -274,11 +274,16 @@ export const RecordingsTable = ({
}, [setRecordings, notify, t]);
const handleNewRecording = useCallback(async () => {
const activeBrowserId = await getActiveBrowserId();
const canCreateRecording = await canCreateBrowserInState("recording");
if (activeBrowserId) {
setActiveBrowserId(activeBrowserId);
setWarningModalOpen(true);
if (!canCreateRecording) {
const activeBrowserId = await getActiveBrowserId();
if (activeBrowserId) {
setActiveBrowserId(activeBrowserId);
setWarningModalOpen(true);
} else {
notify('warning', t('recordingtable.notifications.browser_limit_warning'));
}
} else {
setModalOpen(true);
}
@@ -314,7 +319,6 @@ export const RecordingsTable = ({
};
const handleRetrainRobot = useCallback(async (id: string, name: string) => {
const activeBrowserId = await getActiveBrowserId();
const robot = rows.find(row => row.id === id);
let targetUrl;
@@ -340,11 +344,18 @@ export const RecordingsTable = ({
window.sessionStorage.setItem('initialUrl', targetUrl);
}
if (activeBrowserId) {
setActiveBrowserId(activeBrowserId);
setWarningModalOpen(true);
const canCreateRecording = await canCreateBrowserInState("recording");
if (!canCreateRecording) {
const activeBrowserId = await getActiveBrowserId();
if (activeBrowserId) {
setActiveBrowserId(activeBrowserId);
setWarningModalOpen(true);
} else {
notify('warning', t('recordingtable.notifications.browser_limit_warning'));
}
} else {
startRetrainRecording(id, name, targetUrl);
startRetrainRecording(id, name, targetUrl);
}
}, [rows, setInitialUrl, setRecordingUrl]);

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect } from 'react';
import React, { useCallback, useContext, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { MainMenu } from "../components/dashboard/MainMenu";
import { Stack } from "@mui/material";
@@ -7,13 +7,14 @@ import { Runs } from "../components/run/Runs";
import ProxyForm from '../components/proxy/ProxyForm';
import ApiKey from '../components/api/ApiKey';
import { useGlobalInfoStore } from "../context/globalInfo";
import { createRunForStoredRecording, interpretStoredRecording, notifyAboutAbort, scheduleStoredRecording } from "../api/storage";
import { createAndRunRecording, createRunForStoredRecording, CreateRunResponseWithQueue, interpretStoredRecording, notifyAboutAbort, scheduleStoredRecording } from "../api/storage";
import { io, Socket } from "socket.io-client";
import { stopRecording } from "../api/recording";
import { RunSettings } from "../components/run/RunSettings";
import { ScheduleSettings } from "../components/robot/ScheduleSettings";
import { apiUrl } from "../apiConfig";
import { useNavigate } from 'react-router-dom';
import { AuthContext } from '../context/auth';
interface MainPageProps {
handleEditRecording: (id: string, fileName: string) => void;
@@ -43,12 +44,16 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps)
runId: '',
robotMetaId: ''
});
const [queuedRuns, setQueuedRuns] = React.useState<Set<string>>(new Set());
let aborted = false;
const { notify, setRerenderRuns, setRecordingId } = useGlobalInfoStore();
const navigate = useNavigate();
const { state } = useContext(AuthContext);
const { user } = state;
const abortRunHandler = (runId: string) => {
aborted = true;
notifyAboutAbort(runId).then(async (response) => {
@@ -90,48 +95,109 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps)
}, [currentInterpretationLog])
const handleRunRecording = useCallback((settings: RunSettings) => {
createRunForStoredRecording(runningRecordingId, settings).then(({ browserId, runId, robotMetaId }: CreateRunResponse) => {
createAndRunRecording(runningRecordingId, settings).then((response: CreateRunResponseWithQueue) => {
const { browserId, runId, robotMetaId, queued } = response;
setIds({ browserId, runId, robotMetaId });
navigate(`/runs/${robotMetaId}/run/${runId}`);
const socket =
io(`${apiUrl}/${browserId}`, {
if (queued) {
console.log('Creating queue socket for queued run:', runId);
setQueuedRuns(prev => new Set([...prev, runId]));
const queueSocket = io(`${apiUrl}/queued-run`, {
transports: ["websocket"],
rejectUnauthorized: false,
query: { userId: user?.id }
});
queueSocket.on('connect', () => {
console.log('Queue socket connected for user:', user?.id);
});
queueSocket.on('connect_error', (error) => {
console.log('Queue socket connection error:', error);
});
queueSocket.on('run-completed', (completionData) => {
if (completionData.runId === runId) {
setRunningRecordingName('');
setCurrentInterpretationLog('');
setRerenderRuns(true);
setQueuedRuns(prev => {
const newSet = new Set(prev);
newSet.delete(runId);
return newSet;
});
const robotName = completionData.robotName || runningRecordingName;
if (completionData.status === 'success') {
notify('success', t('main_page.notifications.interpretation_success', { name: robotName }));
} else {
notify('error', t('main_page.notifications.interpretation_failed', { name: robotName }));
}
queueSocket.disconnect();
}
});
setSockets(sockets => [...sockets, queueSocket]);
notify('info', `Run queued: ${runningRecordingName}`);
} else {
const socket = io(`${apiUrl}/${browserId}`, {
transports: ["websocket"],
rejectUnauthorized: false
});
setSockets(sockets => [...sockets, socket]);
socket.on('ready-for-run', () => readyForRunHandler(browserId, runId));
socket.on('debugMessage', debugMessageHandler);
socket.on('run-completed', (data) => {
setRerenderRuns(true);
const robotName = data.robotName;
setSockets(sockets => [...sockets, socket]);
if (data.status === 'success') {
notify('success', t('main_page.notifications.interpretation_success', { name: robotName }));
socket.on('debugMessage', debugMessageHandler);
socket.on('run-completed', (data) => {
setRunningRecordingName('');
setCurrentInterpretationLog('');
setRerenderRuns(true);
const robotName = data.robotName;
if (data.status === 'success') {
notify('success', t('main_page.notifications.interpretation_success', { name: robotName }));
} else {
notify('error', t('main_page.notifications.interpretation_failed', { name: robotName }));
}
});
socket.on('connect_error', (error) => {
console.log('error', `Failed to connect to browser ${browserId}: ${error}`);
notify('error', t('main_page.notifications.connection_failed', { name: runningRecordingName }));
});
socket.on('disconnect', (reason) => {
console.log('warn', `Disconnected from browser ${browserId}: ${reason}`);
});
if (runId) {
notify('info', t('main_page.notifications.run_started', { name: runningRecordingName }));
} else {
notify('error', t('main_page.notifications.interpretation_failed', { name: robotName }));
notify('error', t('main_page.notifications.run_start_failed', { name: runningRecordingName }));
}
});
socket.on('run-aborted', (data) => {
setRerenderRuns(true);
const abortedRobotName = data.robotName;
notify('success', t('main_page.notifications.abort_success', { name: abortedRobotName }));
});
setContent('runs');
if (browserId) {
notify('info', t('main_page.notifications.run_started', { name: runningRecordingName }));
} else {
notify('error', t('main_page.notifications.run_start_failed', { name: runningRecordingName }));
}
})
return (socket: Socket, browserId: string, runId: string) => {
socket.off('ready-for-run', () => readyForRunHandler(browserId, runId));
setContent('runs');
}).catch((error: any) => {
console.error('Error in createAndRunRecording:', error); // ✅ Debug log
});
return (socket: Socket) => {
socket.off('debugMessage', debugMessageHandler);
socket.off('run-completed');
socket.off('connect_error');
socket.off('disconnect');
}
}, [runningRecordingName, sockets, ids, readyForRunHandler, debugMessageHandler])
}, [runningRecordingName, sockets, ids, debugMessageHandler, user?.id, t, notify, setRerenderRuns, setQueuedRuns, navigate, setContent, setIds]);
const handleScheduleRecording = (settings: ScheduleSettings) => {
scheduleStoredRecording(runningRecordingId, settings)