diff --git a/server/src/api/record.ts b/server/src/api/record.ts index b55f06bc..573a8edb 100644 --- a/server/src/api/record.ts +++ b/server/src/api/record.ts @@ -289,7 +289,7 @@ router.get("/robots/:id", requireAPIKey, async (req: Request, res: Response) => * type: string * example: "Failed to retrieve runs" */ -router.get("/robots/:id/runs", requireAPIKey, async (req: Request, res: Response) => { +router.get("/robots/:id/runs",requireAPIKey, async (req: Request, res: Response) => { try { const runs = await Run.findAll({ where: { @@ -321,6 +321,7 @@ router.get("/robots/:id/runs", requireAPIKey, async (req: Request, res: Response } ); + function formatRunResponse(run: any) { const formattedRun = { id: run.id, diff --git a/server/src/routes/storage.ts b/server/src/routes/storage.ts index 91272663..95ca2709 100644 --- a/server/src/routes/storage.ts +++ b/server/src/routes/storage.ts @@ -16,6 +16,7 @@ import { workflowQueue } from '../worker'; import { AuthenticatedRequest } from './record'; import { computeNextRun } from '../utils/schedule'; import { capture } from "../utils/analytics"; +import { tryCatch } from 'bullmq'; export const router = Router(); @@ -57,6 +58,59 @@ router.get('/recordings/:id', requireSignIn, async (req, res) => { } }) +router.get(('/recordings/:id/runs'), requireSignIn, async (req, res) => { + try { + const runs = await Run.findAll({ + where: { + robotMetaId: req.params.id + }, + raw: true + }); + const formattedRuns = runs.map(formatRunResponse); + const response = { + statusCode: 200, + messageCode: "success", + runs: { + totalCount: formattedRuns.length, + items: formattedRuns, + }, + }; + + res.status(200).json(response); +} catch (error) { + console.error("Error fetching runs:", error); + res.status(500).json({ + statusCode: 500, + messageCode: "error", + message: "Failed to retrieve runs", + }); +} +}) + +function formatRunResponse(run: any) { + const formattedRun = { + id: run.id, + status: run.status, + name: run.name, + robotId: run.robotMetaId, // Renaming robotMetaId to robotId + startedAt: run.startedAt, + finishedAt: run.finishedAt, + runId: run.runId, + runByUserId: run.runByUserId, + runByScheduleId: run.runByScheduleId, + runByAPI: run.runByAPI, + data: {}, + screenshot: null, + }; + + if (run.serializableOutput && run.serializableOutput['item-0']) { + formattedRun.data = run.serializableOutput['item-0']; + } else if (run.binaryOutput && run.binaryOutput['item-0']) { + formattedRun.screenshot = run.binaryOutput['item-0']; + } + + return formattedRun; +} /** * PUT endpoint to update the name and limit of a robot. @@ -217,9 +271,6 @@ router.post('/recordings/:id/duplicate', requireSignIn, async (req: Authenticate } }); - - - /** * DELETE endpoint for deleting a recording from the storage. */ diff --git a/src/api/storage.ts b/src/api/storage.ts index 90ab91af..4b2f4e80 100644 --- a/src/api/storage.ts +++ b/src/api/storage.ts @@ -5,6 +5,11 @@ import { ScheduleSettings } from "../components/molecules/ScheduleSettings"; import { CreateRunResponse, ScheduleRunResponse } from "../pages/MainPage"; import { apiUrl } from "../apiConfig"; + + + + + export const getStoredRecordings = async (): Promise => { try { const response = await axios.get(`${apiUrl}/storage/recordings`); @@ -77,18 +82,49 @@ export const getStoredRecording = async (id: string) => { } } + + +export const checkRunsForRecording = async (id: string): Promise => { + + + try { + const response = await axios.get(`${apiUrl}/storage/recordings/${id}/runs`); + + const runs = response.data; + console.log(runs.runs.totalCount) + return runs.runs.totalCount > 0; + } catch (error) { + console.error('Error checking runs for recording:', error); + return false; + } +}; + + export const deleteRecordingFromStorage = async (id: string): Promise => { + + const hasRuns = await checkRunsForRecording(id); + + if (hasRuns) { + + return false; + } try { const response = await axios.delete(`${apiUrl}/storage/recordings/${id}`); if (response.status === 200) { - return response.data; + + return true; } else { throw new Error(`Couldn't delete stored recording ${id}`); } } catch (error: any) { console.log(error); + return false; } + + + + }; export const deleteRunFromStorage = async (id: string): Promise => { @@ -123,7 +159,7 @@ export const createRunForStoredRecording = async (id: string, settings: RunSetti try { const response = await axios.put( `${apiUrl}/storage/runs/${id}`, - { ...settings }); + { ...settings }); if (response.status === 200) { return response.data; } else { diff --git a/src/components/molecules/RecordingsTable.tsx b/src/components/molecules/RecordingsTable.tsx index f309dcc8..bd4d6fe9 100644 --- a/src/components/molecules/RecordingsTable.tsx +++ b/src/components/molecules/RecordingsTable.tsx @@ -13,11 +13,13 @@ import { IconButton, Button, Box, Typography, TextField, MenuItem, Menu, ListIte import { Schedule, DeleteForever, Edit, PlayCircle, Settings, Power, ContentCopy, } from "@mui/icons-material"; import LinkIcon from '@mui/icons-material/Link'; import { useGlobalInfoStore } from "../../context/globalInfo"; -import { deleteRecordingFromStorage, getStoredRecordings } from "../../api/storage"; +import { checkRunsForRecording, deleteRecordingFromStorage, getStoredRecordings } from "../../api/storage"; import { Add } from "@mui/icons-material"; import { useNavigate } from 'react-router-dom'; import { stopRecording } from "../../api/recording"; import { GenericModal } from '../atoms/GenericModal'; +import axios from 'axios'; +import { apiUrl } from '../../apiConfig'; import { Menu as MenuIcon } from '@mui/icons-material'; /** TODO: @@ -161,6 +163,8 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl } }, []); + + return ( @@ -256,6 +260,13 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl handleEditRobot(row.id, row.name, row.params || [])} handleDelete={() => { + + checkRunsForRecording(row.id).then((result: boolean) => { + if (result) { + notify('warning', 'Cannot delete recording as it has active runs'); + } + }) + deleteRecordingFromStorage(row.id).then((result: boolean) => { if (result) { setRows([]); diff --git a/src/components/organisms/ApiKey.tsx b/src/components/organisms/ApiKey.tsx index d9f4c6fe..675edb72 100644 --- a/src/components/organisms/ApiKey.tsx +++ b/src/components/organisms/ApiKey.tsx @@ -36,6 +36,10 @@ const ApiKeyManager = () => { const [copySuccess, setCopySuccess] = useState(false); const { notify } = useGlobalInfoStore(); + + + + useEffect(() => { const fetchApiKey = async () => { try { @@ -49,6 +53,7 @@ const ApiKeyManager = () => { }; fetchApiKey(); + }, []); const generateApiKey = async () => { @@ -56,6 +61,7 @@ const ApiKeyManager = () => { try { const { data } = await axios.post(`${apiUrl}/auth/generate-api-key`); setApiKey(data.api_key); + notify('success', `Generated API Key successfully`); } catch (error: any) { notify('error', `Failed to generate API Key - ${error.message}`); diff --git a/src/components/organisms/BrowserWindow.tsx b/src/components/organisms/BrowserWindow.tsx index cbc46731..697b4adb 100644 --- a/src/components/organisms/BrowserWindow.tsx +++ b/src/components/organisms/BrowserWindow.tsx @@ -8,6 +8,7 @@ import { useActionContext } from '../../context/browserActions'; import { useBrowserSteps, TextStep } from '../../context/browserSteps'; import { useGlobalInfoStore } from '../../context/globalInfo'; + interface ElementInfo { tagName: string; hasOnlyText?: boolean; @@ -316,7 +317,7 @@ export const BrowserWindow = () => { } }, [paginationMode, resetPaginationSelector]); - + return (
{