Merge pull request #82 from amhsirak/develop
feat: more scheduler options
This commit is contained in:
@@ -30,6 +30,8 @@ export class RemoteBrowser {
|
|||||||
*/
|
*/
|
||||||
private browser: Browser | null = null;
|
private browser: Browser | null = null;
|
||||||
|
|
||||||
|
private context: BrowserContext | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Playwright's [CDPSession](https://playwright.dev/docs/api/class-cdpsession) instance,
|
* The Playwright's [CDPSession](https://playwright.dev/docs/api/class-cdpsession) instance,
|
||||||
* used to talk raw Chrome Devtools Protocol.
|
* used to talk raw Chrome Devtools Protocol.
|
||||||
@@ -90,13 +92,13 @@ export class RemoteBrowser {
|
|||||||
*/
|
*/
|
||||||
public initialize = async (options: RemoteBrowserOptions): Promise<void> => {
|
public initialize = async (options: RemoteBrowserOptions): Promise<void> => {
|
||||||
this.browser = <Browser>(await options.browser.launch(options.launchOptions));
|
this.browser = <Browser>(await options.browser.launch(options.launchOptions));
|
||||||
const context = await this.browser.newContext(
|
this.context = await this.browser.newContext(
|
||||||
{
|
{
|
||||||
viewport: { height: 400, width: 900 },
|
viewport: { height: 400, width: 900 },
|
||||||
// recordVideo: { dir: 'videos/' }
|
// recordVideo: { dir: 'videos/' }
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.currentPage = await context.newPage();
|
this.currentPage = await this.context.newPage();
|
||||||
const blocker = await PlaywrightBlocker.fromPrebuiltAdsAndTracking(fetch);
|
const blocker = await PlaywrightBlocker.fromPrebuiltAdsAndTracking(fetch);
|
||||||
await blocker.enableBlockingInPage(this.currentPage);
|
await blocker.enableBlockingInPage(this.currentPage);
|
||||||
this.client = await this.currentPage.context().newCDPSession(this.currentPage);
|
this.client = await this.currentPage.context().newCDPSession(this.currentPage);
|
||||||
@@ -138,6 +140,16 @@ export class RemoteBrowser {
|
|||||||
logger.log('error', `${tabInfo.index} index out of range of pages`)
|
logger.log('error', `${tabInfo.index} index out of range of pages`)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.socket.on('setViewportSize', async (data: { width: number, height: number }) => {
|
||||||
|
const { width, height } = data;
|
||||||
|
logger.log('debug', `Received viewport size: width=${width}, height=${height}`);
|
||||||
|
|
||||||
|
// Update the browser context's viewport dynamically
|
||||||
|
if (this.context && this.browser) {
|
||||||
|
this.context = await this.browser.newContext({ viewport: { width, height } });
|
||||||
|
logger.log('debug', `Viewport size updated to width=${width}, height=${height} for the entire browser context`);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -280,7 +292,7 @@ export class RemoteBrowser {
|
|||||||
if (page) {
|
if (page) {
|
||||||
await this.stopScreencast();
|
await this.stopScreencast();
|
||||||
this.currentPage = page;
|
this.currentPage = page;
|
||||||
await this.currentPage.setViewportSize({ height: 400, width: 900 })
|
//await this.currentPage.setViewportSize({ height: 400, width: 900 })
|
||||||
this.client = await this.currentPage.context().newCDPSession(this.currentPage);
|
this.client = await this.currentPage.context().newCDPSession(this.currentPage);
|
||||||
this.socket.emit('urlChanged', this.currentPage.url());
|
this.socket.emit('urlChanged', this.currentPage.url());
|
||||||
await this.makeAndEmitScreenshot();
|
await this.makeAndEmitScreenshot();
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { Model, DataTypes, Optional } from 'sequelize';
|
import { Model, DataTypes, Optional } from 'sequelize';
|
||||||
import sequelize from '../storage/db';
|
import sequelize from '../storage/db';
|
||||||
import { WorkflowFile, Where, What, WhereWhatPair } from 'maxun-core';
|
import { WorkflowFile, Where, What, WhereWhatPair } from 'maxun-core';
|
||||||
import User from './User'; // Import User model
|
|
||||||
import Run from './Run';
|
|
||||||
|
|
||||||
interface RobotMeta {
|
interface RobotMeta {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -27,6 +25,19 @@ interface RobotAttributes {
|
|||||||
google_sheet_id?: string | null;
|
google_sheet_id?: string | null;
|
||||||
google_access_token?: string | null;
|
google_access_token?: string | null;
|
||||||
google_refresh_token?: string | null;
|
google_refresh_token?: string | null;
|
||||||
|
schedule?: ScheduleConfig | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScheduleConfig {
|
||||||
|
runEvery: number;
|
||||||
|
runEveryUnit: 'MINUTES' | 'HOURS' | 'DAYS' | 'WEEKS' | 'MONTHS';
|
||||||
|
startFrom: 'SUNDAY' | 'MONDAY' | 'TUESDAY' | 'WEDNESDAY' | 'THURSDAY' | 'FRIDAY' | 'SATURDAY';
|
||||||
|
atTimeStart?: string;
|
||||||
|
atTimeEnd?: string;
|
||||||
|
timezone: string;
|
||||||
|
lastRunAt?: Date;
|
||||||
|
nextRunAt?: Date;
|
||||||
|
cronExpression?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RobotCreationAttributes extends Optional<RobotAttributes, 'id'> { }
|
interface RobotCreationAttributes extends Optional<RobotAttributes, 'id'> { }
|
||||||
@@ -41,6 +52,7 @@ class Robot extends Model<RobotAttributes, RobotCreationAttributes> implements R
|
|||||||
public google_sheet_id?: string | null;
|
public google_sheet_id?: string | null;
|
||||||
public google_access_token!: string | null;
|
public google_access_token!: string | null;
|
||||||
public google_refresh_token!: string | null;
|
public google_refresh_token!: string | null;
|
||||||
|
public schedule!: ScheduleConfig | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Robot.init(
|
Robot.init(
|
||||||
@@ -82,6 +94,10 @@ Robot.init(
|
|||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
|
schedule: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sequelize,
|
sequelize,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { requireSignIn } from '../middlewares/auth';
|
|||||||
import Robot from '../models/Robot';
|
import Robot from '../models/Robot';
|
||||||
import Run from '../models/Run';
|
import Run from '../models/Run';
|
||||||
import { BinaryOutputService } from '../storage/mino';
|
import { BinaryOutputService } from '../storage/mino';
|
||||||
// import { workflowQueue } from '../worker';
|
import { workflowQueue } from '../worker';
|
||||||
|
|
||||||
export const router = Router();
|
export const router = Router();
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ router.get('/recordings/:id', requireSignIn, async (req, res) => {
|
|||||||
where: { 'recording_meta.id': req.params.id },
|
where: { 'recording_meta.id': req.params.id },
|
||||||
raw: true
|
raw: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
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');
|
||||||
@@ -208,8 +208,8 @@ router.post('/runs/run/:id', requireSignIn, async (req, res) => {
|
|||||||
if (browser && currentPage) {
|
if (browser && currentPage) {
|
||||||
const interpretationInfo = await browser.interpreter.InterpretRecording(
|
const interpretationInfo = await browser.interpreter.InterpretRecording(
|
||||||
recording.recording, currentPage, plainRun.interpreterSettings);
|
recording.recording, currentPage, plainRun.interpreterSettings);
|
||||||
const binaryOutputService = new BinaryOutputService('maxun-run-screenshots');
|
const binaryOutputService = new BinaryOutputService('maxun-run-screenshots');
|
||||||
const uploadedBinaryOutput = await binaryOutputService.uploadAndStoreBinaryOutput(run, interpretationInfo.binaryOutput);
|
const uploadedBinaryOutput = await binaryOutputService.uploadAndStoreBinaryOutput(run, interpretationInfo.binaryOutput);
|
||||||
await destroyRemoteBrowser(plainRun.browserId);
|
await destroyRemoteBrowser(plainRun.browserId);
|
||||||
await run.update({
|
await run.update({
|
||||||
...run,
|
...run,
|
||||||
@@ -247,18 +247,45 @@ router.put('/schedule/:id/', requireSignIn, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const {
|
const {
|
||||||
|
// enabled = true,
|
||||||
runEvery,
|
runEvery,
|
||||||
runEveryUnit,
|
runEveryUnit,
|
||||||
startFrom,
|
startFrom,
|
||||||
atTime,
|
atTimeStart,
|
||||||
|
atTimeEnd,
|
||||||
timezone
|
timezone
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
if (!id || !runEvery || !runEveryUnit || !startFrom || !atTime || !timezone) {
|
const robot = await Robot.findOne({ where: { 'recording_meta.id': id } });
|
||||||
|
if (!robot) {
|
||||||
|
return res.status(404).json({ error: 'Robot not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// If disabled, remove scheduling
|
||||||
|
// if (!enabled) {
|
||||||
|
// // Remove existing job from queue if it exists
|
||||||
|
// const existingJobs = await workflowQueue.getJobs(['delayed', 'waiting']);
|
||||||
|
// for (const job of existingJobs) {
|
||||||
|
// if (job.data.id === id) {
|
||||||
|
// await job.remove();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Update robot to disable scheduling
|
||||||
|
// await robot.update({
|
||||||
|
// schedule: null
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return res.status(200).json({
|
||||||
|
// message: 'Schedule disabled successfully'
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (!id || !runEvery || !runEveryUnit || !startFrom || !timezone || (runEveryUnit === 'HOURS' || runEveryUnit === 'MINUTES') && (!atTimeStart || !atTimeEnd)) {
|
||||||
return res.status(400).json({ error: 'Missing required parameters' });
|
return res.status(400).json({ error: 'Missing required parameters' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!['HOURS', 'DAYS', 'WEEKS', 'MONTHS'].includes(runEveryUnit)) {
|
if (!['HOURS', 'DAYS', 'WEEKS', 'MONTHS', 'MINUTES'].includes(runEveryUnit)) {
|
||||||
return res.status(400).json({ error: 'Invalid runEvery unit' });
|
return res.status(400).json({ error: 'Invalid runEvery unit' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,8 +293,12 @@ router.put('/schedule/:id/', requireSignIn, async (req, res) => {
|
|||||||
return res.status(400).json({ error: 'Invalid timezone' });
|
return res.status(400).json({ error: 'Invalid timezone' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const [hours, minutes] = atTime.split(':').map(Number);
|
const [startHours, startMinutes] = atTimeStart.split(':').map(Number);
|
||||||
if (isNaN(hours) || isNaN(minutes) || hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
|
const [endHours, endMinutes] = atTimeEnd.split(':').map(Number);
|
||||||
|
|
||||||
|
if (isNaN(startHours) || isNaN(startMinutes) || isNaN(endHours) || isNaN(endMinutes) ||
|
||||||
|
startHours < 0 || startHours > 23 || startMinutes < 0 || startMinutes > 59 ||
|
||||||
|
endHours < 0 || endHours > 23 || endMinutes < 0 || endMinutes > 59) {
|
||||||
return res.status(400).json({ error: 'Invalid time format' });
|
return res.status(400).json({ error: 'Invalid time format' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,18 +309,19 @@ router.put('/schedule/:id/', requireSignIn, async (req, res) => {
|
|||||||
|
|
||||||
let cronExpression;
|
let cronExpression;
|
||||||
switch (runEveryUnit) {
|
switch (runEveryUnit) {
|
||||||
|
case 'MINUTES':
|
||||||
case 'HOURS':
|
case 'HOURS':
|
||||||
cronExpression = `${minutes} */${runEvery} * * *`;
|
cronExpression = `${startMinutes}-${endMinutes} */${runEvery} * * *`;
|
||||||
break;
|
break;
|
||||||
case 'DAYS':
|
case 'DAYS':
|
||||||
cronExpression = `${minutes} ${hours} */${runEvery} * *`;
|
cronExpression = `${startMinutes} ${startHours} */${runEvery} * *`;
|
||||||
break;
|
break;
|
||||||
case 'WEEKS':
|
case 'WEEKS':
|
||||||
const dayIndex = days.indexOf(startFrom);
|
const dayIndex = days.indexOf(startFrom);
|
||||||
cronExpression = `${minutes} ${hours} * * ${dayIndex}/${7 * runEvery}`;
|
cronExpression = `${startMinutes} ${startHours} * * ${dayIndex}/${7 * runEvery}`;
|
||||||
break;
|
break;
|
||||||
case 'MONTHS':
|
case 'MONTHS':
|
||||||
cronExpression = `${minutes} ${hours} 1-7 */${runEvery} *`;
|
cronExpression = `${startMinutes} ${startHours} 1-7 */${runEvery} *`;
|
||||||
if (startFrom !== 'SUNDAY') {
|
if (startFrom !== 'SUNDAY') {
|
||||||
const dayIndex = days.indexOf(startFrom);
|
const dayIndex = days.indexOf(startFrom);
|
||||||
cronExpression += ` ${dayIndex}`;
|
cronExpression += ` ${dayIndex}`;
|
||||||
@@ -304,22 +336,50 @@ router.put('/schedule/:id/', requireSignIn, async (req, res) => {
|
|||||||
const runId = uuid();
|
const runId = uuid();
|
||||||
const userId = req.user.id;
|
const userId = req.user.id;
|
||||||
|
|
||||||
// await workflowQueue.add(
|
// Remove existing jobs for this robot just in case some were left
|
||||||
// 'run workflow',
|
// const existingJobs = await workflowQueue.getJobs(['delayed', 'waiting']);
|
||||||
// { id, runId, userId },
|
// for (const job of existingJobs) {
|
||||||
// {
|
// if (job.data.id === id) {
|
||||||
// repeat: {
|
// await job.remove();
|
||||||
// pattern: cronExpression,
|
// }
|
||||||
// tz: timezone
|
// }
|
||||||
// }
|
|
||||||
// }
|
// Add new job
|
||||||
// );
|
const job = await workflowQueue.add(
|
||||||
|
'run workflow',
|
||||||
|
{ id, runId, userId },
|
||||||
|
{
|
||||||
|
repeat: {
|
||||||
|
pattern: cronExpression,
|
||||||
|
tz: timezone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const nextRun = job.timestamp;
|
||||||
|
|
||||||
|
// Update robot with schedule details
|
||||||
|
await robot.update({
|
||||||
|
schedule: {
|
||||||
|
runEvery,
|
||||||
|
runEveryUnit,
|
||||||
|
startFrom,
|
||||||
|
atTimeStart,
|
||||||
|
atTimeEnd,
|
||||||
|
timezone,
|
||||||
|
cronExpression,
|
||||||
|
lastRunAt: undefined,
|
||||||
|
nextRunAt: new Date(nextRun)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch updated schedule details after setting it
|
||||||
|
const updatedRobot = await Robot.findOne({ where: { 'recording_meta.id': id } });
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
message: 'success',
|
message: 'success',
|
||||||
runId,
|
runId,
|
||||||
// cronExpression,
|
robot: updatedRobot
|
||||||
// nextRunTime: getNextRunTime(cronExpression, timezone)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -328,12 +388,55 @@ router.put('/schedule/:id/', requireSignIn, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// function getNextRunTime(cronExpression, timezone) {
|
// Endpoint to get schedule details
|
||||||
// const schedule = cron.schedule(cronExpression, () => {}, { timezone });
|
router.get('/schedule/:id', requireSignIn, async (req, res) => {
|
||||||
// const nextDate = schedule.nextDate();
|
try {
|
||||||
// schedule.stop();
|
const robot = await Robot.findOne({ where: { 'recording_meta.id': req.params.id }, raw: true });
|
||||||
// return nextDate.toDate();
|
|
||||||
// }
|
if (!robot) {
|
||||||
|
return res.status(404).json({ error: 'Robot not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
schedule: robot.schedule
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting schedule:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to get schedule' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Endpoint to delete schedule
|
||||||
|
router.delete('/schedule/:id', requireSignIn, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const robot = await Robot.findOne({ where: { 'recording_meta.id': id } });
|
||||||
|
if (!robot) {
|
||||||
|
return res.status(404).json({ error: 'Robot not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove existing job from queue if it exists
|
||||||
|
const existingJobs = await workflowQueue.getJobs(['delayed', 'waiting']);
|
||||||
|
for (const job of existingJobs) {
|
||||||
|
if (job.data.id === id) {
|
||||||
|
await job.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the schedule from the robot
|
||||||
|
await robot.update({
|
||||||
|
schedule: null
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({ message: 'Schedule deleted successfully' });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting schedule:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to delete schedule' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST endpoint for aborting a current interpretation of the run.
|
* POST endpoint for aborting a current interpretation of the run.
|
||||||
|
|||||||
@@ -58,16 +58,19 @@ readdirSync(path.join(__dirname, 'api')).forEach((r) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const workerProcess = fork(path.resolve(__dirname, './worker.ts'));
|
const workerProcess = fork(path.resolve(__dirname, './worker.ts'), [], {
|
||||||
workerProcess.on('message', (message) => {
|
execArgv: ['--inspect=5859'], // Specify a different debug port for the worker
|
||||||
console.log(`Message from worker: ${message}`);
|
});
|
||||||
});
|
|
||||||
|
workerProcess.on('message', (message) => {
|
||||||
|
console.log(`Message from worker: ${message}`);
|
||||||
|
});
|
||||||
workerProcess.on('error', (error) => {
|
workerProcess.on('error', (error) => {
|
||||||
console.error(`Error in worker: ${error}`);
|
console.error(`Error in worker: ${error}`);
|
||||||
});
|
});
|
||||||
workerProcess.on('exit', (code) => {
|
workerProcess.on('exit', (code) => {
|
||||||
console.log(`Worker exited with code: ${code}`);
|
console.log(`Worker exited with code: ${code}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/', function (req, res) {
|
app.get('/', function (req, res) {
|
||||||
return res.send('Maxun server started 🚀');
|
return res.send('Maxun server started 🚀');
|
||||||
@@ -81,6 +84,6 @@ server.listen(SERVER_PORT, async () => {
|
|||||||
|
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
console.log('Main app shutting down...');
|
console.log('Main app shutting down...');
|
||||||
//workerProcess.kill();
|
workerProcess.kill();
|
||||||
process.exit();
|
process.exit();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,4 +52,8 @@ process.on('SIGINT', () => {
|
|||||||
process.exit();
|
process.exit();
|
||||||
});
|
});
|
||||||
|
|
||||||
export { workflowQueue, worker };
|
export { workflowQueue, worker };
|
||||||
|
|
||||||
|
export const temp = () => {
|
||||||
|
console.log('temp');
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ export const getStoredRecordings = async (): Promise<string[] | null> => {
|
|||||||
} else {
|
} else {
|
||||||
throw new Error('Couldn\'t retrieve stored recordings');
|
throw new Error('Couldn\'t retrieve stored recordings');
|
||||||
}
|
}
|
||||||
} catch(error: any) {
|
} catch (error: any) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@ export const getStoredRuns = async (): Promise<string[] | null> => {
|
|||||||
} else {
|
} else {
|
||||||
throw new Error('Couldn\'t retrieve stored recordings');
|
throw new Error('Couldn\'t retrieve stored recordings');
|
||||||
}
|
}
|
||||||
} catch(error: any) {
|
} catch (error: any) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export const getStoredRecording = async (id: string) => {
|
|||||||
} else {
|
} else {
|
||||||
throw new Error(`Couldn't retrieve stored recording ${id}`);
|
throw new Error(`Couldn't retrieve stored recording ${id}`);
|
||||||
}
|
}
|
||||||
} catch(error: any) {
|
} catch (error: any) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -54,7 +54,7 @@ export const deleteRecordingFromStorage = async (id: string): Promise<boolean> =
|
|||||||
} else {
|
} else {
|
||||||
throw new Error(`Couldn't delete stored recording ${id}`);
|
throw new Error(`Couldn't delete stored recording ${id}`);
|
||||||
}
|
}
|
||||||
} catch(error: any) {
|
} catch (error: any) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -68,7 +68,7 @@ export const deleteRunFromStorage = async (id: string): Promise<boolean> => {
|
|||||||
} else {
|
} else {
|
||||||
throw new Error(`Couldn't delete stored recording ${id}`);
|
throw new Error(`Couldn't delete stored recording ${id}`);
|
||||||
}
|
}
|
||||||
} catch(error: any) {
|
} catch (error: any) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -82,7 +82,7 @@ export const editRecordingFromStorage = async (browserId: string, id: string): P
|
|||||||
} else {
|
} else {
|
||||||
throw new Error(`Couldn't edit stored recording ${id}`);
|
throw new Error(`Couldn't edit stored recording ${id}`);
|
||||||
}
|
}
|
||||||
} catch(error: any) {
|
} catch (error: any) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -92,15 +92,15 @@ export const createRunForStoredRecording = async (id: string, settings: RunSetti
|
|||||||
try {
|
try {
|
||||||
const response = await axios.put(
|
const response = await axios.put(
|
||||||
`http://localhost:8080/storage/runs/${id}`,
|
`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 ${id}`);
|
throw new Error(`Couldn't create a run for a recording ${id}`);
|
||||||
}
|
}
|
||||||
} catch(error: any) {
|
} catch (error: any) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return {browserId: '', runId: ''};
|
return { browserId: '', runId: '' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,13 +112,13 @@ export const interpretStoredRecording = async (id: string): Promise<boolean> =>
|
|||||||
} else {
|
} else {
|
||||||
throw new Error(`Couldn't run a recording ${id}`);
|
throw new Error(`Couldn't run a recording ${id}`);
|
||||||
}
|
}
|
||||||
} catch(error: any) {
|
} catch (error: any) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const notifyAboutAbort = async (id:string): Promise<boolean> => {
|
export const notifyAboutAbort = async (id: string): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(`http://localhost:8080/storage/runs/abort/${id}`);
|
const response = await axios.post(`http://localhost:8080/storage/runs/abort/${id}`);
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
@@ -126,7 +126,7 @@ export const notifyAboutAbort = async (id:string): Promise<boolean> => {
|
|||||||
} else {
|
} else {
|
||||||
throw new Error(`Couldn't abort a running recording with id ${id}`);
|
throw new Error(`Couldn't abort a running recording with id ${id}`);
|
||||||
}
|
}
|
||||||
} catch(error: any) {
|
} catch (error: any) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -136,14 +136,42 @@ export const scheduleStoredRecording = async (id: string, settings: ScheduleSett
|
|||||||
try {
|
try {
|
||||||
const response = await axios.put(
|
const response = await axios.put(
|
||||||
`http://localhost:8080/storage/schedule/${id}`,
|
`http://localhost:8080/storage/schedule/${id}`,
|
||||||
{...settings});
|
{ ...settings });
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
return response.data;
|
return response.data;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Couldn't schedule recording ${id}. Please try again later.`);
|
throw new Error(`Couldn't schedule recording ${id}. Please try again later.`);
|
||||||
}
|
}
|
||||||
} catch(error: any) {
|
} catch (error: any) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return {message: '', runId: ''};
|
return { message: '', runId: '' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getSchedule = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`http://localhost:8080/storage/schedule/${id}`);
|
||||||
|
if (response.status === 200) {
|
||||||
|
return response.data.schedule;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Couldn't retrieve schedule for recording ${id}`);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log(error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteSchedule = async (id: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.delete(`http://localhost:8080/storage/schedule/${id}`);
|
||||||
|
if (response.status === 200) {
|
||||||
|
return response.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Couldn't delete schedule for recording ${id}`);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log(error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,40 +1,51 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { GenericModal } from "../atoms/GenericModal";
|
import { GenericModal } from "../atoms/GenericModal";
|
||||||
import { MenuItem, TextField, Typography, Box } from "@mui/material";
|
import { MenuItem, TextField, Typography, Box, Switch, FormControlLabel } from "@mui/material";
|
||||||
import { Dropdown } from "../atoms/DropdownMui";
|
import { Dropdown } from "../atoms/DropdownMui";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import { modalStyle } from "./AddWhereCondModal";
|
import { modalStyle } from "./AddWhereCondModal";
|
||||||
import { validMomentTimezones } from '../../constants/const';
|
import { validMomentTimezones } from '../../constants/const';
|
||||||
|
import { useGlobalInfoStore } from '../../context/globalInfo';
|
||||||
|
import { getSchedule, deleteSchedule } from '../../api/storage';
|
||||||
|
|
||||||
interface ScheduleSettingsProps {
|
interface ScheduleSettingsProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
handleStart: (settings: ScheduleSettings) => void;
|
handleStart: (settings: ScheduleSettings) => void;
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
|
initialSettings?: ScheduleSettings | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScheduleSettings {
|
export interface ScheduleSettings {
|
||||||
runEvery: number;
|
runEvery: number;
|
||||||
runEveryUnit: string;
|
runEveryUnit: string;
|
||||||
startFrom: string;
|
startFrom: string;
|
||||||
atTime: string;
|
atTimeStart?: string;
|
||||||
|
atTimeEnd?: string;
|
||||||
timezone: string;
|
timezone: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose }: ScheduleSettingsProps) => {
|
export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initialSettings }: ScheduleSettingsProps) => {
|
||||||
|
const [schedule, setSchedule] = useState<ScheduleSettings | null>(null);
|
||||||
const [settings, setSettings] = useState<ScheduleSettings>({
|
const [settings, setSettings] = useState<ScheduleSettings>({
|
||||||
runEvery: 1,
|
runEvery: 1,
|
||||||
runEveryUnit: 'HOURS',
|
runEveryUnit: 'HOURS',
|
||||||
startFrom: 'MONDAY',
|
startFrom: 'MONDAY',
|
||||||
atTime: '00:00',
|
atTimeStart: '00:00',
|
||||||
|
atTimeEnd: '01:00',
|
||||||
timezone: 'UTC'
|
timezone: 'UTC'
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleChange = (field: keyof ScheduleSettings, value: string | number) => {
|
// Load initial settings if provided
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialSettings) {
|
||||||
|
setSettings(initialSettings);
|
||||||
|
}
|
||||||
|
}, [initialSettings]);
|
||||||
|
|
||||||
|
const handleChange = (field: keyof ScheduleSettings, value: string | number | boolean) => {
|
||||||
setSettings(prev => ({ ...prev, [field]: value }));
|
setSettings(prev => ({ ...prev, [field]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(`Settings:`, settings);
|
|
||||||
|
|
||||||
const textStyle = {
|
const textStyle = {
|
||||||
width: '150px',
|
width: '150px',
|
||||||
height: '52px',
|
height: '52px',
|
||||||
@@ -49,11 +60,12 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose }: Sche
|
|||||||
};
|
};
|
||||||
|
|
||||||
const units = [
|
const units = [
|
||||||
|
'MINUTES',
|
||||||
'HOURS',
|
'HOURS',
|
||||||
'DAYS',
|
'DAYS',
|
||||||
'WEEKS',
|
'WEEKS',
|
||||||
'MONTHS'
|
'MONTHS'
|
||||||
]
|
];
|
||||||
|
|
||||||
const days = [
|
const days = [
|
||||||
'MONDAY',
|
'MONDAY',
|
||||||
@@ -63,7 +75,48 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose }: Sche
|
|||||||
'FRIDAY',
|
'FRIDAY',
|
||||||
'SATURDAY',
|
'SATURDAY',
|
||||||
'SUNDAY'
|
'SUNDAY'
|
||||||
]
|
];
|
||||||
|
|
||||||
|
const { recordingId } = useGlobalInfoStore();
|
||||||
|
|
||||||
|
console.log(`Recoridng ID Shculde: ${recordingId}`);
|
||||||
|
|
||||||
|
const deleteRobotSchedule = () => {
|
||||||
|
if (recordingId) {
|
||||||
|
deleteSchedule(recordingId);
|
||||||
|
setSchedule(null);
|
||||||
|
} else {
|
||||||
|
console.error('No recording id provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
setSettings({
|
||||||
|
runEvery: 1,
|
||||||
|
runEveryUnit: 'HOURS',
|
||||||
|
startFrom: 'MONDAY',
|
||||||
|
atTimeStart: '00:00',
|
||||||
|
atTimeEnd: '01:00',
|
||||||
|
timezone: 'UTC'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRobotSchedule = async () => {
|
||||||
|
if (recordingId) {
|
||||||
|
const scheduleData = await getSchedule(recordingId);
|
||||||
|
console.log(`Robot found schedule: ${JSON.stringify(scheduleData, null, 2)}`);
|
||||||
|
setSchedule(scheduleData);
|
||||||
|
} else {
|
||||||
|
console.error('No recording id provided');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
const fetchSchedule = async () => {
|
||||||
|
await getRobotSchedule();
|
||||||
|
};
|
||||||
|
fetchSchedule();
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GenericModal
|
<GenericModal
|
||||||
@@ -79,79 +132,117 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose }: Sche
|
|||||||
'& > *': { marginBottom: '20px' },
|
'& > *': { marginBottom: '20px' },
|
||||||
}}>
|
}}>
|
||||||
<Typography variant="h6" sx={{ marginBottom: '20px' }}>Schedule Settings</Typography>
|
<Typography variant="h6" sx={{ marginBottom: '20px' }}>Schedule Settings</Typography>
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
(schedule !== null) ? (
|
||||||
|
<>
|
||||||
|
<Typography>Robot is scheduled to run every {schedule.runEvery} {schedule.runEveryUnit} starting from {schedule.startFrom} at {schedule.atTimeStart} to {schedule.atTimeEnd} in {schedule.timezone} timezone.</Typography>
|
||||||
|
<Box mt={2} display="flex" justifyContent="space-between">
|
||||||
|
<Button
|
||||||
|
onClick={deleteRobotSchedule}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
Delete Schedule
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
||||||
|
<Typography sx={{ marginRight: '10px' }}>Run once every</Typography>
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
value={settings.runEvery}
|
||||||
|
onChange={(e) => handleChange('runEvery', parseInt(e.target.value))}
|
||||||
|
sx={textStyle}
|
||||||
|
inputProps={{ min: 1 }}
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
label=""
|
||||||
|
id="runEveryUnit"
|
||||||
|
value={settings.runEveryUnit}
|
||||||
|
handleSelect={(e) => handleChange('runEveryUnit', e.target.value)}
|
||||||
|
sx={dropDownStyle}
|
||||||
|
>
|
||||||
|
{units.map((unit) => (
|
||||||
|
<MenuItem key={unit} value={unit}>{unit}</MenuItem>
|
||||||
|
))}
|
||||||
|
</Dropdown>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
||||||
<Typography sx={{ marginRight: '10px' }}>Run once every</Typography>
|
<Typography sx={{ marginBottom: '5px', marginRight: '25px' }}>Start from / On</Typography>
|
||||||
<TextField
|
<Dropdown
|
||||||
type="number"
|
label=""
|
||||||
value={settings.runEvery}
|
id="startFrom"
|
||||||
onChange={(e) => handleChange('runEvery', parseInt(e.target.value))}
|
value={settings.startFrom}
|
||||||
sx={textStyle}
|
handleSelect={(e) => handleChange('startFrom', e.target.value)}
|
||||||
inputProps={{ min: 1 }}
|
sx={dropDownStyle}
|
||||||
/>
|
>
|
||||||
<Dropdown
|
{days.map((day) => (
|
||||||
label=""
|
<MenuItem key={day} value={day}>{day}</MenuItem>
|
||||||
id="runEveryUnit"
|
))}
|
||||||
value={settings.runEveryUnit}
|
</Dropdown>
|
||||||
handleSelect={(e) => handleChange('runEveryUnit', e.target.value)}
|
</Box>
|
||||||
sx={dropDownStyle}
|
|
||||||
>
|
|
||||||
{units.map((unit) => (
|
|
||||||
<MenuItem key={unit} value={unit}>{unit}</MenuItem>
|
|
||||||
))}
|
|
||||||
</Dropdown>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
{['MINUTES', 'HOURS'].includes(settings.runEveryUnit) ? (
|
||||||
<Typography sx={{ marginBottom: '5px', marginRight: '25px' }}>Start from / On</Typography>
|
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
||||||
<Dropdown
|
<Box sx={{ marginRight: '20px' }}>
|
||||||
label=""
|
<Typography sx={{ marginBottom: '5px' }}>In Between</Typography>
|
||||||
id="startFrom"
|
<TextField
|
||||||
value={settings.startFrom}
|
type="time"
|
||||||
handleSelect={(e) => handleChange('startFrom', e.target.value)}
|
value={settings.atTimeStart}
|
||||||
sx={dropDownStyle}
|
onChange={(e) => handleChange('atTimeStart', e.target.value)}
|
||||||
>
|
sx={textStyle}
|
||||||
{days.map((day) => (
|
/>
|
||||||
<MenuItem key={day} value={day}>{day}</MenuItem>
|
<TextField
|
||||||
))}
|
type="time"
|
||||||
</Dropdown>
|
value={settings.atTimeEnd}
|
||||||
</Box>
|
onChange={(e) => handleChange('atTimeEnd', e.target.value)}
|
||||||
|
sx={textStyle}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
||||||
|
<Typography sx={{ marginBottom: '5px', marginRight: '10px' }}>At Around</Typography>
|
||||||
|
<TextField
|
||||||
|
type="time"
|
||||||
|
value={settings.atTimeStart}
|
||||||
|
onChange={(e) => handleChange('atTimeStart', e.target.value)}
|
||||||
|
sx={textStyle}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
||||||
<Box sx={{ marginRight: '20px' }}>
|
<Typography sx={{ marginRight: '10px' }}>Timezone</Typography>
|
||||||
<Typography sx={{ marginBottom: '5px' }}>At around</Typography>
|
<Dropdown
|
||||||
<TextField
|
label=""
|
||||||
type="time"
|
id="timezone"
|
||||||
value={settings.atTime}
|
value={settings.timezone}
|
||||||
onChange={(e) => handleChange('atTime', e.target.value)}
|
handleSelect={(e) => handleChange('timezone', e.target.value)}
|
||||||
sx={textStyle}
|
sx={dropDownStyle}
|
||||||
/>
|
>
|
||||||
</Box>
|
{validMomentTimezones.map((tz) => (
|
||||||
<Box>
|
<MenuItem key={tz} value={tz}>{tz}</MenuItem>
|
||||||
<Typography sx={{ marginBottom: '5px' }}>Timezone</Typography>
|
))}
|
||||||
<Dropdown
|
</Dropdown>
|
||||||
label=""
|
</Box>
|
||||||
id="timezone"
|
<Box mt={2} display="flex" justifyContent="flex-end">
|
||||||
value={settings.timezone}
|
<Button onClick={() => handleStart(settings)} variant="contained" color="primary">
|
||||||
handleSelect={(e) => handleChange('timezone', e.target.value)}
|
Save Schedule
|
||||||
sx={dropDownStyle}
|
</Button>
|
||||||
>
|
<Button onClick={handleClose} color="primary" variant="outlined" style={{ marginLeft: '10px' }}>
|
||||||
{validMomentTimezones.map((tz) => (
|
Cancel
|
||||||
<MenuItem key={tz} value={tz}>{tz}</MenuItem>
|
</Button>
|
||||||
))}
|
</Box>
|
||||||
</Dropdown>
|
</>
|
||||||
</Box>
|
)
|
||||||
</Box>
|
}
|
||||||
|
</>
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={() => handleStart(settings)}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</Box>
|
</Box>
|
||||||
</GenericModal>
|
</GenericModal>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ScheduleSettingsModal;
|
|
||||||
|
|||||||
Reference in New Issue
Block a user