Merge pull request #61 from amhsirak/develop

feat: storage
This commit is contained in:
Karishma Shukla
2024-10-10 03:31:53 +05:30
committed by GitHub
18 changed files with 539 additions and 242 deletions

4
server/src/api/run.ts Normal file
View File

@@ -0,0 +1,4 @@
import { readFile, readFiles } from "../workflow-management/storage";
import { Router, Request, Response } from 'express';
import { requireAPIKey } from "../middlewares/api";
const router = Router();

View File

@@ -1,5 +1,6 @@
import { Sequelize } from 'sequelize'; import { Sequelize } from 'sequelize';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import setupAssociations from '../models/associations';
dotenv.config(); dotenv.config();
const sequelize = new Sequelize( const sequelize = new Sequelize(
@@ -22,6 +23,7 @@ export const connectDB = async () => {
export const syncDB = async () => { export const syncDB = async () => {
try { try {
//setupAssociations();
await sequelize.sync({ force: false }); // force: true will drop and recreate tables on every run await sequelize.sync({ force: false }); // force: true will drop and recreate tables on every run
console.log('Database synced successfully!'); console.log('Database synced successfully!');
} catch (error) { } catch (error) {

View File

@@ -0,0 +1,55 @@
import { Model, DataTypes, Optional } from 'sequelize';
import sequelize from '../db/config';
import { WorkflowFile, Where, What, WhereWhatPair } from 'maxun-core';
interface RobotMeta {
name: string;
id: string;
createdAt: string;
pairs: number;
updatedAt: string;
params: any[];
}
interface RobotWorkflow {
workflow: WhereWhatPair[];
}
interface RobotAttributes {
id: string;
recording_meta: RobotMeta;
recording: RobotWorkflow;
}
interface RobotCreationAttributes extends Optional<RobotAttributes, 'id'> { }
class Robot extends Model<RobotAttributes, RobotCreationAttributes> implements RobotAttributes {
public id!: string;
public recording_meta!: RobotMeta;
public recording!: RobotWorkflow;
}
Robot.init(
{
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
recording_meta: {
type: DataTypes.JSONB,
allowNull: false,
},
recording: {
type: DataTypes.JSONB,
allowNull: false,
},
},
{
sequelize,
tableName: 'robot',
timestamps: false,
}
);
export default Robot;

116
server/src/models/Run.ts Normal file
View File

@@ -0,0 +1,116 @@
import { Model, DataTypes, Optional } from 'sequelize';
import sequelize from '../db/config';
import Robot from './Robot';
// TODO:
// 1. rename variables
// 2. we might not need interpreter settings?
// 3. store binaryOutput in MinIO
interface InterpreterSettings {
maxConcurrency: number;
maxRepeats: number;
debug: boolean;
}
interface RunAttributes {
id: string;
status: string;
name: string;
robotId: string;
robotMetaId: string;
startedAt: string;
finishedAt: string;
browserId: string;
interpreterSettings: InterpreterSettings;
log: string;
runId: string;
serializableOutput: Record<string, any[]>;
binaryOutput: Record<string, any>;
}
interface RunCreationAttributes extends Optional<RunAttributes, 'id'> { }
class Run extends Model<RunAttributes, RunCreationAttributes> implements RunAttributes {
public id!: string;
public status!: string;
public name!: string;
public robotId!: string;
public robotMetaId!: string;
public startedAt!: string;
public finishedAt!: string;
public browserId!: string;
public interpreterSettings!: InterpreterSettings;
public log!: string;
public runId!: string;
public serializableOutput!: Record<string, any[]>;
public binaryOutput!: Record<string, any>;
}
Run.init(
{
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
status: {
type: DataTypes.STRING(50),
allowNull: false,
},
name: {
type: DataTypes.STRING(255),
allowNull: false,
},
robotId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: Robot,
key: 'id',
},
},
robotMetaId: {
type: DataTypes.UUID,
allowNull: false,
},
startedAt: {
type: DataTypes.STRING(255),
allowNull: false,
},
finishedAt: {
type: DataTypes.STRING(255),
allowNull: false,
},
browserId: {
type: DataTypes.UUID,
allowNull: false,
},
interpreterSettings: {
type: DataTypes.JSONB,
allowNull: false,
},
log: {
type: DataTypes.TEXT,
allowNull: true,
},
runId: {
type: DataTypes.UUID,
allowNull: false,
},
serializableOutput: {
type: DataTypes.JSONB,
allowNull: true,
},
binaryOutput: {
type: DataTypes.JSONB,
allowNull: true,
},
},
{
sequelize,
tableName: 'run',
timestamps: false,
}
);
export default Run;

View File

@@ -0,0 +1,7 @@
import Robot from './Robot';
import Run from './Run';
export default function setupAssociations() {
Run.belongsTo(Robot, { foreignKey: 'robotId' });
Robot.hasMany(Run, { foreignKey: 'robotId' });
}

View File

@@ -11,12 +11,14 @@ import cron from 'node-cron';
import { googleSheetUpdateTasks, processGoogleSheetUpdates } from '../workflow-management/integrations/gsheet'; import { googleSheetUpdateTasks, processGoogleSheetUpdates } from '../workflow-management/integrations/gsheet';
import { getDecryptedProxyConfig } from './proxy'; import { getDecryptedProxyConfig } from './proxy';
import { requireSignIn } from '../middlewares/auth'; import { requireSignIn } from '../middlewares/auth';
// import { workflowQueue } from '../worker'; import Robot from '../models/Robot';
import Run from '../models/Run';
import { workflowQueue } from '../worker';
// todo: move from here // todo: move from here
export const getRecordingByFileName = async (fileName: string): Promise<any | null> => { export const getRecordingByFileName = async (fileName: string): Promise<any | null> => {
try { try {
const recording = await readFile(`./../storage/recordings/${fileName}.waw.json`) const recording = await readFile(`./../storage/recordings/${fileName}.json`)
const parsedRecording = JSON.parse(recording); const parsedRecording = JSON.parse(recording);
return parsedRecording; return parsedRecording;
} catch (error: any) { } catch (error: any) {
@@ -40,7 +42,7 @@ router.all('/', requireSignIn, (req, res, next) => {
*/ */
router.get('/recordings', requireSignIn, async (req, res) => { router.get('/recordings', requireSignIn, async (req, res) => {
try { try {
const data = await readFiles('./../storage/recordings/'); const data = await Robot.findAll();
return res.send(data); return res.send(data);
} catch (e) { } catch (e) {
logger.log('info', 'Error while reading recordings'); logger.log('info', 'Error while reading recordings');
@@ -51,13 +53,15 @@ router.get('/recordings', requireSignIn, async (req, res) => {
/** /**
* DELETE endpoint for deleting a recording from the storage. * DELETE endpoint for deleting a recording from the storage.
*/ */
router.delete('/recordings/:fileName', requireSignIn, async (req, res) => { router.delete('/recordings/:id', requireSignIn, async (req, res) => {
try { try {
await deleteFile(`./../storage/recordings/${req.params.fileName}.waw.json`); await Robot.destroy({
where: { 'recording_meta.id': req.params.id }
});
return res.send(true); return res.send(true);
} catch (e) { } catch (e) {
const { message } = e as Error; const { message } = e as Error;
logger.log('info', `Error while deleting a recording with name: ${req.params.fileName}.waw.json`); logger.log('info', `Error while deleting a recording with name: ${req.params.fileName}.json`);
return res.send(false); return res.send(false);
} }
}); });
@@ -67,7 +71,7 @@ router.delete('/recordings/:fileName', requireSignIn, async (req, res) => {
*/ */
router.get('/runs', requireSignIn, async (req, res) => { router.get('/runs', requireSignIn, async (req, res) => {
try { try {
const data = await readFiles('./../storage/runs/'); const data = await Run.findAll();
return res.send(data); return res.send(data);
} catch (e) { } catch (e) {
logger.log('info', 'Error while reading runs'); logger.log('info', 'Error while reading runs');
@@ -78,9 +82,9 @@ router.get('/runs', requireSignIn, async (req, res) => {
/** /**
* DELETE endpoint for deleting a run from the storage. * DELETE endpoint for deleting a run from the storage.
*/ */
router.delete('/runs/:fileName', requireSignIn, async (req, res) => { router.delete('/runs/:id', requireSignIn, async (req, res) => {
try { try {
await deleteFile(`./../storage/runs/${req.params.fileName}.json`); await Run.destroy({ where: { runId: req.params.id } });
return res.send(true); return res.send(true);
} catch (e) { } catch (e) {
const { message } = e as Error; const { message } = e as Error;
@@ -93,9 +97,17 @@ router.delete('/runs/:fileName', requireSignIn, async (req, res) => {
* PUT endpoint for starting a remote browser instance and saving run metadata to the storage. * PUT endpoint for starting a remote browser instance and saving run metadata to the storage.
* Making it ready for interpretation and returning a runId. * Making it ready for interpretation and returning a runId.
*/ */
router.put('/runs/:fileName', requireSignIn, async (req, res) => { router.put('/runs/:id', requireSignIn, async (req, res) => {
try { try {
const recording = await getRecordingByFileName(req.params.fileName); console.log(`Params recieved:`, req.params)
const recording = await Robot.findOne({
where: {
'recording_meta.id': req.params.id
},
raw: true
});
console.log(`Recording found:`, recording)
if (!recording || !recording.recording_meta || !recording.recording_meta.id) { if (!recording || !recording.recording_meta || !recording.recording_meta.id) {
return res.status(404).send({ error: 'Recording not found' }); return res.status(404).send({ error: 'Recording not found' });
@@ -124,33 +136,41 @@ router.put('/runs/:fileName', requireSignIn, async (req, res) => {
const runId = uuid(); const runId = uuid();
const run_meta = { const run = await Run.create({
status: 'RUNNING', status: 'RUNNING',
name: req.params.fileName, name: recording.recording_meta.name,
recordingId: recording.recording_meta.id, robotId: recording.id,
robotMetaId: recording.recording_meta.id,
startedAt: new Date().toLocaleString(), startedAt: new Date().toLocaleString(),
finishedAt: '', finishedAt: '',
browserId: id, browserId: id,
interpreterSettings: req.body, interpreterSettings: req.body,
log: '', log: '',
runId, runId,
}; serializableOutput: {},
fs.mkdirSync('../storage/runs', { recursive: true }) binaryOutput: {},
await saveFile( });
`../storage/runs/${req.params.fileName}_${runId}.json`,
JSON.stringify({ ...run_meta }, null, 2)
);
logger.log('debug', `Created run with name: ${req.params.fileName}.json`);
console.log('Run meta:', run_meta); const plainRun = run.toJSON();
console.log(`Created run (plain object):`, plainRun);
// // we need to handle this via DB
// fs.mkdirSync('../storage/runs', { recursive: true })
// await saveFile(
// `../storage/runs/${req.params.fileName}_${runId}.json`,
// JSON.stringify({ ...run_meta }, null, 2)
// );
// logger.log('debug', `Created run with name: ${req.params.fileName}.json`);
// console.log('Run meta:', run_meta);
return res.send({ return res.send({
browserId: id, browserId: id,
runId: runId, runId: plainRun.runId,
}); });
} catch (e) { } catch (e) {
const { message } = e as Error; const { message } = e as Error;
logger.log('info', `Error while creating a run with name: ${req.params.fileName}.json`); logger.log('info', `Error while creating a run with recording id: ${req.params.id} - ${message}`);
return res.send(''); return res.send('');
} }
}); });
@@ -158,15 +178,19 @@ router.put('/runs/:fileName', requireSignIn, async (req, res) => {
/** /**
* GET endpoint for getting a run from the storage. * GET endpoint for getting a run from the storage.
*/ */
router.get('/runs/run/:fileName/:runId', requireSignIn, async (req, res) => { router.get('/runs/run/:id', requireSignIn, async (req, res) => {
try { try {
console.log(`Params for GET /runs/run/:id`, req.params.id)
// read the run from storage // read the run from storage
const run = await readFile(`./../storage/runs/${req.params.fileName}_${req.params.runId}.json`) const run = await Run.findOne({ where: { runId: req.params.runId }, raw: true });
const parsedRun = JSON.parse(run); //const parsedRun = JSON.parse(run);
return res.send(parsedRun); if (!run) {
return res.status(404).send(null);
}
return res.send(run);
} catch (e) { } catch (e) {
const { message } = e as Error; const { message } = e as Error;
logger.log('error', `Error ${message} while reading a run with name: ${req.params.fileName}_${req.params.runId}.json`); logger.log('error', `Error ${message} while reading a run with id: ${req.params.id}.json`);
return res.send(null); return res.send(null);
} }
}); });
@@ -174,38 +198,48 @@ router.get('/runs/run/:fileName/:runId', requireSignIn, async (req, res) => {
/** /**
* PUT endpoint for finishing a run and saving it to the storage. * PUT endpoint for finishing a run and saving it to the storage.
*/ */
router.post('/runs/run/:fileName/:runId', requireSignIn, async (req, res) => { router.post('/runs/run/:id', requireSignIn, async (req, res) => {
try { try {
const recording = await readFile(`./../storage/recordings/${req.params.fileName}.waw.json`) // const recording = await readFile(`./../storage/recordings/${req.params.fileName}.json`)
const parsedRecording = JSON.parse(recording); // const parsedRecording = JSON.parse(recording);
const run = await readFile(`./../storage/runs/${req.params.fileName}_${req.params.runId}.json`) // const run = await readFile(`./../storage/runs/${req.params.fileName}_${req.params.runId}.json`)
const parsedRun = JSON.parse(run); // const parsedRun = JSON.parse(run);
console.log(`Params for POST /runs/run/:id`, req.params.id)
const run = await Run.findOne({ where: { runId: req.params.id } });
if (!run) {
return res.status(404).send(false);
}
console.log(`found run: ${run}`)
const plainRun = run.toJSON();
const recording = await Robot.findOne({ where: { 'recording_meta.id': plainRun.robotMetaId }, raw: true });
if (!recording) {
return res.status(404).send(false);
}
// interpret the run in active browser // interpret the run in active browser
const browser = browserPool.getRemoteBrowser(parsedRun.browserId); const browser = browserPool.getRemoteBrowser(plainRun.browserId);
const currentPage = browser?.getCurrentPage(); const currentPage = browser?.getCurrentPage();
if (browser && currentPage) { if (browser && currentPage) {
const interpretationInfo = await browser.interpreter.InterpretRecording( const interpretationInfo = await browser.interpreter.InterpretRecording(
parsedRecording.recording, currentPage, parsedRun.interpreterSettings); recording.recording, currentPage, plainRun.interpreterSettings);
await destroyRemoteBrowser(parsedRun.browserId); await destroyRemoteBrowser(plainRun.browserId);
const run_meta = { await run.update({
...parsedRun, ...run,
status: 'success', status: 'success',
finishedAt: new Date().toLocaleString(), finishedAt: new Date().toLocaleString(),
browserId: parsedRun.browserId, browserId: plainRun.browserId,
log: interpretationInfo.log.join('\n'), log: interpretationInfo.log.join('\n'),
serializableOutput: interpretationInfo.serializableOutput, serializableOutput: interpretationInfo.serializableOutput,
binaryOutput: interpretationInfo.binaryOutput, binaryOutput: interpretationInfo.binaryOutput,
}; });
fs.mkdirSync('../storage/runs', { recursive: true }) googleSheetUpdateTasks[req.params.id] = {
await saveFile( name: plainRun.name,
`../storage/runs/${parsedRun.name}_${req.params.runId}.json`, runId: plainRun.runId,
JSON.stringify(run_meta, null, 2)
);
googleSheetUpdateTasks[req.params.runId] = {
name: parsedRun.name,
runId: req.params.runId,
status: 'pending', status: 'pending',
retries: 5, retries: 5,
}; };
@@ -216,15 +250,15 @@ router.post('/runs/run/:fileName/:runId', requireSignIn, async (req, res) => {
} }
} catch (e) { } catch (e) {
const { message } = e as Error; const { message } = e as Error;
logger.log('info', `Error while running a recording with name: ${req.params.fileName}_${req.params.runId}.json`); logger.log('info', `Error while running a recording with id: ${req.params.id} - ${message}`);
return res.send(false); return res.send(false);
} }
}); });
router.put('/schedule/:fileName/', requireSignIn, async (req, res) => { router.put('/schedule/:id/', requireSignIn, async (req, res) => {
console.log(req.body); console.log(req.body);
try { try {
const { fileName } = req.params; const { id } = req.params;
const { const {
runEvery, runEvery,
runEveryUnit, runEveryUnit,
@@ -233,7 +267,7 @@ router.put('/schedule/:fileName/', requireSignIn, async (req, res) => {
timezone timezone
} = req.body; } = req.body;
if (!fileName || !runEvery || !runEveryUnit || !startFrom || !atTime || !timezone) { if (!id || !runEvery || !runEveryUnit || !startFrom || !atTime || !timezone) {
return res.status(400).json({ error: 'Missing required parameters' }); return res.status(400).json({ error: 'Missing required parameters' });
} }
@@ -281,17 +315,18 @@ router.put('/schedule/:fileName/', requireSignIn, async (req, res) => {
} }
const runId = uuid(); const runId = uuid();
const userId = req.user.id;
// await workflowQueue.add( await workflowQueue.add(
// 'run workflow', 'run workflow',
// { fileName, runId }, { id, runId, userId },
// { {
// repeat: { repeat: {
// pattern: cronExpression, pattern: cronExpression,
// tz: timezone tz: timezone
// } }
// } }
// ); );
res.status(200).json({ res.status(200).json({
message: 'success', message: 'success',
@@ -316,12 +351,16 @@ router.put('/schedule/:fileName/', requireSignIn, async (req, res) => {
/** /**
* POST endpoint for aborting a current interpretation of the run. * POST endpoint for aborting a current interpretation of the run.
*/ */
router.post('/runs/abort/:fileName/:runId', requireSignIn, async (req, res) => { router.post('/runs/abort/:id', requireSignIn, async (req, res) => {
try { try {
const run = await readFile(`./../storage/runs/${req.params.fileName}_${req.params.runId}.json`) console.log(`Params for POST /runs/abort/:id`, req.params.id)
const parsedRun = JSON.parse(run); const run = await Run.findOne({ where: { runId: req.params.id } });
if (!run) {
return res.status(404).send(false);
}
const plainRun = run.toJSON();
const browser = browserPool.getRemoteBrowser(parsedRun.browserId); const browser = browserPool.getRemoteBrowser(plainRun.browserId);
const currentLog = browser?.interpreter.debugMessages.join('/n'); const currentLog = browser?.interpreter.debugMessages.join('/n');
const serializableOutput = browser?.interpreter.serializableData.reduce((reducedObject, item, index) => { const serializableOutput = browser?.interpreter.serializableData.reduce((reducedObject, item, index) => {
return { return {
@@ -335,19 +374,21 @@ router.post('/runs/abort/:fileName/:runId', requireSignIn, async (req, res) => {
...reducedObject, ...reducedObject,
} }
}, {}); }, {});
const run_meta = { await run.update({
...parsedRun, ...run,
status: 'aborted', status: 'aborted',
finishedAt: null, finishedAt: new Date().toLocaleString(),
browserId: null, browserId: plainRun.browserId,
log: currentLog, log: currentLog,
}; serializableOutput,
binaryOutput,
});
fs.mkdirSync('../storage/runs', { recursive: true }) // fs.mkdirSync('../storage/runs', { recursive: true })
await saveFile( // await saveFile(
`../storage/runs/${parsedRun.name}_${req.params.runId}.json`, // `../storage/runs/${run.name}_${req.params.runId}.json`,
JSON.stringify({ ...run_meta, serializableOutput, binaryOutput }, null, 2) // JSON.stringify({ ...run_meta, serializableOutput, binaryOutput }, null, 2)
); // );
return res.send(true); return res.send(true);
} catch (e) { } catch (e) {
const { message } = e as Error; const { message } = e as Error;

View File

@@ -7,6 +7,7 @@ import logger from "../logger";
import { browserPool } from "../server"; import { browserPool } from "../server";
import { readFile } from "../workflow-management/storage"; import { readFile } from "../workflow-management/storage";
import { requireSignIn } from '../middlewares/auth'; import { requireSignIn } from '../middlewares/auth';
import Robot from '../models/Robot';
export const router = Router(); export const router = Router();
@@ -102,23 +103,60 @@ router.put('/pair/:index', requireSignIn, (req, res) => {
/** /**
* PUT endpoint for updating the currently generated workflow file from the one in the storage. * PUT endpoint for updating the currently generated workflow file from the one in the storage.
*/ */
router.put('/:browserId/:fileName', requireSignIn, async (req, res) => { // router.put('/:browserId/:fileName', requireSignIn, async (req, res) => {
// try {
// const browser = browserPool.getRemoteBrowser(req.params.browserId);
// logger.log('debug', `Updating workflow file`);
// if (browser && browser.generator) {
// const recording = await readFile(`./../storage/recordings/${req.params.fileName}.json`)
// const parsedRecording = JSON.parse(recording);
// if (parsedRecording.recording) {
// browser.generator?.updateWorkflowFile(parsedRecording.recording, parsedRecording.recording_meta);
// const workflowFile = browser.generator?.getWorkflowFile();
// return res.send(workflowFile);
// }
// }
// return res.send(null);
// } catch (e) {
// const { message } = e as Error;
// logger.log('info', `Error while reading a recording with name: ${req.params.fileName}.json`);
// return res.send(null);
// }
// });
router.put('/:browserId/:robotId', requireSignIn, async (req, res) => {
try { try {
const browser = browserPool.getRemoteBrowser(req.params.browserId); const browser = browserPool.getRemoteBrowser(req.params.browserId);
logger.log('debug', `Updating workflow file`); logger.log('debug', `Updating workflow for Robot ID: ${req.params.robotId}`);
if (browser && browser.generator) { if (browser && browser.generator) {
const recording = await readFile(`./../storage/recordings/${req.params.fileName}.waw.json`) const robot = await Robot.findByPk(req.params.robotId);
const parsedRecording = JSON.parse(recording);
if (parsedRecording.recording) { if (!robot) {
browser.generator?.updateWorkflowFile(parsedRecording.recording, parsedRecording.recording_meta); logger.log('info', `Robot not found with ID: ${req.params.robotId}`);
const workflowFile = browser.generator?.getWorkflowFile(); return res.status(404).send({ error: 'Robot not found' });
}
const { recording, recording_meta } = robot;
if (recording && recording.workflow) {
browser.generator.updateWorkflowFile(recording, recording_meta);
const workflowFile = browser.generator.getWorkflowFile();
return res.send(workflowFile); return res.send(workflowFile);
} else {
logger.log('info', `Invalid recording data for Robot ID: ${req.params.robotId}`);
return res.status(400).send({ error: 'Invalid recording data' });
} }
} }
return res.send(null);
logger.log('info', `Browser or generator not available for ID: ${req.params.browserId}`);
return res.status(400).send({ error: 'Browser or generator not available' });
} catch (e) { } catch (e) {
const { message } = e as Error; const { message } = e as Error;
logger.log('info', `Error while reading a recording with name: ${req.params.fileName}.waw.json`); logger.log('error', `Error while updating workflow for Robot ID: ${req.params.robotId}. Error: ${message}`);
return res.send(null); return res.status(500).send({ error: 'Internal server error' });
} }
}); });
export default router;

View File

@@ -20,9 +20,9 @@ connection.on('error', (err) => {
const workflowQueue = new Queue('workflow', { connection }); const workflowQueue = new Queue('workflow', { connection });
const worker = new Worker('workflow', async job => { const worker = new Worker('workflow', async job => {
const { fileName, runId } = job.data; const { runId, userId } = job.data;
try { try {
const result = await handleRunRecording(fileName, runId); const result = await handleRunRecording(runId, userId);
return result; return result;
} catch (error) { } catch (error) {
logger.error('Error running workflow:', error); logger.error('Error running workflow:', error);
@@ -31,11 +31,11 @@ const worker = new Worker('workflow', async job => {
}, { connection }); }, { connection });
worker.on('completed', async (job: any) => { worker.on('completed', async (job: any) => {
logger.log(`info`, `Job ${job.id} completed for ${job.data.fileName}_${job.data.runId}`); logger.log(`info`, `Job ${job.id} completed for ${job.data.runId}`);
}); });
worker.on('failed', async (job: any, err) => { worker.on('failed', async (job: any, err) => {
logger.log(`error`, `Job ${job.id} failed for ${job.data.fileName}_${job.data.runId}:`, err); logger.log(`error`, `Job ${job.id} failed for ${job.data.runId}:`, err);
}); });
console.log('Worker is running...'); console.log('Worker is running...');

View File

@@ -14,6 +14,8 @@ import {
} from "../selector"; } from "../selector";
import { CustomActions } from "../../../../src/shared/types"; import { CustomActions } from "../../../../src/shared/types";
import { workflow } from "../../routes"; import { workflow } from "../../routes";
import Robot from "../../models/Robot";
import Run from "../../models/Run";
import { saveFile } from "../storage"; import { saveFile } from "../storage";
import fs from "fs"; import fs from "fs";
import { getBestSelectorForAction } from "../utils"; import { getBestSelectorForAction } from "../utils";
@@ -486,11 +488,12 @@ export class WorkflowGenerator {
updatedAt: new Date().toLocaleString(), updatedAt: new Date().toLocaleString(),
params: this.getParams() || [], params: this.getParams() || [],
} }
fs.mkdirSync('../storage/recordings', { recursive: true }) const robot = await Robot.create({
await saveFile( recording_meta: this.recordingMeta,
`../storage/recordings/${fileName}.waw.json`, recording: recording,
JSON.stringify({ recording_meta: this.recordingMeta, recording }, null, 2) });
);
logger.log('info', `Robot saved with id: ${robot.id}`);
} }
catch (e) { catch (e) {
const { message } = e as Error; const { message } = e as Error;

View File

@@ -8,55 +8,76 @@ import logger from '../../logger';
import { browserPool } from "../../server"; import { browserPool } from "../../server";
import { googleSheetUpdateTasks, processGoogleSheetUpdates } from "../integrations/gsheet"; import { googleSheetUpdateTasks, processGoogleSheetUpdates } from "../integrations/gsheet";
import { getRecordingByFileName } from "../../routes/storage"; import { getRecordingByFileName } from "../../routes/storage";
import Robot from "../../models/Robot";
import Run from "../../models/Run";
import { getDecryptedProxyConfig } from "../../routes/proxy";
async function runWorkflow(fileName: string, runId: string) { async function runWorkflow(id: string, userId: string) {
if (!runId) { if (!id) {
runId = uuid(); id = uuid();
} }
const recording = await getRecordingByFileName(fileName); const recording = await Robot.findOne({
where: {
'recording_meta.id': id
},
raw: true
});
if (!recording || !recording.recording_meta || !recording.recording_meta.id) { if (!recording || !recording.recording_meta || !recording.recording_meta.id) {
logger.log('info', `Recording with name: ${fileName} not found`); return {
return { success: false,
success: false, error: 'Recording not found'
error: `Recording with name: ${fileName} not found`, };
}; }
}
const proxyConfig = await getDecryptedProxyConfig(userId);
let proxyOptions: any = {};
if (proxyConfig.proxy_url) {
proxyOptions = {
server: proxyConfig.proxy_url,
...(proxyConfig.proxy_username && proxyConfig.proxy_password && {
username: proxyConfig.proxy_username,
password: proxyConfig.proxy_password,
}),
};
}
try { try {
const browserId = createRemoteBrowserForRun({ const browserId = createRemoteBrowserForRun({
browser: chromium, browser: chromium,
launchOptions: { headless: true } launchOptions: {
headless: true,
proxy: proxyOptions.server ? proxyOptions : undefined,
}
}); });
const run_meta = {
const run = await Run.create({
status: 'Scheduled', status: 'Scheduled',
name: fileName, name: recording.recording_meta.name,
recordingId: recording.recording_meta.id, robotId: recording.id,
robotMetaId: recording.recording_meta.id,
startedAt: new Date().toLocaleString(), startedAt: new Date().toLocaleString(),
finishedAt: '', finishedAt: '',
browserId: browserId, browserId: id,
interpreterSettings: { maxConcurrency: 1, maxRepeats: 1, debug: true }, interpreterSettings: { maxConcurrency: 1, maxRepeats: 1, debug: true },
log: '', log: '',
runId: runId, runId: id,
}; serializableOutput: {},
binaryOutput: {},
});
fs.mkdirSync('../storage/runs', { recursive: true }); const plainRun = run.toJSON();
await saveFile(
`../storage/runs/${fileName}_${runId}.json`,
JSON.stringify(run_meta, null, 2)
);
logger.log('debug', `Scheduled run with name: ${fileName}_${runId}.json`);
return { return {
browserId, browserId,
runId runId: plainRun.runId,
} }
} catch (e) { } catch (e) {
const { message } = e as Error; const { message } = e as Error;
logger.log('info', `Error while scheduling a run with name: ${fileName}_${runId}.json`); logger.log('info', `Error while scheduling a run with id: ${id}`);
console.log(message); console.log(message);
return { return {
success: false, success: false,
@@ -65,21 +86,29 @@ async function runWorkflow(fileName: string, runId: string) {
} }
} }
async function executeRun(fileName: string, runId: string) { async function executeRun(id: string) {
try { try {
const recording = await readFile(`./../storage/recordings/${fileName}.waw.json`); const run = await Run.findOne({ where: { runId: id } });
const parsedRecording = JSON.parse(recording); if (!run) {
return {
success: false,
error: 'Run not found'
}
}
const run = await readFile(`./../storage/runs/${fileName}_${runId}.json`); const plainRun = run.toJSON();
const parsedRun = JSON.parse(run);
parsedRun.status = 'running'; const recording = await Robot.findOne({ where: { 'recording_meta.id': plainRun.robotMetaId }, raw: true });
await saveFile( if (!recording) {
`../storage/runs/${fileName}_${runId}.json`, return {
JSON.stringify(parsedRun, null, 2) success: false,
); error: 'Recording not found'
}
}
const browser = browserPool.getRemoteBrowser(parsedRun.browserId); plainRun.status = 'running';
const browser = browserPool.getRemoteBrowser(plainRun.browserId);
if (!browser) { if (!browser) {
throw new Error('Could not access browser'); throw new Error('Could not access browser');
} }
@@ -90,61 +119,47 @@ async function executeRun(fileName: string, runId: string) {
} }
const interpretationInfo = await browser.interpreter.InterpretRecording( const interpretationInfo = await browser.interpreter.InterpretRecording(
parsedRecording.recording, currentPage, parsedRun.interpreterSettings); recording.recording, currentPage, plainRun.interpreterSettings);
await destroyRemoteBrowser(parsedRun.browserId); await destroyRemoteBrowser(plainRun.browserId);
const updated_run_meta = { await run.update({
...parsedRun, ...run,
status: 'success', status: 'success',
finishedAt: new Date().toLocaleString(), finishedAt: new Date().toLocaleString(),
browserId: parsedRun.browserId, browserId: plainRun.browserId,
log: interpretationInfo.log.join('\n'), log: interpretationInfo.log.join('\n'),
serializableOutput: interpretationInfo.serializableOutput, serializableOutput: interpretationInfo.serializableOutput,
binaryOutput: interpretationInfo.binaryOutput, binaryOutput: interpretationInfo.binaryOutput,
}; });
await saveFile( googleSheetUpdateTasks[id] = {
`../storage/runs/${fileName}_${runId}.json`, name: plainRun.name,
JSON.stringify(updated_run_meta, null, 2) runId: id,
);
googleSheetUpdateTasks[runId] = {
name: parsedRun.name,
runId: runId,
status: 'pending', status: 'pending',
retries: 5, retries: 5,
}; };
processGoogleSheetUpdates(); processGoogleSheetUpdates();
return true; return true;
} catch (error: any) { } catch (error: any) {
logger.log('info', `Error while running a recording with name: ${fileName}_${runId}.json`); logger.log('info', `Error while running a recording with id: ${id} - ${error.message}`);
console.log(error.message); console.log(error.message);
const errorRun = await readFile(`./../storage/runs/${fileName}_${runId}.json`);
const parsedErrorRun = JSON.parse(errorRun);
parsedErrorRun.status = 'ERROR';
parsedErrorRun.log += `\nError: ${error.message}`;
await saveFile(
`../storage/runs/${fileName}_${runId}.json`,
JSON.stringify(parsedErrorRun, null, 2)
);
return false; return false;
} }
} }
async function readyForRunHandler(browserId: string, fileName: string, runId: string) { async function readyForRunHandler(browserId: string, id: string) {
try { try {
const interpretation = await executeRun(fileName, runId); const interpretation = await executeRun(id);
if (interpretation) { if (interpretation) {
logger.log('info', `Interpretation of ${fileName} succeeded`); logger.log('info', `Interpretation of ${id} succeeded`);
} else { } else {
logger.log('error', `Interpretation of ${fileName} failed`); logger.log('error', `Interpretation of ${id} failed`);
await destroyRemoteBrowser(browserId); await destroyRemoteBrowser(browserId);
} }
resetRecordingState(browserId, fileName, runId); resetRecordingState(browserId, id);
} catch (error: any) { } catch (error: any) {
logger.error(`Error during readyForRunHandler: ${error.message}`); logger.error(`Error during readyForRunHandler: ${error.message}`);
@@ -152,20 +167,18 @@ async function readyForRunHandler(browserId: string, fileName: string, runId: st
} }
} }
function resetRecordingState(browserId: string, fileName: string, runId: string) { function resetRecordingState(browserId: string, id: string) {
browserId = ''; browserId = '';
fileName = ''; id = '';
runId = '';
logger.log(`info`, `reset values for ${browserId}, ${fileName}, and ${runId}`);
} }
export async function handleRunRecording(fileName: string, runId: string) { export async function handleRunRecording(id: string, userId: string) {
try { try {
const result = await runWorkflow(fileName, runId); const result = await runWorkflow(id, userId);
const { browserId, runId: newRunId } = result; const { browserId, runId: newRunId } = result;
if (!browserId || !newRunId) { if (!browserId || !newRunId || !userId) {
throw new Error('browserId or runId is undefined'); throw new Error('browserId or runId or userId is undefined');
} }
const socket = io(`http://localhost:8080/${browserId}`, { const socket = io(`http://localhost:8080/${browserId}`, {
@@ -173,9 +186,9 @@ export async function handleRunRecording(fileName: string, runId: string) {
rejectUnauthorized: false rejectUnauthorized: false
}); });
socket.on('ready-for-run', () => readyForRunHandler(browserId, fileName, newRunId)); socket.on('ready-for-run', () => readyForRunHandler(browserId, newRunId));
logger.log('info', `Running recording: ${fileName}`); logger.log('info', `Running recording: ${id}`);
socket.on('disconnect', () => { socket.on('disconnect', () => {
cleanupSocketListeners(socket, browserId, newRunId); cleanupSocketListeners(socket, browserId, newRunId);
@@ -186,9 +199,9 @@ export async function handleRunRecording(fileName: string, runId: string) {
} }
} }
function cleanupSocketListeners(socket: Socket, browserId: string, runId: string) { function cleanupSocketListeners(socket: Socket, browserId: string, id: string) {
socket.off('ready-for-run', () => readyForRunHandler(browserId, '', runId)); socket.off('ready-for-run', () => readyForRunHandler(browserId, id));
logger.log('info', `Cleaned up listeners for browserId: ${browserId}, runId: ${runId}`); logger.log('info', `Cleaned up listeners for browserId: ${browserId}, runId: ${id}`);
} }
export { runWorkflow }; export { runWorkflow };

View File

@@ -32,13 +32,13 @@ export const getStoredRuns = async (): Promise<string[] | null> => {
} }
}; };
export const deleteRecordingFromStorage = async (fileName: string): Promise<boolean> => { export const deleteRecordingFromStorage = async (id: string): Promise<boolean> => {
try { try {
const response = await axios.delete(`http://localhost:8080/storage/recordings/${fileName}`); const response = await axios.delete(`http://localhost:8080/storage/recordings/${id}`);
if (response.status === 200) { if (response.status === 200) {
return response.data; return response.data;
} else { } else {
throw new Error(`Couldn't delete stored recording ${fileName}`); throw new Error(`Couldn't delete stored recording ${id}`);
} }
} catch(error: any) { } catch(error: any) {
console.log(error); console.log(error);
@@ -46,13 +46,13 @@ export const deleteRecordingFromStorage = async (fileName: string): Promise<bool
} }
}; };
export const deleteRunFromStorage = async (fileName: string): Promise<boolean> => { export const deleteRunFromStorage = async (id: string): Promise<boolean> => {
try { try {
const response = await axios.delete(`http://localhost:8080/storage/runs/${fileName}`); const response = await axios.delete(`http://localhost:8080/storage/runs/${id}`);
if (response.status === 200) { if (response.status === 200) {
return response.data; return response.data;
} else { } else {
throw new Error(`Couldn't delete stored recording ${fileName}`); throw new Error(`Couldn't delete stored recording ${id}`);
} }
} catch(error: any) { } catch(error: any) {
console.log(error); console.log(error);
@@ -60,13 +60,13 @@ export const deleteRunFromStorage = async (fileName: string): Promise<boolean> =
} }
}; };
export const editRecordingFromStorage = async (browserId: string, fileName: string): Promise<WorkflowFile | null> => { export const editRecordingFromStorage = async (browserId: string, robotId: string): Promise<WorkflowFile | null> => {
try { try {
const response = await axios.put(`http://localhost:8080/workflow/${browserId}/${fileName}`); const response = await axios.put(`http://localhost:8080/workflow/${browserId}/${robotId}`);
if (response.status === 200) { if (response.status === 200) {
return response.data; return response.data;
} else { } else {
throw new Error(`Couldn't edit stored recording ${fileName}`); throw new Error(`Couldn't edit stored recording ${robotId}`);
} }
} catch(error: any) { } catch(error: any) {
console.log(error); console.log(error);
@@ -74,15 +74,15 @@ export const editRecordingFromStorage = async (browserId: string, fileName: stri
} }
}; };
export const createRunForStoredRecording = async (fileName: string, settings: RunSettings): Promise<CreateRunResponse> => { export const createRunForStoredRecording = async (id: string, settings: RunSettings): Promise<CreateRunResponse> => {
try { try {
const response = await axios.put( const response = await axios.put(
`http://localhost:8080/storage/runs/${fileName}`, `http://localhost:8080/storage/runs/${id}`,
{...settings}); {...settings});
if (response.status === 200) { if (response.status === 200) {
return response.data; return response.data;
} else { } else {
throw new Error(`Couldn't create a run for a recording ${fileName}`); throw new Error(`Couldn't create a run for a recording ${id}`);
} }
} catch(error: any) { } catch(error: any) {
console.log(error); console.log(error);
@@ -90,13 +90,13 @@ export const createRunForStoredRecording = async (fileName: string, settings: Ru
} }
} }
export const interpretStoredRecording = async (fileName: string, runId: string): Promise<boolean> => { export const interpretStoredRecording = async (id: string): Promise<boolean> => {
try { try {
const response = await axios.post(`http://localhost:8080/storage/runs/run/${fileName}/${runId}`); const response = await axios.post(`http://localhost:8080/storage/runs/run/${id}`);
if (response.status === 200) { if (response.status === 200) {
return response.data; return response.data;
} else { } else {
throw new Error(`Couldn't run a recording ${fileName}`); throw new Error(`Couldn't run a recording ${id}`);
} }
} catch(error: any) { } catch(error: any) {
console.log(error); console.log(error);
@@ -104,13 +104,13 @@ export const interpretStoredRecording = async (fileName: string, runId: string):
} }
} }
export const notifyAboutAbort = async (fileName: string, runId:string): Promise<boolean> => { export const notifyAboutAbort = async (id:string): Promise<boolean> => {
try { try {
const response = await axios.post(`http://localhost:8080/storage/runs/abort/${fileName}/${runId}`); const response = await axios.post(`http://localhost:8080/storage/runs/abort/${id}`);
if (response.status === 200) { if (response.status === 200) {
return response.data; return response.data;
} else { } else {
throw new Error(`Couldn't abort a running recording ${fileName} with id ${runId}`); throw new Error(`Couldn't abort a running recording with id ${id}`);
} }
} catch(error: any) { } catch(error: any) {
console.log(error); console.log(error);

View File

@@ -65,7 +65,7 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRun
return ( return (
<TableCell key={column.id} align={column.align}> <TableCell key={column.id} align={column.align}>
<IconButton aria-label="add" size= "small" onClick={() => { <IconButton aria-label="add" size= "small" onClick={() => {
deleteRunFromStorage(`${row.name}_${row.runId}`).then((result: boolean) => { deleteRunFromStorage(`${row.runId}`).then((result: boolean) => {
if (result) { if (result) {
handleDelete(); handleDelete();
} }

View File

@@ -16,7 +16,7 @@ import { useGlobalInfoStore } from "../../context/globalInfo";
import { deleteRecordingFromStorage, getStoredRecordings } from "../../api/storage"; import { deleteRecordingFromStorage, getStoredRecordings } from "../../api/storage";
interface Column { interface Column {
id: 'interpret' | 'name' | 'createdAt' | 'edit' | 'updatedAt' | 'delete' | 'schedule' | 'integrate'; id: 'id' | 'interpret' | 'name' | 'createdAt' | 'edit' | 'updatedAt' | 'delete' | 'schedule' | 'integrate';
label: string; label: string;
minWidth?: number; minWidth?: number;
align?: 'right'; align?: 'right';
@@ -24,6 +24,7 @@ interface Column {
} }
const columns: readonly Column[] = [ const columns: readonly Column[] = [
{ id: 'id', label: 'ID', minWidth: 80 },
{ id: 'interpret', label: 'Run', minWidth: 80 }, { id: 'interpret', label: 'Run', minWidth: 80 },
{ id: 'name', label: 'Name', minWidth: 80 }, { id: 'name', label: 'Name', minWidth: 80 },
{ {
@@ -61,7 +62,7 @@ const columns: readonly Column[] = [
]; ];
interface Data { interface Data {
id: number; id: string;
name: string; name: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
@@ -70,10 +71,10 @@ interface Data {
} }
interface RecordingsTableProps { interface RecordingsTableProps {
handleEditRecording: (fileName: string) => void; handleEditRecording: (id: string, fileName: string) => void;
handleRunRecording: (fileName: string, params: string[]) => void; handleRunRecording: (id: string,fileName: string, params: string[]) => void;
handleScheduleRecording: (fileName: string, params: string[]) => void; handleScheduleRecording: (id: string,fileName: string, params: string[]) => void;
handleIntegrateRecording: (fileName: string, params: string[]) => void; handleIntegrateRecording: (id: string,fileName: string, params: string[]) => void;
} }
export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handleScheduleRecording, handleIntegrateRecording }: RecordingsTableProps) => { export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handleScheduleRecording, handleIntegrateRecording }: RecordingsTableProps) => {
@@ -96,13 +97,12 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
const recordings = await getStoredRecordings(); const recordings = await getStoredRecordings();
if (recordings) { if (recordings) {
const parsedRows: Data[] = []; const parsedRows: Data[] = [];
recordings.map((recording, index) => { recordings.map((recording: any, index: number) => {
const parsedRecording = JSON.parse(recording); if (recording && recording.recording_meta) {
if (parsedRecording.recording_meta) {
parsedRows.push({ parsedRows.push({
id: index, id: index,
...parsedRecording.recording_meta, ...recording.recording_meta,
content: parsedRecording.recording content: recording.recording
}); });
} }
}); });
@@ -156,14 +156,14 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
case 'interpret': case 'interpret':
return ( return (
<TableCell key={column.id} align={column.align}> <TableCell key={column.id} align={column.align}>
<InterpretButton handleInterpret={() => handleRunRecording(row.name, row.params || [])} /> <InterpretButton handleInterpret={() => handleRunRecording(row.id, row.name, row.params || [])} />
</TableCell> </TableCell>
); );
case 'edit': case 'edit':
return ( return (
<TableCell key={column.id} align={column.align}> <TableCell key={column.id} align={column.align}>
<IconButton aria-label="add" size="small" onClick={() => { <IconButton aria-label="add" size="small" onClick={() => {
handleEditRecording(row.name); handleEditRecording(row.id, row.name);
}} sx={{ '&:hover': { color: '#1976d2', backgroundColor: 'transparent' } }}> }} sx={{ '&:hover': { color: '#1976d2', backgroundColor: 'transparent' } }}>
<Edit /> <Edit />
</IconButton> </IconButton>
@@ -172,20 +172,20 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
case 'schedule': case 'schedule':
return ( return (
<TableCell key={column.id} align={column.align}> <TableCell key={column.id} align={column.align}>
<ScheduleButton handleSchedule={() => handleScheduleRecording(row.name, row.params || [])} /> <ScheduleButton handleSchedule={() => handleScheduleRecording(row.id, row.name, row.params || [])} />
</TableCell> </TableCell>
); );
case 'integrate': case 'integrate':
return ( return (
<TableCell key={column.id} align={column.align}> <TableCell key={column.id} align={column.align}>
<IntegrateButton handleIntegrate={() => handleIntegrateRecording(row.name, row.params || [])} /> <IntegrateButton handleIntegrate={() => handleIntegrateRecording(row.id, row.name, row.params || [])} />
</TableCell> </TableCell>
); );
case 'delete': case 'delete':
return ( return (
<TableCell key={column.id} align={column.align}> <TableCell key={column.id} align={column.align}>
<IconButton aria-label="add" size="small" onClick={() => { <IconButton aria-label="add" size="small" onClick={() => {
deleteRecordingFromStorage(row.name).then((result: boolean) => { deleteRecordingFromStorage(row.id).then((result: boolean) => {
if (result) { if (result) {
setRows([]); setRows([]);
notify('success', 'Recording deleted successfully'); notify('success', 'Recording deleted successfully');

View File

@@ -77,11 +77,11 @@ export const RunsTable = (
const runs = await getStoredRuns(); const runs = await getStoredRuns();
if (runs) { if (runs) {
const parsedRows: Data[] = []; const parsedRows: Data[] = [];
runs.map((run, index) => { runs.map((run: any, index) => {
const parsedRun = JSON.parse(run); // const run = JSON.parse(run);
parsedRows.push({ parsedRows.push({
id: index, id: index,
...parsedRun, ...run,
}); });
}); });
setRows(parsedRows); setRows(parsedRows);

View File

@@ -6,68 +6,78 @@ import { ScheduleSettings, ScheduleSettingsModal } from "../molecules/ScheduleSe
import { IntegrationSettings, IntegrationSettingsModal } from "../molecules/IntegrationSettings"; import { IntegrationSettings, IntegrationSettingsModal } from "../molecules/IntegrationSettings";
interface RecordingsProps { interface RecordingsProps {
handleEditRecording: (fileName: string) => void; handleEditRecording: (id: string, fileName: string) => void;
handleRunRecording: (settings: RunSettings) => void; handleRunRecording: (settings: RunSettings) => void;
handleScheduleRecording: (settings: ScheduleSettings) => void; handleScheduleRecording: (settings: ScheduleSettings) => void;
handleIntegrateRecording: (settings: IntegrationSettings) => void; handleIntegrateRecording: (settings: IntegrationSettings) => void;
setFileName: (fileName: string) => void; setRecordingInfo: (id: string, name: string) => void;
} }
export const Recordings = ({ handleEditRecording, handleRunRecording, setFileName, handleScheduleRecording, handleIntegrateRecording }: RecordingsProps) => { export const Recordings = ({ handleEditRecording, handleRunRecording, setRecordingInfo, handleScheduleRecording, handleIntegrateRecording }: RecordingsProps) => {
const [runSettingsAreOpen, setRunSettingsAreOpen] = useState(false); const [runSettingsAreOpen, setRunSettingsAreOpen] = useState(false);
const [scheduleSettingsAreOpen, setScheduleSettingsAreOpen] = useState(false); const [scheduleSettingsAreOpen, setScheduleSettingsAreOpen] = useState(false);
const [integrateSettingsAreOpen, setIntegrateSettingsAreOpen] = useState(false); const [integrateSettingsAreOpen, setIntegrateSettingsAreOpen] = useState(false);
const [params, setParams] = useState<string[]>([]); const [params, setParams] = useState<string[]>([]);
const [selectedRecordingId, setSelectedRecordingId] = useState<string>('');
const handleSettingsAndIntegrate = (fileName: string, params: string[]) => { const handleSettingsAndIntegrate = (id: string, name: string, params: string[]) => {
if (params.length === 0) { if (params.length === 0) {
setIntegrateSettingsAreOpen(true); setIntegrateSettingsAreOpen(true);
setFileName(fileName); setRecordingInfo(id, name);
setSelectedRecordingId(id);
} else { } else {
setParams(params); setParams(params);
setIntegrateSettingsAreOpen(true); setIntegrateSettingsAreOpen(true);
setFileName(fileName); setRecordingInfo(id, name);
setSelectedRecordingId(id);
} }
} }
const handleSettingsAndRun = (fileName: string, params: string[]) => { const handleSettingsAndRun = (id: string, name: string, params: string[]) => {
if (params.length === 0) { if (params.length === 0) {
setRunSettingsAreOpen(true); setRunSettingsAreOpen(true);
setFileName(fileName); setRecordingInfo(id, name);
setSelectedRecordingId(id);
} else { } else {
setParams(params); setParams(params);
setRunSettingsAreOpen(true); setRunSettingsAreOpen(true);
setFileName(fileName); setRecordingInfo(id, name);
setSelectedRecordingId(id);
} }
} }
const handleSettingsAndSchedule = (fileName: string, params: string[]) => { const handleSettingsAndSchedule = (id: string, name: string, params: string[]) => {
if (params.length === 0) { if (params.length === 0) {
setScheduleSettingsAreOpen(true); setScheduleSettingsAreOpen(true);
setFileName(fileName); setRecordingInfo(id, name);
setSelectedRecordingId(id);
} else { } else {
setParams(params); setParams(params);
setScheduleSettingsAreOpen(true); setScheduleSettingsAreOpen(true);
setFileName(fileName); setRecordingInfo(id, name);
setSelectedRecordingId(id);
} }
} }
const handleClose = () => { const handleClose = () => {
setParams([]); setParams([]);
setRunSettingsAreOpen(false); setRunSettingsAreOpen(false);
setFileName(''); setRecordingInfo('', '');
setSelectedRecordingId('');
} }
const handleIntegrateClose = () => { const handleIntegrateClose = () => {
setParams([]); setParams([]);
setIntegrateSettingsAreOpen(false); setIntegrateSettingsAreOpen(false);
setFileName(''); setRecordingInfo('', '');
setSelectedRecordingId('');
} }
const handleScheduleClose = () => { const handleScheduleClose = () => {
setParams([]); setParams([]);
setScheduleSettingsAreOpen(false); setScheduleSettingsAreOpen(false);
setFileName(''); setRecordingInfo('', '');
setSelectedRecordingId('');
} }
return ( return (
@@ -98,4 +108,4 @@ export const Recordings = ({ handleEditRecording, handleRunRecording, setFileNam
</Grid> </Grid>
</React.Fragment> </React.Fragment>
); );
} }

View File

@@ -16,6 +16,8 @@ interface GlobalInfo {
setRerenderRuns: (rerenderRuns: boolean) => void; setRerenderRuns: (rerenderRuns: boolean) => void;
recordingLength: number; recordingLength: number;
setRecordingLength: (recordingLength: number) => void; setRecordingLength: (recordingLength: number) => void;
recordingId: string | null;
setRecordingId: (newId: string | null) => void;
recordingName: string; recordingName: string;
setRecordingName: (recordingName: string) => void; setRecordingName: (recordingName: string) => void;
}; };
@@ -29,6 +31,7 @@ class GlobalInfoStore implements Partial<GlobalInfo> {
message: '', message: '',
isOpen: false, isOpen: false,
}; };
recordingId = null;
recordings: string[] = []; recordings: string[] = [];
rerenderRuns = false; rerenderRuns = false;
recordingName = ''; recordingName = '';
@@ -46,6 +49,7 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => {
const [recordings, setRecordings] = useState<string[]>(globalInfoStore.recordings); const [recordings, setRecordings] = useState<string[]>(globalInfoStore.recordings);
const [rerenderRuns, setRerenderRuns] = useState<boolean>(globalInfoStore.rerenderRuns); const [rerenderRuns, setRerenderRuns] = useState<boolean>(globalInfoStore.rerenderRuns);
const [recordingLength, setRecordingLength] = useState<number>(globalInfoStore.recordingLength); const [recordingLength, setRecordingLength] = useState<number>(globalInfoStore.recordingLength);
const [recordingId, setRecordingId] = useState<string | null>(globalInfoStore.recordingId);
const [recordingName, setRecordingName] = useState<string>(globalInfoStore.recordingName); const [recordingName, setRecordingName] = useState<string>(globalInfoStore.recordingName);
const notify = (severity: 'error' | 'warning' | 'info' | 'success', message: string) => { const notify = (severity: 'error' | 'warning' | 'info' | 'success', message: string) => {
@@ -79,6 +83,8 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => {
setRerenderRuns, setRerenderRuns,
recordingLength, recordingLength,
setRecordingLength, setRecordingLength,
recordingId,
setRecordingId,
recordingName, recordingName,
setRecordingName setRecordingName
}} }}

View File

@@ -15,7 +15,7 @@ import { ScheduleSettings } from "../components/molecules/ScheduleSettings";
import { IntegrationSettings } from "../components/molecules/IntegrationSettings"; import { IntegrationSettings } from "../components/molecules/IntegrationSettings";
interface MainPageProps { interface MainPageProps {
handleEditRecording: (fileName: string) => void; handleEditRecording: (id: string, fileName: string) => void;
} }
export interface CreateRunResponse { export interface CreateRunResponse {
@@ -32,6 +32,7 @@ export const MainPage = ({ handleEditRecording }: MainPageProps) => {
const [content, setContent] = React.useState('recordings'); const [content, setContent] = React.useState('recordings');
const [sockets, setSockets] = React.useState<Socket[]>([]); const [sockets, setSockets] = React.useState<Socket[]>([]);
const [runningRecordingId, setRunningRecordingId] = React.useState('');
const [runningRecordingName, setRunningRecordingName] = React.useState(''); const [runningRecordingName, setRunningRecordingName] = React.useState('');
const [currentInterpretationLog, setCurrentInterpretationLog] = React.useState(''); const [currentInterpretationLog, setCurrentInterpretationLog] = React.useState('');
const [ids, setIds] = React.useState<CreateRunResponse>({ const [ids, setIds] = React.useState<CreateRunResponse>({
@@ -45,7 +46,7 @@ export const MainPage = ({ handleEditRecording }: MainPageProps) => {
const abortRunHandler = (runId: string) => { const abortRunHandler = (runId: string) => {
aborted = true; aborted = true;
notifyAboutAbort(runningRecordingName, runId).then(async (response) => { notifyAboutAbort(runId).then(async (response) => {
if (response) { if (response) {
notify('success', `Interpretation of ${runningRecordingName} aborted successfully`); notify('success', `Interpretation of ${runningRecordingName} aborted successfully`);
await stopRecording(ids.browserId); await stopRecording(ids.browserId);
@@ -55,12 +56,13 @@ export const MainPage = ({ handleEditRecording }: MainPageProps) => {
}) })
} }
const setFileName = (fileName: string) => { const setRecordingInfo = (id: string, name: string) => {
setRunningRecordingName(fileName); setRunningRecordingId(id);
setRunningRecordingName(name);
} }
const readyForRunHandler = useCallback((browserId: string, runId: string) => { const readyForRunHandler = useCallback((browserId: string, runId: string) => {
interpretStoredRecording(runningRecordingName, runId).then(async (interpretation: boolean) => { interpretStoredRecording(runId).then(async (interpretation: boolean) => {
if (!aborted) { if (!aborted) {
if (interpretation) { if (interpretation) {
notify('success', `Interpretation of ${runningRecordingName} succeeded`); notify('success', `Interpretation of ${runningRecordingName} succeeded`);
@@ -82,7 +84,7 @@ export const MainPage = ({ handleEditRecording }: MainPageProps) => {
}, [currentInterpretationLog]) }, [currentInterpretationLog])
const handleRunRecording = useCallback((settings: RunSettings) => { const handleRunRecording = useCallback((settings: RunSettings) => {
createRunForStoredRecording(runningRecordingName, settings).then(({ browserId, runId }: CreateRunResponse) => { createRunForStoredRecording(runningRecordingId, settings).then(({ browserId, runId }: CreateRunResponse) => {
setIds({ browserId, runId }); setIds({ browserId, runId });
const socket = const socket =
io(`http://localhost:8080/${browserId}`, { io(`http://localhost:8080/${browserId}`, {
@@ -98,7 +100,7 @@ export const MainPage = ({ handleEditRecording }: MainPageProps) => {
} else { } else {
notify('error', `Failed to run recording: ${runningRecordingName}`); notify('error', `Failed to run recording: ${runningRecordingName}`);
} }
}); })
return (socket: Socket, browserId: string, runId: string) => { return (socket: Socket, browserId: string, runId: string) => {
socket.off('ready-for-run', () => readyForRunHandler(browserId, runId)); socket.off('ready-for-run', () => readyForRunHandler(browserId, runId));
socket.off('debugMessage', debugMessageHandler); socket.off('debugMessage', debugMessageHandler);
@@ -133,7 +135,7 @@ export const MainPage = ({ handleEditRecording }: MainPageProps) => {
return <Recordings return <Recordings
handleEditRecording={handleEditRecording} handleEditRecording={handleEditRecording}
handleRunRecording={handleRunRecording} handleRunRecording={handleRunRecording}
setFileName={setFileName} setRecordingInfo={setRecordingInfo}
handleScheduleRecording={handleScheduleRecording} handleScheduleRecording={handleScheduleRecording}
handleIntegrateRecording={handleIntegrateRecording} handleIntegrateRecording={handleIntegrateRecording}
/>; />;

View File

@@ -18,20 +18,20 @@ export const PageWrapper = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { browserId, setBrowserId, notification, recordingName, setRecordingName } = useGlobalInfoStore(); const { browserId, setBrowserId, notification, recordingName, setRecordingName, recordingId, setRecordingId } = useGlobalInfoStore();
const handleNewRecording = () => { const handleNewRecording = () => {
setBrowserId('new-recording'); setBrowserId('new-recording');
setRecordingName(''); setRecordingName('');
setRecordingId('');
navigate('/recording'); navigate('/recording');
} }
const handleEditRecording = (fileName: string) => { const handleEditRecording = (recordingId: string, fileName: string) => {
setRecordingName(fileName); setRecordingName(fileName);
setRecordingId(recordingId);
setBrowserId('new-recording'); setBrowserId('new-recording');
navigate('/recording'); navigate('/recording');
} }
const isNotification = (): boolean => { const isNotification = (): boolean => {