Merge pull request #68 from amhsirak/develop

feat: new ui
This commit is contained in:
Karishma Shukla
2024-10-12 15:07:00 +05:30
committed by GitHub
21 changed files with 350 additions and 245 deletions

View File

@@ -275,7 +275,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: 720, width: 1280 }) await this.currentPage.setViewportSize({ height: 500, width: 1280 })
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();

View File

@@ -33,13 +33,13 @@ const defaultModalStyle = {
top: '50%', top: '50%',
left: '50%', left: '50%',
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
width: '50%', width: 500,
bgcolor: 'background.paper', bgcolor: 'background.paper',
boxShadow: 24, boxShadow: 24,
p: 4, p: 4,
height: '30%', height: '30%',
display: 'block', display: 'block',
overflow: 'hidden', overflow: 'scroll',
padding: '5px 25px 10px 25px', padding: '5px 25px 10px 25px',
zIndex: 3147483647, zIndex: 3147483647,
}; };

View File

@@ -12,6 +12,7 @@ import { UrlForm } from './UrlForm';
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useSocketStore } from "../../context/socket"; import { useSocketStore } from "../../context/socket";
import { getCurrentUrl } from "../../api/recording"; import { getCurrentUrl } from "../../api/recording";
import { useGlobalInfoStore } from '../../context/globalInfo';
const StyledNavBar = styled.div<{ browserWidth: number }>` const StyledNavBar = styled.div<{ browserWidth: number }>`
display: flex; display: flex;
@@ -31,8 +32,7 @@ const BrowserNavBar: FC<NavBarProps> = ({
}) => { }) => {
const { socket } = useSocketStore(); const { socket } = useSocketStore();
const { recordingUrl, setRecordingUrl } = useGlobalInfoStore();
const [currentUrl, setCurrentUrl] = useState<string>('https://');
const handleRefresh = useCallback((): void => { const handleRefresh = useCallback((): void => {
socket?.emit('input:refresh'); socket?.emit('input:refresh');
@@ -44,14 +44,13 @@ const BrowserNavBar: FC<NavBarProps> = ({
const handleCurrentUrlChange = useCallback((url: string) => { const handleCurrentUrlChange = useCallback((url: string) => {
handleUrlChanged(url); handleUrlChanged(url);
setCurrentUrl(url); setRecordingUrl(url);
}, [handleUrlChanged, currentUrl]); }, [handleUrlChanged, recordingUrl]);
useEffect(() => { useEffect(() => {
getCurrentUrl().then((response) => { getCurrentUrl().then((response) => {
if (response) { if (response) {
handleUrlChanged(response); handleUrlChanged(response);
setCurrentUrl(response);
} }
}).catch((error) => { }).catch((error) => {
console.log("Fetching current url failed"); console.log("Fetching current url failed");
@@ -72,12 +71,13 @@ const BrowserNavBar: FC<NavBarProps> = ({
const addAddress = (address: string) => { const addAddress = (address: string) => {
if (socket) { if (socket) {
handleUrlChanged(address); handleUrlChanged(address);
setRecordingUrl(address);
handleGoTo(address); handleGoTo(address);
} }
}; };
return ( return (
<StyledNavBar browserWidth={browserWidth}> <StyledNavBar browserWidth={900}>
<NavBarButton <NavBarButton
type="button" type="button"
onClick={() => { onClick={() => {
@@ -111,7 +111,7 @@ const BrowserNavBar: FC<NavBarProps> = ({
</NavBarButton> </NavBarButton>
<UrlForm <UrlForm
currentAddress={currentUrl} currentAddress={recordingUrl}
handleRefresh={handleRefresh} handleRefresh={handleRefresh}
setCurrentAddress={addAddress} setCurrentAddress={addAddress}
/> />

View File

@@ -31,7 +31,7 @@ export const BrowserTabs = (
return ( return (
<Box sx={{ <Box sx={{
width: `${width}px`, width: 800,
display: 'flex', display: 'flex',
overflow: 'auto', overflow: 'auto',
alignItems: 'center', alignItems: 'center',

View File

@@ -6,10 +6,12 @@ import { useGlobalInfoStore } from "../../context/globalInfo";
import { Button, IconButton } from "@mui/material"; import { Button, IconButton } from "@mui/material";
import { RecordingIcon } from "../atoms/RecorderIcon"; import { RecordingIcon } from "../atoms/RecorderIcon";
import { SaveRecording } from "./SaveRecording"; import { SaveRecording } from "./SaveRecording";
import { Circle } from "@mui/icons-material"; import { Circle, Add, Logout, Clear } from "@mui/icons-material";
import MeetingRoomIcon from '@mui/icons-material/MeetingRoom'; import MeetingRoomIcon from '@mui/icons-material/MeetingRoom';
import { Link, useLocation, useNavigate } from 'react-router-dom'; import { Link, useLocation, useNavigate } from 'react-router-dom';
import { AuthContext } from '../../context/auth'; import { AuthContext } from '../../context/auth';
import { GenericModal } from '../atoms/GenericModal';
import TextField from '@mui/material/TextField';
interface NavBarProps { interface NavBarProps {
newRecording: () => void; newRecording: () => void;
@@ -17,11 +19,15 @@ interface NavBarProps {
isRecording: boolean; isRecording: boolean;
} }
export const NavBar = ({ newRecording, recordingName, isRecording }: NavBarProps) => { export const NavBar: React.FC<NavBarProps> = ({ newRecording, recordingName, isRecording }) => {
const { notify, browserId, setBrowserId, recordingLength } = useGlobalInfoStore(); const { notify, browserId, setBrowserId, recordingLength, recordingUrl, setRecordingUrl } = useGlobalInfoStore();
const { state, dispatch } = useContext(AuthContext); const { state, dispatch } = useContext(AuthContext);
const { user } = state; const { user } = state;
const [isModalOpen, setModalOpen] = useState(false);
console.log(`Recording URL: ${recordingUrl}`)
const navigate = useNavigate(); const navigate = useNavigate();
const logout = async () => { const logout = async () => {
@@ -48,9 +54,14 @@ export const NavBar = ({ newRecording, recordingName, isRecording }: NavBarProps
setBrowserId(null); setBrowserId(null);
await stopRecording(browserId); await stopRecording(browserId);
} }
setModalOpen(true);
};
const startRecording = () => {
setModalOpen(false);
newRecording(); newRecording();
notify('info', 'New Recording started'); notify('info', 'New Recording started for ' + recordingUrl);
} };
return ( return (
<NavBarWrapper> <NavBarWrapper>
@@ -68,61 +79,92 @@ export const NavBar = ({ newRecording, recordingName, isRecording }: NavBarProps
display: 'flex', display: 'flex',
justifyContent: 'flex-end', justifyContent: 'flex-end',
}}> }}>
<IconButton {
aria-label="new" !isRecording ? (
size={"small"} <>
onClick={handleNewRecording} <IconButton
sx={{ aria-label="new"
width: isRecording ? '100px' : '130px', size={"small"}
onClick={handleNewRecording}
sx={{
width: '140px',
borderRadius: '5px',
padding: '8px',
background: '#ff00c3',
color: 'white',
marginRight: '10px',
fontFamily: '"Roboto","Helvetica","Arial",sans-serif',
fontWeight: '500',
fontSize: '0.875rem',
lineHeight: '1.75',
letterSpacing: '0.02857em',
'&:hover': { color: 'white', backgroundColor: '#ff00c3' }
}
}
>
<Add sx={{ marginRight: '5px' }} /> Create Robot
</IconButton>
<IconButton sx={{
width: '140px',
borderRadius: '5px',
padding: '8px',
background: '#ff00c3',
color: 'white',
marginRight: '10px',
fontFamily: '"Roboto","Helvetica","Arial",sans-serif',
fontWeight: '500',
fontSize: '0.875rem',
lineHeight: '1.75',
letterSpacing: '0.02857em',
'&:hover': { color: 'white', backgroundColor: '#ff00c3' }
}} onClick={logout}>
<Logout sx={{ marginRight: '5px' }} />
Logout</IconButton>
</>
) : <IconButton sx={{
width: '140px',
borderRadius: '5px', borderRadius: '5px',
padding: '8px', padding: '8px',
background: 'white', background: 'red',
color: 'rgba(255,0,0,0.7)', color: 'white',
marginRight: '10px', marginRight: '10px',
fontFamily: '"Roboto","Helvetica","Arial",sans-serif', fontFamily: '"Roboto","Helvetica","Arial",sans-serif',
fontWeight: '500', fontWeight: '500',
fontSize: '0.875rem', fontSize: '0.875rem',
lineHeight: '1.75', lineHeight: '1.75',
letterSpacing: '0.02857em', letterSpacing: '0.02857em',
'&:hover': { color: 'red', backgroundColor: 'white' } '&:hover': { color: 'white', backgroundColor: 'red' }
} }} onClick={goToMainMenu}>
} <Clear sx={{ marginRight: '5px' }} />
> Discard</IconButton>
<Circle sx={{ marginRight: '5px' }} /> {isRecording ? 'NEW' : 'RECORD'} }
</IconButton>
{ {
recordingLength > 0 recordingLength > 0
? <SaveRecording fileName={recordingName} /> ? <SaveRecording fileName={recordingName} />
: null : null
} }
{isRecording ? <Button sx={{
width: '100px',
background: '#fff',
color: 'rgba(25, 118, 210, 0.7)',
padding: '9px',
marginRight: '19px',
'&:hover': {
background: 'white',
color: 'rgb(25, 118, 210)',
}
}} onClick={goToMainMenu}>
<MeetingRoomIcon sx={{ marginRight: '5px' }} />
exit</Button>
: null}
<Button sx={{
width: '100px',
background: '#fff',
color: 'rgba(25, 118, 210, 0.7)',
padding: '9px',
marginRight: '19px',
'&:hover': {
background: 'white',
color: 'rgb(25, 118, 210)',
}
}} onClick={logout}>
<MeetingRoomIcon sx={{ marginRight: '5px' }} />
logout</Button>
</div> </div>
<GenericModal isOpen={isModalOpen} onClose={() => setModalOpen(false)}>
<div style={{ padding: '20px' }}>
<h2>Enter URL</h2>
<TextField
label="URL"
variant="outlined"
fullWidth
value={recordingUrl}
onChange={(e: any) => setRecordingUrl(e.target.value)}
style={{ marginBottom: '20px' }}
/>
<Button
variant="contained"
color="primary"
onClick={startRecording}
disabled={!recordingUrl}
>
Submit & Start Recording
</Button>
</div>
</GenericModal>
</> </>
) : "" ) : ""
} }
@@ -133,13 +175,13 @@ export const NavBar = ({ newRecording, recordingName, isRecording }: NavBarProps
const NavBarWrapper = styled.div` const NavBarWrapper = styled.div`
grid-area: navbar; grid-area: navbar;
background-color: #3f4853; background-color: white;
padding:5px; padding:5px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
`; `;
const ProjectName = styled.b` const ProjectName = styled.b`
color: white; color: #3f4853;
font-size: 1.3em; font-size: 1.3em;
`; `;

View File

@@ -14,9 +14,15 @@ import { Schedule, DeleteForever, Edit, PlayCircle } from "@mui/icons-material";
import LinkIcon from '@mui/icons-material/Link'; import LinkIcon from '@mui/icons-material/Link';
import { useGlobalInfoStore } from "../../context/globalInfo"; import { useGlobalInfoStore } from "../../context/globalInfo";
import { deleteRecordingFromStorage, getStoredRecordings } from "../../api/storage"; import { deleteRecordingFromStorage, getStoredRecordings } from "../../api/storage";
import { Typography } from '@mui/material';
/** TODO:
* 1. allow editing existing robot after persisting browser steps
* 2. show robot settings: id, url, etc.
*/
interface Column { interface Column {
id: 'id' | 'interpret' | 'name' | 'createdAt' | 'edit' | 'updatedAt' | 'delete' | 'schedule' | 'integrate'; id: 'interpret' | 'name' | 'delete' | 'schedule' | 'integrate';
label: string; label: string;
minWidth?: number; minWidth?: number;
align?: 'right'; align?: 'right';
@@ -24,20 +30,19 @@ 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 },
{ // {
id: 'createdAt', // id: 'createdAt',
label: 'Created at', // label: 'Created at',
minWidth: 80, // minWidth: 80,
//format: (value: string) => value.toLocaleString('en-US'), // //format: (value: string) => value.toLocaleString('en-US'),
}, // },
{ // {
id: 'edit', // id: 'edit',
label: 'Edit', // label: 'Edit',
minWidth: 80, // minWidth: 80,
}, // },
{ {
id: 'schedule', id: 'schedule',
label: 'Schedule', label: 'Schedule',
@@ -48,12 +53,12 @@ const columns: readonly Column[] = [
label: 'Integrate', label: 'Integrate',
minWidth: 80, minWidth: 80,
}, },
{ // {
id: 'updatedAt', // id: 'updatedAt',
label: 'Updated at', // label: 'Updated at',
minWidth: 80, // minWidth: 80,
//format: (value: string) => value.toLocaleString('en-US'), // //format: (value: string) => value.toLocaleString('en-US'),
}, // },
{ {
id: 'delete', id: 'delete',
label: 'Delete', label: 'Delete',
@@ -121,7 +126,10 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
return ( return (
<React.Fragment> <React.Fragment>
<TableContainer component={Paper} sx={{ width: '100%', overflow: 'hidden' }}> <Typography variant="h6" gutterBottom component="div">
My Robots
</Typography>
<TableContainer component={Paper} sx={{ width: '100%', overflow: 'hidden', marginTop: '15px' }}>
<Table stickyHeader aria-label="sticky table"> <Table stickyHeader aria-label="sticky table">
<TableHead> <TableHead>
<TableRow> <TableRow>
@@ -159,16 +167,16 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
<InterpretButton handleInterpret={() => handleRunRecording(row.id, 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.id, row.name); // handleEditRecording(row.id, row.name);
}} sx={{ '&:hover': { color: '#1976d2', backgroundColor: 'transparent' } }}> // }} sx={{ '&:hover': { color: '#1976d2', backgroundColor: 'transparent' } }}>
<Edit /> // <Edit />
</IconButton> // </IconButton>
</TableCell> // </TableCell>
); // );
case 'schedule': case 'schedule':
return ( return (
<TableCell key={column.id} align={column.align}> <TableCell key={column.id} align={column.align}>

View File

@@ -1,4 +1,4 @@
import { Box, Tabs, Typography, Tab } from "@mui/material"; import { Box, Tabs, Typography, Tab, Paper } from "@mui/material";
import Highlight from "react-highlight"; import Highlight from "react-highlight";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import * as React from "react"; import * as React from "react";
@@ -8,8 +8,14 @@ import SettingsIcon from '@mui/icons-material/Settings';
import ImageIcon from '@mui/icons-material/Image'; import ImageIcon from '@mui/icons-material/Image';
import ArticleIcon from '@mui/icons-material/Article'; import ArticleIcon from '@mui/icons-material/Article';
import { Buffer } from 'buffer'; import { Buffer } from 'buffer';
import { useEffect } from "react"; import { useEffect, useState } from "react";
import AssignmentIcon from '@mui/icons-material/Assignment'; import AssignmentIcon from '@mui/icons-material/Assignment';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
interface RunContentProps { interface RunContentProps {
row: Data, row: Data,
@@ -21,19 +27,34 @@ interface RunContentProps {
export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRef, abortRunHandler }: RunContentProps) => { export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRef, abortRunHandler }: RunContentProps) => {
const [tab, setTab] = React.useState<string>('log'); const [tab, setTab] = React.useState<string>('log');
const [tableData, setTableData] = useState<any[]>([]);
const [columns, setColumns] = useState<string[]>([]);
useEffect(() => { useEffect(() => {
setTab(tab); setTab(tab);
}, [interpretationInProgress]) }, [interpretationInProgress])
useEffect(() => {
if (row.serializableOutput && Object.keys(row.serializableOutput).length > 0) {
const firstKey = Object.keys(row.serializableOutput)[0];
const data = row.serializableOutput[firstKey];
if (Array.isArray(data)) {
setTableData(data);
if (data.length > 0) {
setColumns(Object.keys(data[0]));
}
}
}
}, [row.serializableOutput]);
return ( return (
<Box sx={{ width: '100%' }}> <Box sx={{ width: '100%' }}>
<TabContext value={tab}> <TabContext value={tab}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}> <Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tab} onChange={(e, newTab) => setTab(newTab)} aria-label="run-content-tabs"> <Tabs value={tab} onChange={(e, newTab) => setTab(newTab)} aria-label="run-content-tabs">
<Tab label="Log" value='log' /> {/* <Tab label="Log" value='log' /> */}
<Tab label="Input" value='input' /> <Tab label="Input" value='input' />
<Tab label="Output" value='output' /> <Tab label="Output Data" value='output' />
</Tabs> </Tabs>
</Box> </Box>
<TabPanel value='log'> <TabPanel value='log'>
@@ -107,29 +128,52 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
<div> <div>
<Typography variant='h6' sx={{ display: 'flex', alignItems: 'center' }}> <Typography variant='h6' sx={{ display: 'flex', alignItems: 'center' }}>
<ArticleIcon sx={{ marginRight: '15px' }} /> <ArticleIcon sx={{ marginRight: '15px' }} />
Serializable output</Typography> Serializable output
</Typography>
{Object.keys(row.serializableOutput).map((key) => { {Object.keys(row.serializableOutput).map((key) => {
return ( return (
<div key={`number-of-serializable-output-${key}`}> <div key={`number-of-serializable-output-${key}`}>
<Typography> <Typography>
{key}: {key}:
<a href={`data:application/json;utf8,${JSON.stringify(row.serializableOutput[key], null, 2)}`} <a href={`data:application/json;utf8,${JSON.stringify(row.serializableOutput[key], null, 2)}`}
download={key} style={{ margin: '10px' }}>Download</a> download={key} style={{ margin: '10px' }}>Download as JSON</a>
</Typography> </Typography>
<Box sx={{
width: 'fit-content',
background: 'rgba(0,0,0,0.06)',
maxHeight: '300px',
overflow: 'scroll',
}}>
<pre key={`serializable-output-${key}`}>
{row.serializableOutput[key] ? JSON.stringify(row.serializableOutput[key], null, 2)
: 'The output is empty.'}
</pre>
</Box>
</div> </div>
) )
})} })}
{tableData.length > 0 ? (
<TableContainer component={Paper} sx={{ maxHeight: 440, marginTop: 2 }}>
<Table stickyHeader aria-label="sticky table">
<TableHead>
<TableRow>
{columns.map((column) => (
<TableCell key={column}>{column}</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{tableData.map((row, index) => (
<TableRow key={index}>
{columns.map((column) => (
<TableCell key={column}>{row[column]}</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
) : (
<Box sx={{
width: 'fit-content',
background: 'rgba(0,0,0,0.06)',
maxHeight: '300px',
overflow: 'scroll',
}}>
<pre>
{JSON.stringify(row.serializableOutput, null, 2)}
</pre>
</Box>
)}
</div> </div>
} }
{row.binaryOutput {row.binaryOutput

View File

@@ -16,7 +16,7 @@ import { Accordion, AccordionSummary, AccordionDetails, Typography } from '@mui/
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
interface Column { interface Column {
id: 'status' | 'name' | 'startedAt' | 'finishedAt' | 'duration' | 'task' | 'runId' | 'delete'; id: 'status' | 'name' | 'startedAt' | 'finishedAt' | 'runId' | 'delete';
label: string; label: string;
minWidth?: number; minWidth?: number;
align?: 'right'; align?: 'right';
@@ -25,12 +25,11 @@ interface Column {
export const columns: readonly Column[] = [ export const columns: readonly Column[] = [
{ id: 'status', label: 'Status', minWidth: 80 }, { id: 'status', label: 'Status', minWidth: 80 },
{ id: 'name', label: 'Name', minWidth: 80 }, { id: 'name', label: 'Robot Name', minWidth: 80 },
{ id: 'startedAt', label: 'Started at', minWidth: 80 }, { id: 'startedAt', label: 'Started at', minWidth: 80 },
{ id: 'finishedAt', label: 'Finished at', minWidth: 80 }, { id: 'finishedAt', label: 'Finished at', minWidth: 80 },
{ id: 'duration', label: 'Duration', minWidth: 80 }, { id: 'runId', label: 'Run ID', minWidth: 80 },
{ id: 'runId', label: 'Run id', minWidth: 80 }, // { id: 'task', label: 'Task', minWidth: 80 },
{ id: 'task', label: 'Task', minWidth: 80 },
{ id: 'delete', label: 'Delete', minWidth: 80 }, { id: 'delete', label: 'Delete', minWidth: 80 },
]; ];
@@ -40,8 +39,7 @@ export interface Data {
name: string; name: string;
startedAt: string; startedAt: string;
finishedAt: string; finishedAt: string;
duration: string; // task: string;
task: string;
log: string; log: string;
runId: string; runId: string;
interpreterSettings: RunSettings; interpreterSettings: RunSettings;
@@ -78,7 +76,6 @@ export const RunsTable = (
if (runs) { if (runs) {
const parsedRows: Data[] = []; const parsedRows: Data[] = [];
runs.map((run: any, index) => { runs.map((run: any, index) => {
// const run = JSON.parse(run);
parsedRows.push({ parsedRows.push({
id: index, id: index,
...run, ...run,

View File

@@ -8,6 +8,7 @@ import { TextField, Typography } from "@mui/material";
import { WarningText } from "../atoms/texts"; import { WarningText } from "../atoms/texts";
import NotificationImportantIcon from "@mui/icons-material/NotificationImportant"; import NotificationImportantIcon from "@mui/icons-material/NotificationImportant";
import FlagIcon from '@mui/icons-material/Flag'; import FlagIcon from '@mui/icons-material/Flag';
import { DoneAll } from '@mui/icons-material'
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
interface SaveRecordingProps { interface SaveRecordingProps {
@@ -68,26 +69,32 @@ export const SaveRecording = ({ fileName }: SaveRecordingProps) => {
return ( return (
<div> <div>
<Button sx={{ <IconButton sx={{
width: '100px', width: '140px',
background: 'white', background: 'green',
color: 'rgba(0,128,0,0.7)', color: 'white',
'&:hover': { background: 'white', color: 'green' }, '&:hover': { background: 'green', color: 'white' },
padding: '11px', padding: '13px',
marginRight: '10px', marginRight: '10px',
borderRadius: '5px',
fontFamily: '"Roboto","Helvetica","Arial",sans-serif',
fontWeight: '500',
fontSize: '0.875rem',
lineHeight: '1.75',
letterSpacing: '0.02857em',
}} onClick={() => setOpenModal(true)}> }} onClick={() => setOpenModal(true)}>
<FlagIcon sx={{ marginRight: '3px' }} /> FINISH <DoneAll sx={{ marginRight: '5px' }} /> Finish
</Button> </IconButton>
<GenericModal isOpen={openModal} onClose={() => setOpenModal(false)} modalStyle={modalStyle}> <GenericModal isOpen={openModal} onClose={() => setOpenModal(false)} modalStyle={modalStyle}>
<form onSubmit={handleSaveRecording} style={{ paddingTop: '50px', display: 'flex', flexDirection: 'column' }} > <form onSubmit={handleSaveRecording} style={{ paddingTop: '20px', display: 'flex', flexDirection: 'column' }} >
<Typography>Save the recording as:</Typography> <Typography variant="h5">Save the robot as</Typography>
<TextField <TextField
required required
sx={{ width: '250px', paddingBottom: '10px', margin: '15px 0px' }} sx={{ width: '250px', paddingBottom: '10px', margin: '15px 0px' }}
onChange={handleChangeOfTitle} onChange={handleChangeOfTitle}
id="title" id="title"
label="Recording title" label="Robot Name"
variant="outlined" variant="outlined"
defaultValue={recordingName ? recordingName : null} defaultValue={recordingName ? recordingName : null}
/> />
@@ -97,7 +104,7 @@ export const SaveRecording = ({ fileName }: SaveRecordingProps) => {
<Button color="error" variant="contained" onClick={saveRecording}>Confirm</Button> <Button color="error" variant="contained" onClick={saveRecording}>Confirm</Button>
<WarningText> <WarningText>
<NotificationImportantIcon color="warning" /> <NotificationImportantIcon color="warning" />
Recording already exists, please confirm the recording's overwrite. Robot with this name already exists, please confirm the Robot's overwrite.
</WarningText> </WarningText>
</React.Fragment>) </React.Fragment>)
: <Button type="submit" variant="contained">Save</Button> : <Button type="submit" variant="contained">Save</Button>

View File

@@ -1,12 +1,12 @@
import { useState, useCallback, useEffect, } from 'react'; import React, { useState, useEffect, useCallback, useRef } from 'react';
import type { SyntheticEvent, } from 'react'; import type { SyntheticEvent } from 'react';
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight'; import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
import { NavBarForm, NavBarInput } from "../atoms/form"; import { NavBarForm, NavBarInput } from "../atoms/form";
import { UrlFormButton } from "../atoms/buttons/buttons"; import { UrlFormButton } from "../atoms/buttons/buttons";
import { useSocketStore } from '../../context/socket'; import { useSocketStore } from '../../context/socket';
import { Socket } from "socket.io-client"; import { Socket } from "socket.io-client";
// TODO: Bring back REFRESHHHHHHH
type Props = { type Props = {
currentAddress: string; currentAddress: string;
handleRefresh: (socket: Socket) => void; handleRefresh: (socket: Socket) => void;
@@ -18,45 +18,43 @@ export const UrlForm = ({
handleRefresh, handleRefresh,
setCurrentAddress, setCurrentAddress,
}: Props) => { }: Props) => {
// states:
const [address, setAddress] = useState<string>(currentAddress); const [address, setAddress] = useState<string>(currentAddress);
// context:
const { socket } = useSocketStore(); const { socket } = useSocketStore();
const lastSubmittedRef = useRef<string>('');
const areSameAddresses = address === currentAddress;
const onChange = useCallback((event: SyntheticEvent): void => { const onChange = useCallback((event: SyntheticEvent): void => {
setAddress((event.target as HTMLInputElement).value); setAddress((event.target as HTMLInputElement).value);
}, [address]); }, []);
const submitForm = useCallback((url: string): void => {
// Add protocol if missing
if (!/^(?:f|ht)tps?\:\/\//.test(url)) {
url = "https://" + url;
setAddress(url); // Update the input field to reflect protocol addition
}
try {
// Validate the URL
new URL(url);
setCurrentAddress(url);
lastSubmittedRef.current = url; // Update the last submitted URL
} catch (e) {
alert(`ERROR: ${url} is not a valid url!`);
}
}, [setCurrentAddress]);
const onSubmit = (event: SyntheticEvent): void => { const onSubmit = (event: SyntheticEvent): void => {
event.preventDefault(); event.preventDefault();
let url = address; submitForm(address);
// add protocol if missing
if (!/^(?:f|ht)tps?\:\/\//.test(address)) {
url = "https://" + address;
setAddress(url);
}
if (areSameAddresses) {
if (socket) {
handleRefresh(socket);
}
} else {
try {
// try the validity of url
new URL(url);
setCurrentAddress(url);
} catch (e) {
alert(`ERROR: ${url} is not a valid url!`);
}
}
}; };
// Sync internal state with currentAddress prop when it changes and auto-submit once
useEffect(() => { useEffect(() => {
setAddress(currentAddress) setAddress(currentAddress);
}, [currentAddress]); if (currentAddress !== '' && currentAddress !== lastSubmittedRef.current) {
submitForm(currentAddress);
}
}, [currentAddress, submitForm]);
return ( return (
<NavBarForm onSubmit={onSubmit}> <NavBarForm onSubmit={onSubmit}>

View File

@@ -89,8 +89,9 @@ const ApiKeyManager = () => {
return ( return (
<Container> <Container>
<Typography variant="h5" sx={{ marginBottom: '20px'}}>Manage Your API Key</Typography> <Typography variant="h6" gutterBottom component="div" style={{ marginBottom: '20px', alignSelf: 'flex-start' }}>
Manage Your API Key
</Typography>
{apiKey ? ( {apiKey ? (
<TableContainer component={Paper} sx={{ width: '100%', overflow: 'hidden' }}> <TableContainer component={Paper} sx={{ width: '100%', overflow: 'hidden' }}>
<Table> <Table>
@@ -106,18 +107,18 @@ const ApiKeyManager = () => {
<TableCell>{apiKeyName}</TableCell> <TableCell>{apiKeyName}</TableCell>
<TableCell>{showKey ? `${apiKey?.substring(0, 10)}...` : '***************'}</TableCell> <TableCell>{showKey ? `${apiKey?.substring(0, 10)}...` : '***************'}</TableCell>
<TableCell> <TableCell>
<Tooltip title="Copy API Key"> <Tooltip title="Copy">
<IconButton onClick={copyToClipboard}> <IconButton onClick={copyToClipboard}>
<ContentCopy /> <ContentCopy />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Tooltip title={showKey ? 'Hide API Key' : 'Show API Key'}> <Tooltip title={showKey ? 'Hide' : 'Show'}>
<IconButton onClick={() => setShowKey(!showKey)}> <IconButton onClick={() => setShowKey(!showKey)}>
<Visibility /> <Visibility />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Tooltip title="Delete API Key"> <Tooltip title="Delete">
<IconButton onClick={deleteApiKey} color="error"> <IconButton onClick={deleteApiKey}>
<Delete /> <Delete />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
@@ -129,7 +130,7 @@ const ApiKeyManager = () => {
) : ( ) : (
<> <>
<Typography>You haven't generated an API key yet.</Typography> <Typography>You haven't generated an API key yet.</Typography>
<Button onClick={generateApiKey} variant="contained" color="primary" sx={{ marginTop: '15px'}}> <Button onClick={generateApiKey} variant="contained" color="primary" sx={{ marginTop: '15px' }}>
Generate API Key Generate API Key
</Button> </Button>
</> </>

View File

@@ -6,7 +6,9 @@ import { useBrowserDimensionsStore } from "../../context/browserDimensions";
import { BrowserTabs } from "../molecules/BrowserTabs"; import { BrowserTabs } from "../molecules/BrowserTabs";
import { useSocketStore } from "../../context/socket"; import { useSocketStore } from "../../context/socket";
import { getCurrentTabs, getCurrentUrl, interpretCurrentRecording } from "../../api/recording"; import { getCurrentTabs, getCurrentUrl, interpretCurrentRecording } from "../../api/recording";
import { Box } from '@mui/material';
// TODO: Tab !show currentUrl after recordingUrl global state
export const BrowserContent = () => { export const BrowserContent = () => {
const { width } = useBrowserDimensionsStore(); const { width } = useBrowserDimensionsStore();
const { socket } = useSocketStore(); const { socket } = useSocketStore();
@@ -113,7 +115,7 @@ export const BrowserContent = () => {
}, []) }, [])
return ( return (
<BrowserContentWrapper> <>
<BrowserTabs <BrowserTabs
tabs={tabs} tabs={tabs}
handleTabChange={handleTabChange} handleTabChange={handleTabChange}
@@ -124,14 +126,13 @@ export const BrowserContent = () => {
/> />
<BrowserNavBar <BrowserNavBar
// todo: use width from browser dimension once fixed // todo: use width from browser dimension once fixed
browserWidth={1270} browserWidth={900}
handleUrlChanged={handleUrlChanged} handleUrlChanged={handleUrlChanged}
/> />
<BrowserWindow/> <BrowserWindow/>
</BrowserContentWrapper> </>
); );
} }
const BrowserContentWrapper = styled.div` const BrowserContentWrapper = styled.div`
grid-area: browser;
`; `;

View File

@@ -63,7 +63,7 @@ export const LeftSidePanel = (
if (id) { if (id) {
fetchWorkflow(id, workflowHandler); fetchWorkflow(id, workflowHandler);
} }
}, (1000 * 60 * 15)); }, (900 * 60 * 15));
return () => clearInterval(interval) return () => clearInterval(interval)
}, [id]); }, [id]);

View File

@@ -19,10 +19,11 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
<Paper <Paper
sx={{ sx={{
height: 'auto', height: 'auto',
maxWidth: 'fit-content', width: '250px',
backgroundColor: 'lightgray', backgroundColor: 'white',
paddingTop: '2rem', paddingTop: '2rem',
}} }}
variant="outlined"
> >
<Box sx={{ <Box sx={{
width: '100%', width: '100%',
@@ -38,7 +39,7 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
<Tab sx={{ <Tab sx={{
alignItems: 'baseline', alignItems: 'baseline',
fontSize: 'medium', fontSize: 'medium',
}} value="recordings" label="Recordings" /> }} value="recordings" label="Robots" />
<Tab sx={{ <Tab sx={{
alignItems: 'baseline', alignItems: 'baseline',
fontSize: 'medium', fontSize: 'medium',

View File

@@ -8,8 +8,7 @@ const FormContainer = styled(Box)({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
gap: '16px', gap: '16px',
padding: '20px', marginLeft: '30px'
borderRadius: '8px',
}); });
const FormControl = styled(Box)({ const FormControl = styled(Box)({
@@ -85,10 +84,10 @@ const ProxyForm: React.FC = () => {
return ( return (
<FormContainer> <FormContainer>
<form onSubmit={handleSubmit}> <Typography variant="h6" gutterBottom component="div" style={{ marginTop: '20px' }}>
<Typography variant="subtitle1" gutterBottom style={{ marginBottom: '20px', marginTop: '20px' }}> Proxy Configuration
Proxy Configuration </Typography>
</Typography> <Box component="form" onSubmit={handleSubmit} sx={{ maxWidth: 400, width: '100%' }}>
<FormControl> <FormControl>
<TextField <TextField
label="Proxy Server URL" label="Proxy Server URL"
@@ -98,9 +97,9 @@ const ProxyForm: React.FC = () => {
fullWidth fullWidth
required required
error={!!errors.server_url} error={!!errors.server_url}
helperText={errors.server_url || `Proxy to be used for all requests. helperText={errors.server_url || `Proxy to be used for all robots. HTTP and SOCKS proxies are supported.
HTTP and SOCKS proxies are supported, for example http://myproxy.com:3128 or Example http://myproxy.com:3128 or socks5://myproxy.com:3128.
socks5://myproxy.com:3128. Short form myproxy.com:3128 is considered an HTTP proxy.`} Short form myproxy.com:3128 is considered an HTTP proxy.`}
/> />
</FormControl> </FormControl>
<FormControl> <FormControl>
@@ -147,7 +146,7 @@ const ProxyForm: React.FC = () => {
> >
Add Proxy Add Proxy
</Button> </Button>
</form> </Box>
</FormContainer> </FormContainer>
); );
}; };

View File

@@ -20,6 +20,8 @@ interface GlobalInfo {
setRecordingId: (newId: string | null) => void; setRecordingId: (newId: string | null) => void;
recordingName: string; recordingName: string;
setRecordingName: (recordingName: string) => void; setRecordingName: (recordingName: string) => void;
recordingUrl: string;
setRecordingUrl: (recordingUrl: string) => void;
}; };
class GlobalInfoStore implements Partial<GlobalInfo> { class GlobalInfoStore implements Partial<GlobalInfo> {
@@ -35,6 +37,7 @@ class GlobalInfoStore implements Partial<GlobalInfo> {
recordings: string[] = []; recordings: string[] = [];
rerenderRuns = false; rerenderRuns = false;
recordingName = ''; recordingName = '';
recordingUrl = 'https://';
}; };
const globalInfoStore = new GlobalInfoStore(); const globalInfoStore = new GlobalInfoStore();
@@ -51,6 +54,7 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => {
const [recordingLength, setRecordingLength] = useState<number>(globalInfoStore.recordingLength); const [recordingLength, setRecordingLength] = useState<number>(globalInfoStore.recordingLength);
const [recordingId, setRecordingId] = useState<string | null>(globalInfoStore.recordingId); const [recordingId, setRecordingId] = useState<string | null>(globalInfoStore.recordingId);
const [recordingName, setRecordingName] = useState<string>(globalInfoStore.recordingName); const [recordingName, setRecordingName] = useState<string>(globalInfoStore.recordingName);
const [recordingUrl, setRecordingUrl] = useState<string>(globalInfoStore.recordingUrl);
const notify = (severity: 'error' | 'warning' | 'info' | 'success', message: string) => { const notify = (severity: 'error' | 'warning' | 'info' | 'success', message: string) => {
setNotification({ severity, message, isOpen: true }); setNotification({ severity, message, isOpen: true });
@@ -86,7 +90,9 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => {
recordingId, recordingId,
setRecordingId, setRecordingId,
recordingName, recordingName,
setRecordingName setRecordingName,
recordingUrl,
setRecordingUrl,
}} }}
> >
{children} {children}

View File

@@ -104,7 +104,6 @@ const Login = () => {
)} )}
</Button> </Button>
{/* Redirect to Register */}
<Typography variant="body2" align="center"> <Typography variant="body2" align="center">
Dont have an account?{' '} Dont have an account?{' '}
<Link to="/register" style={{ textDecoration: 'none', color: '#1976d2' }}> <Link to="/register" style={{ textDecoration: 'none', color: '#1976d2' }}>

View File

@@ -158,7 +158,7 @@ export const MainPage = ({ handleEditRecording }: MainPageProps) => {
} }
return ( return (
<Stack direction='row' spacing={0} sx={{ minHeight: '800px' }}> <Stack direction='row' spacing={0} sx={{ minHeight: '900px' }}>
<MainMenu value={content} handleChangeContent={setContent} /> <MainMenu value={content} handleChangeContent={setContent} />
{DisplayContent()} {DisplayContent()}
</Stack> </Stack>

View File

@@ -50,64 +50,66 @@ const Register = () => {
}; };
return ( return (
<Container maxWidth="sm" sx={{ mt: 8 }}> <Box
<Box sx={{
sx={{ display: 'flex',
display: 'flex', flexDirection: 'column',
flexDirection: 'column', alignItems: 'center',
alignItems: 'center', mt: 5,
}} }}
> >
<Typography component="h1" variant="h5"> <Typography variant="h4" gutterBottom>
Create an account Create an account
</Typography>
<Box component="form" onSubmit={submitForm} sx={{ maxWidth: 400, width: '100%' }}>
<TextField
margin="normal"
required
fullWidth
id="email"
label="Email Address"
name="email"
value={email}
onChange={handleChange}
autoComplete="email"
/>
<TextField
margin="normal"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
value={password}
onChange={handleChange}
autoComplete="current-password"
/>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
sx={{ mt: 3, mb: 2 }}
disabled={loading || !email || !password}
>
{loading ? (
<>
<CircularProgress size={20} sx={{ mr: 2 }} />
Loading
</>
) : (
'Register'
)}
</Button>
<Typography variant="body2" align="center">
Already have an account?{' '}
<Link to="/login" style={{ textDecoration: 'none', color: '#1976d2' }}>
Login
</Link>
</Typography> </Typography>
<Box component="form" onSubmit={submitForm} sx={{ mt: 1 }}>
<TextField
margin="normal"
required
fullWidth
id="email"
label="Email Address"
name="email"
value={email}
onChange={handleChange}
autoComplete="email"
/>
<TextField
margin="normal"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
value={password}
onChange={handleChange}
autoComplete="current-password"
/>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
sx={{ mt: 3, mb: 2 }}
disabled={loading || !email || !password}
>
{loading ? (
<CircularProgress size={24} sx={{ color: '#fff' }} />
) : (
'Register'
)}
</Button>
<Typography variant="body2" align="center">
Already have an account?{' '}
<Link to="/login" style={{ textDecoration: 'none', color: '#1976d2' }}>
Login
</Link>
</Typography>
</Box>
</Box> </Box>
</Container> </Box>
); );
}; };