Merge pull request #82 from amhsirak/develop

feat: more scheduler options
This commit is contained in:
Karishma Shukla
2024-10-23 04:19:15 +05:30
committed by GitHub
7 changed files with 398 additions and 141 deletions

View File

@@ -30,6 +30,8 @@ export class RemoteBrowser {
*/
private browser: Browser | null = null;
private context: BrowserContext | null = null;
/**
* The Playwright's [CDPSession](https://playwright.dev/docs/api/class-cdpsession) instance,
* used to talk raw Chrome Devtools Protocol.
@@ -90,13 +92,13 @@ export class RemoteBrowser {
*/
public initialize = async (options: RemoteBrowserOptions): Promise<void> => {
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 },
// recordVideo: { dir: 'videos/' }
}
);
this.currentPage = await context.newPage();
this.currentPage = await this.context.newPage();
const blocker = await PlaywrightBlocker.fromPrebuiltAdsAndTracking(fetch);
await blocker.enableBlockingInPage(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`)
}
});
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) {
await this.stopScreencast();
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.socket.emit('urlChanged', this.currentPage.url());
await this.makeAndEmitScreenshot();

View File

@@ -1,8 +1,6 @@
import { Model, DataTypes, Optional } from 'sequelize';
import sequelize from '../storage/db';
import { WorkflowFile, Where, What, WhereWhatPair } from 'maxun-core';
import User from './User'; // Import User model
import Run from './Run';
interface RobotMeta {
name: string;
@@ -27,6 +25,19 @@ interface RobotAttributes {
google_sheet_id?: string | null;
google_access_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'> { }
@@ -41,6 +52,7 @@ class Robot extends Model<RobotAttributes, RobotCreationAttributes> implements R
public google_sheet_id?: string | null;
public google_access_token!: string | null;
public google_refresh_token!: string | null;
public schedule!: ScheduleConfig | null;
}
Robot.init(
@@ -82,6 +94,10 @@ Robot.init(
type: DataTypes.STRING,
allowNull: true,
},
schedule: {
type: DataTypes.JSONB,
allowNull: true,
},
},
{
sequelize,

View File

@@ -12,7 +12,7 @@ import { requireSignIn } from '../middlewares/auth';
import Robot from '../models/Robot';
import Run from '../models/Run';
import { BinaryOutputService } from '../storage/mino';
// import { workflowQueue } from '../worker';
import { workflowQueue } from '../worker';
export const router = Router();
@@ -46,7 +46,7 @@ router.get('/recordings/:id', requireSignIn, async (req, res) => {
where: { 'recording_meta.id': req.params.id },
raw: true
}
);
);
return res.send(data);
} catch (e) {
logger.log('info', 'Error while reading recordings');
@@ -208,8 +208,8 @@ router.post('/runs/run/:id', requireSignIn, async (req, res) => {
if (browser && currentPage) {
const interpretationInfo = await browser.interpreter.InterpretRecording(
recording.recording, currentPage, plainRun.interpreterSettings);
const binaryOutputService = new BinaryOutputService('maxun-run-screenshots');
const uploadedBinaryOutput = await binaryOutputService.uploadAndStoreBinaryOutput(run, interpretationInfo.binaryOutput);
const binaryOutputService = new BinaryOutputService('maxun-run-screenshots');
const uploadedBinaryOutput = await binaryOutputService.uploadAndStoreBinaryOutput(run, interpretationInfo.binaryOutput);
await destroyRemoteBrowser(plainRun.browserId);
await run.update({
...run,
@@ -247,18 +247,45 @@ router.put('/schedule/:id/', requireSignIn, async (req, res) => {
try {
const { id } = req.params;
const {
// enabled = true,
runEvery,
runEveryUnit,
startFrom,
atTime,
atTimeStart,
atTimeEnd,
timezone
} = 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' });
}
if (!['HOURS', 'DAYS', 'WEEKS', 'MONTHS'].includes(runEveryUnit)) {
if (!['HOURS', 'DAYS', 'WEEKS', 'MONTHS', 'MINUTES'].includes(runEveryUnit)) {
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' });
}
const [hours, minutes] = atTime.split(':').map(Number);
if (isNaN(hours) || isNaN(minutes) || hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
const [startHours, startMinutes] = atTimeStart.split(':').map(Number);
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' });
}
@@ -278,18 +309,19 @@ router.put('/schedule/:id/', requireSignIn, async (req, res) => {
let cronExpression;
switch (runEveryUnit) {
case 'MINUTES':
case 'HOURS':
cronExpression = `${minutes} */${runEvery} * * *`;
cronExpression = `${startMinutes}-${endMinutes} */${runEvery} * * *`;
break;
case 'DAYS':
cronExpression = `${minutes} ${hours} */${runEvery} * *`;
cronExpression = `${startMinutes} ${startHours} */${runEvery} * *`;
break;
case 'WEEKS':
const dayIndex = days.indexOf(startFrom);
cronExpression = `${minutes} ${hours} * * ${dayIndex}/${7 * runEvery}`;
cronExpression = `${startMinutes} ${startHours} * * ${dayIndex}/${7 * runEvery}`;
break;
case 'MONTHS':
cronExpression = `${minutes} ${hours} 1-7 */${runEvery} *`;
cronExpression = `${startMinutes} ${startHours} 1-7 */${runEvery} *`;
if (startFrom !== 'SUNDAY') {
const dayIndex = days.indexOf(startFrom);
cronExpression += ` ${dayIndex}`;
@@ -304,22 +336,50 @@ router.put('/schedule/:id/', requireSignIn, async (req, res) => {
const runId = uuid();
const userId = req.user.id;
// await workflowQueue.add(
// 'run workflow',
// { id, runId, userId },
// {
// repeat: {
// pattern: cronExpression,
// tz: timezone
// }
// }
// );
// Remove existing jobs for this robot just in case some were left
// const existingJobs = await workflowQueue.getJobs(['delayed', 'waiting']);
// for (const job of existingJobs) {
// if (job.data.id === id) {
// await job.remove();
// }
// }
// 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({
message: 'success',
runId,
// cronExpression,
// nextRunTime: getNextRunTime(cronExpression, timezone)
robot: updatedRobot
});
} catch (error) {
@@ -328,12 +388,55 @@ router.put('/schedule/:id/', requireSignIn, async (req, res) => {
}
});
// function getNextRunTime(cronExpression, timezone) {
// const schedule = cron.schedule(cronExpression, () => {}, { timezone });
// const nextDate = schedule.nextDate();
// schedule.stop();
// return nextDate.toDate();
// }
// Endpoint to get schedule details
router.get('/schedule/:id', requireSignIn, async (req, res) => {
try {
const robot = await Robot.findOne({ where: { 'recording_meta.id': req.params.id }, raw: true });
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.

View File

@@ -58,16 +58,19 @@ readdirSync(path.join(__dirname, 'api')).forEach((r) => {
}
});
const workerProcess = fork(path.resolve(__dirname, './worker.ts'));
workerProcess.on('message', (message) => {
console.log(`Message from worker: ${message}`);
});
const workerProcess = fork(path.resolve(__dirname, './worker.ts'), [], {
execArgv: ['--inspect=5859'], // Specify a different debug port for the worker
});
workerProcess.on('message', (message) => {
console.log(`Message from worker: ${message}`);
});
workerProcess.on('error', (error) => {
console.error(`Error in worker: ${error}`);
});
workerProcess.on('exit', (code) => {
console.error(`Error in worker: ${error}`);
});
workerProcess.on('exit', (code) => {
console.log(`Worker exited with code: ${code}`);
});
});
app.get('/', function (req, res) {
return res.send('Maxun server started 🚀');
@@ -81,6 +84,6 @@ server.listen(SERVER_PORT, async () => {
process.on('SIGINT', () => {
console.log('Main app shutting down...');
//workerProcess.kill();
workerProcess.kill();
process.exit();
});

View File

@@ -52,4 +52,8 @@ process.on('SIGINT', () => {
process.exit();
});
export { workflowQueue, worker };
export { workflowQueue, worker };
export const temp = () => {
console.log('temp');
}

View File

@@ -12,7 +12,7 @@ export const getStoredRecordings = async (): Promise<string[] | null> => {
} else {
throw new Error('Couldn\'t retrieve stored recordings');
}
} catch(error: any) {
} catch (error: any) {
console.log(error);
return null;
}
@@ -26,7 +26,7 @@ export const getStoredRuns = async (): Promise<string[] | null> => {
} else {
throw new Error('Couldn\'t retrieve stored recordings');
}
} catch(error: any) {
} catch (error: any) {
console.log(error);
return null;
}
@@ -40,7 +40,7 @@ export const getStoredRecording = async (id: string) => {
} else {
throw new Error(`Couldn't retrieve stored recording ${id}`);
}
} catch(error: any) {
} catch (error: any) {
console.log(error);
return null;
}
@@ -54,7 +54,7 @@ export const deleteRecordingFromStorage = async (id: string): Promise<boolean> =
} else {
throw new Error(`Couldn't delete stored recording ${id}`);
}
} catch(error: any) {
} catch (error: any) {
console.log(error);
return false;
}
@@ -68,7 +68,7 @@ export const deleteRunFromStorage = async (id: string): Promise<boolean> => {
} else {
throw new Error(`Couldn't delete stored recording ${id}`);
}
} catch(error: any) {
} catch (error: any) {
console.log(error);
return false;
}
@@ -82,7 +82,7 @@ export const editRecordingFromStorage = async (browserId: string, id: string): P
} else {
throw new Error(`Couldn't edit stored recording ${id}`);
}
} catch(error: any) {
} catch (error: any) {
console.log(error);
return null;
}
@@ -92,15 +92,15 @@ export const createRunForStoredRecording = async (id: string, settings: RunSetti
try {
const response = await axios.put(
`http://localhost:8080/storage/runs/${id}`,
{...settings});
{ ...settings });
if (response.status === 200) {
return response.data;
} else {
throw new Error(`Couldn't create a run for a recording ${id}`);
}
} catch(error: any) {
} catch (error: any) {
console.log(error);
return {browserId: '', runId: ''};
return { browserId: '', runId: '' };
}
}
@@ -112,13 +112,13 @@ export const interpretStoredRecording = async (id: string): Promise<boolean> =>
} else {
throw new Error(`Couldn't run a recording ${id}`);
}
} catch(error: any) {
} catch (error: any) {
console.log(error);
return false;
}
}
export const notifyAboutAbort = async (id:string): Promise<boolean> => {
export const notifyAboutAbort = async (id: string): Promise<boolean> => {
try {
const response = await axios.post(`http://localhost:8080/storage/runs/abort/${id}`);
if (response.status === 200) {
@@ -126,7 +126,7 @@ export const notifyAboutAbort = async (id:string): Promise<boolean> => {
} else {
throw new Error(`Couldn't abort a running recording with id ${id}`);
}
} catch(error: any) {
} catch (error: any) {
console.log(error);
return false;
}
@@ -136,14 +136,42 @@ export const scheduleStoredRecording = async (id: string, settings: ScheduleSett
try {
const response = await axios.put(
`http://localhost:8080/storage/schedule/${id}`,
{...settings});
{ ...settings });
if (response.status === 200) {
return response.data;
} else {
throw new Error(`Couldn't schedule recording ${id}. Please try again later.`);
}
} catch(error: any) {
} catch (error: any) {
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;
}
}

View File

@@ -1,40 +1,51 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
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 Button from "@mui/material/Button";
import { modalStyle } from "./AddWhereCondModal";
import { validMomentTimezones } from '../../constants/const';
import { useGlobalInfoStore } from '../../context/globalInfo';
import { getSchedule, deleteSchedule } from '../../api/storage';
interface ScheduleSettingsProps {
isOpen: boolean;
handleStart: (settings: ScheduleSettings) => void;
handleClose: () => void;
initialSettings?: ScheduleSettings | null;
}
export interface ScheduleSettings {
runEvery: number;
runEveryUnit: string;
startFrom: string;
atTime: string;
atTimeStart?: string;
atTimeEnd?: 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>({
runEvery: 1,
runEveryUnit: 'HOURS',
startFrom: 'MONDAY',
atTime: '00:00',
atTimeStart: '00:00',
atTimeEnd: '01:00',
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 }));
};
console.log(`Settings:`, settings);
const textStyle = {
width: '150px',
height: '52px',
@@ -49,11 +60,12 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose }: Sche
};
const units = [
'MINUTES',
'HOURS',
'DAYS',
'WEEKS',
'MONTHS'
]
];
const days = [
'MONDAY',
@@ -63,7 +75,48 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose }: Sche
'FRIDAY',
'SATURDAY',
'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 (
<GenericModal
@@ -79,79 +132,117 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose }: Sche
'& > *': { marginBottom: '20px' },
}}>
<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%' }}>
<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%' }}>
<Typography sx={{ marginBottom: '5px', marginRight: '25px' }}>Start from / On</Typography>
<Dropdown
label=""
id="startFrom"
value={settings.startFrom}
handleSelect={(e) => handleChange('startFrom', e.target.value)}
sx={dropDownStyle}
>
{days.map((day) => (
<MenuItem key={day} value={day}>{day}</MenuItem>
))}
</Dropdown>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
<Typography sx={{ marginBottom: '5px', marginRight: '25px' }}>Start from / On</Typography>
<Dropdown
label=""
id="startFrom"
value={settings.startFrom}
handleSelect={(e) => handleChange('startFrom', e.target.value)}
sx={dropDownStyle}
>
{days.map((day) => (
<MenuItem key={day} value={day}>{day}</MenuItem>
))}
</Dropdown>
</Box>
{['MINUTES', 'HOURS'].includes(settings.runEveryUnit) ? (
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
<Box sx={{ marginRight: '20px' }}>
<Typography sx={{ marginBottom: '5px' }}>In Between</Typography>
<TextField
type="time"
value={settings.atTimeStart}
onChange={(e) => handleChange('atTimeStart', e.target.value)}
sx={textStyle}
/>
<TextField
type="time"
value={settings.atTimeEnd}
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={{ marginRight: '20px' }}>
<Typography sx={{ marginBottom: '5px' }}>At around</Typography>
<TextField
type="time"
value={settings.atTime}
onChange={(e) => handleChange('atTime', e.target.value)}
sx={textStyle}
/>
</Box>
<Box>
<Typography sx={{ marginBottom: '5px' }}>Timezone</Typography>
<Dropdown
label=""
id="timezone"
value={settings.timezone}
handleSelect={(e) => handleChange('timezone', e.target.value)}
sx={dropDownStyle}
>
{validMomentTimezones.map((tz) => (
<MenuItem key={tz} value={tz}>{tz}</MenuItem>
))}
</Dropdown>
</Box>
</Box>
<Button
variant="contained"
onClick={() => handleStart(settings)}
>
Save
</Button>
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
<Typography sx={{ marginRight: '10px' }}>Timezone</Typography>
<Dropdown
label=""
id="timezone"
value={settings.timezone}
handleSelect={(e) => handleChange('timezone', e.target.value)}
sx={dropDownStyle}
>
{validMomentTimezones.map((tz) => (
<MenuItem key={tz} value={tz}>{tz}</MenuItem>
))}
</Dropdown>
</Box>
<Box mt={2} display="flex" justifyContent="flex-end">
<Button onClick={() => handleStart(settings)} variant="contained" color="primary">
Save Schedule
</Button>
<Button onClick={handleClose} color="primary" variant="outlined" style={{ marginLeft: '10px' }}>
Cancel
</Button>
</Box>
</>
)
}
</>
</Box>
</GenericModal>
);
}
export default ScheduleSettingsModal;
};