@@ -106,7 +106,7 @@ router.get("/robots/:id", requireAPIKey, async (req: Request, res: Response) =>
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: Format runs to send more data formatted
|
||||
// TODO: Format runs to send more data formatted
|
||||
router.get("/robots/:id/runs", requireAPIKey, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const runs = await Run.findAll({
|
||||
|
||||
@@ -275,7 +275,7 @@ export class RemoteBrowser {
|
||||
if (page) {
|
||||
await this.stopScreencast();
|
||||
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.socket.emit('urlChanged', this.currentPage.url());
|
||||
await this.makeAndEmitScreenshot();
|
||||
|
||||
@@ -33,13 +33,13 @@ const defaultModalStyle = {
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: '50%',
|
||||
width: 500,
|
||||
bgcolor: 'background.paper',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
height: '30%',
|
||||
display: 'block',
|
||||
overflow: 'hidden',
|
||||
overflow: 'scroll',
|
||||
padding: '5px 25px 10px 25px',
|
||||
zIndex: 3147483647,
|
||||
};
|
||||
};
|
||||
@@ -12,6 +12,7 @@ import { UrlForm } from './UrlForm';
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useSocketStore } from "../../context/socket";
|
||||
import { getCurrentUrl } from "../../api/recording";
|
||||
import { useGlobalInfoStore } from '../../context/globalInfo';
|
||||
|
||||
const StyledNavBar = styled.div<{ browserWidth: number }>`
|
||||
display: flex;
|
||||
@@ -31,8 +32,7 @@ const BrowserNavBar: FC<NavBarProps> = ({
|
||||
}) => {
|
||||
|
||||
const { socket } = useSocketStore();
|
||||
|
||||
const [currentUrl, setCurrentUrl] = useState<string>('https://');
|
||||
const { recordingUrl, setRecordingUrl } = useGlobalInfoStore();
|
||||
|
||||
const handleRefresh = useCallback((): void => {
|
||||
socket?.emit('input:refresh');
|
||||
@@ -44,14 +44,13 @@ const BrowserNavBar: FC<NavBarProps> = ({
|
||||
|
||||
const handleCurrentUrlChange = useCallback((url: string) => {
|
||||
handleUrlChanged(url);
|
||||
setCurrentUrl(url);
|
||||
}, [handleUrlChanged, currentUrl]);
|
||||
setRecordingUrl(url);
|
||||
}, [handleUrlChanged, recordingUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
getCurrentUrl().then((response) => {
|
||||
if (response) {
|
||||
handleUrlChanged(response);
|
||||
setCurrentUrl(response);
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.log("Fetching current url failed");
|
||||
@@ -72,12 +71,13 @@ const BrowserNavBar: FC<NavBarProps> = ({
|
||||
const addAddress = (address: string) => {
|
||||
if (socket) {
|
||||
handleUrlChanged(address);
|
||||
setRecordingUrl(address);
|
||||
handleGoTo(address);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledNavBar browserWidth={browserWidth}>
|
||||
<StyledNavBar browserWidth={900}>
|
||||
<NavBarButton
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -111,7 +111,7 @@ const BrowserNavBar: FC<NavBarProps> = ({
|
||||
</NavBarButton>
|
||||
|
||||
<UrlForm
|
||||
currentAddress={currentUrl}
|
||||
currentAddress={recordingUrl}
|
||||
handleRefresh={handleRefresh}
|
||||
setCurrentAddress={addAddress}
|
||||
/>
|
||||
|
||||
@@ -31,7 +31,7 @@ export const BrowserTabs = (
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
width: `${width}px`,
|
||||
width: 800,
|
||||
display: 'flex',
|
||||
overflow: 'auto',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -208,4 +208,4 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
|
||||
</SwipeableDrawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,12 @@ import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||
import { Button, IconButton } from "@mui/material";
|
||||
import { RecordingIcon } from "../atoms/RecorderIcon";
|
||||
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 { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { AuthContext } from '../../context/auth';
|
||||
import { GenericModal } from '../atoms/GenericModal';
|
||||
import TextField from '@mui/material/TextField';
|
||||
|
||||
interface NavBarProps {
|
||||
newRecording: () => void;
|
||||
@@ -17,11 +19,15 @@ interface NavBarProps {
|
||||
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 { user } = state;
|
||||
const [isModalOpen, setModalOpen] = useState(false);
|
||||
|
||||
console.log(`Recording URL: ${recordingUrl}`)
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const logout = async () => {
|
||||
@@ -48,9 +54,14 @@ export const NavBar = ({ newRecording, recordingName, isRecording }: NavBarProps
|
||||
setBrowserId(null);
|
||||
await stopRecording(browserId);
|
||||
}
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const startRecording = () => {
|
||||
setModalOpen(false);
|
||||
newRecording();
|
||||
notify('info', 'New Recording started');
|
||||
}
|
||||
notify('info', 'New Recording started for ' + recordingUrl);
|
||||
};
|
||||
|
||||
return (
|
||||
<NavBarWrapper>
|
||||
@@ -68,61 +79,92 @@ export const NavBar = ({ newRecording, recordingName, isRecording }: NavBarProps
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
}}>
|
||||
<IconButton
|
||||
aria-label="new"
|
||||
size={"small"}
|
||||
onClick={handleNewRecording}
|
||||
sx={{
|
||||
width: isRecording ? '100px' : '130px',
|
||||
{
|
||||
!isRecording ? (
|
||||
<>
|
||||
<IconButton
|
||||
aria-label="new"
|
||||
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',
|
||||
padding: '8px',
|
||||
background: 'white',
|
||||
color: 'rgba(255,0,0,0.7)',
|
||||
background: 'red',
|
||||
color: 'white',
|
||||
marginRight: '10px',
|
||||
fontFamily: '"Roboto","Helvetica","Arial",sans-serif',
|
||||
fontWeight: '500',
|
||||
fontSize: '0.875rem',
|
||||
lineHeight: '1.75',
|
||||
letterSpacing: '0.02857em',
|
||||
'&:hover': { color: 'red', backgroundColor: 'white' }
|
||||
}
|
||||
}
|
||||
>
|
||||
<Circle sx={{ marginRight: '5px' }} /> {isRecording ? 'NEW' : 'RECORD'}
|
||||
</IconButton>
|
||||
'&:hover': { color: 'white', backgroundColor: 'red' }
|
||||
}} onClick={goToMainMenu}>
|
||||
<Clear sx={{ marginRight: '5px' }} />
|
||||
Discard</IconButton>
|
||||
}
|
||||
{
|
||||
recordingLength > 0
|
||||
? <SaveRecording fileName={recordingName} />
|
||||
: 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>
|
||||
<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`
|
||||
grid-area: navbar;
|
||||
background-color: #3f4853;
|
||||
background-color: white;
|
||||
padding:5px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const ProjectName = styled.b`
|
||||
color: white;
|
||||
color: #3f4853;
|
||||
font-size: 1.3em;
|
||||
`;
|
||||
|
||||
@@ -14,9 +14,15 @@ import { Schedule, DeleteForever, Edit, PlayCircle } from "@mui/icons-material";
|
||||
import LinkIcon from '@mui/icons-material/Link';
|
||||
import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||
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 {
|
||||
id: 'id' | 'interpret' | 'name' | 'createdAt' | 'edit' | 'updatedAt' | 'delete' | 'schedule' | 'integrate';
|
||||
id: 'interpret' | 'name' | 'delete' | 'schedule' | 'integrate';
|
||||
label: string;
|
||||
minWidth?: number;
|
||||
align?: 'right';
|
||||
@@ -24,20 +30,19 @@ interface Column {
|
||||
}
|
||||
|
||||
const columns: readonly Column[] = [
|
||||
{ id: 'id', label: 'ID', minWidth: 80 },
|
||||
{ id: 'interpret', label: 'Run', minWidth: 80 },
|
||||
{ id: 'name', label: 'Name', minWidth: 80 },
|
||||
{
|
||||
id: 'createdAt',
|
||||
label: 'Created at',
|
||||
minWidth: 80,
|
||||
//format: (value: string) => value.toLocaleString('en-US'),
|
||||
},
|
||||
{
|
||||
id: 'edit',
|
||||
label: 'Edit',
|
||||
minWidth: 80,
|
||||
},
|
||||
// {
|
||||
// id: 'createdAt',
|
||||
// label: 'Created at',
|
||||
// minWidth: 80,
|
||||
// //format: (value: string) => value.toLocaleString('en-US'),
|
||||
// },
|
||||
// {
|
||||
// id: 'edit',
|
||||
// label: 'Edit',
|
||||
// minWidth: 80,
|
||||
// },
|
||||
{
|
||||
id: 'schedule',
|
||||
label: 'Schedule',
|
||||
@@ -48,12 +53,12 @@ const columns: readonly Column[] = [
|
||||
label: 'Integrate',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
id: 'updatedAt',
|
||||
label: 'Updated at',
|
||||
minWidth: 80,
|
||||
//format: (value: string) => value.toLocaleString('en-US'),
|
||||
},
|
||||
// {
|
||||
// id: 'updatedAt',
|
||||
// label: 'Updated at',
|
||||
// minWidth: 80,
|
||||
// //format: (value: string) => value.toLocaleString('en-US'),
|
||||
// },
|
||||
{
|
||||
id: 'delete',
|
||||
label: 'Delete',
|
||||
@@ -121,7 +126,10 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
|
||||
|
||||
return (
|
||||
<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">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
@@ -159,16 +167,16 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
|
||||
<InterpretButton handleInterpret={() => handleRunRecording(row.id, row.name, row.params || [])} />
|
||||
</TableCell>
|
||||
);
|
||||
case 'edit':
|
||||
return (
|
||||
<TableCell key={column.id} align={column.align}>
|
||||
<IconButton aria-label="add" size="small" onClick={() => {
|
||||
handleEditRecording(row.id, row.name);
|
||||
}} sx={{ '&:hover': { color: '#1976d2', backgroundColor: 'transparent' } }}>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
);
|
||||
// case 'edit':
|
||||
// return (
|
||||
// <TableCell key={column.id} align={column.align}>
|
||||
// <IconButton aria-label="add" size="small" onClick={() => {
|
||||
// handleEditRecording(row.id, row.name);
|
||||
// }} sx={{ '&:hover': { color: '#1976d2', backgroundColor: 'transparent' } }}>
|
||||
// <Edit />
|
||||
// </IconButton>
|
||||
// </TableCell>
|
||||
// );
|
||||
case 'schedule':
|
||||
return (
|
||||
<TableCell key={column.id} align={column.align}>
|
||||
|
||||
@@ -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 Button from "@mui/material/Button";
|
||||
import * as React from "react";
|
||||
@@ -8,8 +8,14 @@ import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import ImageIcon from '@mui/icons-material/Image';
|
||||
import ArticleIcon from '@mui/icons-material/Article';
|
||||
import { Buffer } from 'buffer';
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
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 {
|
||||
row: Data,
|
||||
@@ -21,19 +27,34 @@ interface RunContentProps {
|
||||
|
||||
export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRef, abortRunHandler }: RunContentProps) => {
|
||||
const [tab, setTab] = React.useState<string>('log');
|
||||
const [tableData, setTableData] = useState<any[]>([]);
|
||||
const [columns, setColumns] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setTab(tab);
|
||||
}, [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 (
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<TabContext value={tab}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<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="Output" value='output' />
|
||||
<Tab label="Output Data" value='output' />
|
||||
</Tabs>
|
||||
</Box>
|
||||
<TabPanel value='log'>
|
||||
@@ -107,29 +128,52 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
|
||||
<div>
|
||||
<Typography variant='h6' sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<ArticleIcon sx={{ marginRight: '15px' }} />
|
||||
Serializable output</Typography>
|
||||
Serializable output
|
||||
</Typography>
|
||||
{Object.keys(row.serializableOutput).map((key) => {
|
||||
return (
|
||||
<div key={`number-of-serializable-output-${key}`}>
|
||||
<Typography>
|
||||
{key}:
|
||||
<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>
|
||||
<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>
|
||||
)
|
||||
})}
|
||||
{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>
|
||||
}
|
||||
{row.binaryOutput
|
||||
|
||||
@@ -16,7 +16,7 @@ import { Accordion, AccordionSummary, AccordionDetails, Typography } from '@mui/
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
|
||||
interface Column {
|
||||
id: 'status' | 'name' | 'startedAt' | 'finishedAt' | 'duration' | 'task' | 'runId' | 'delete';
|
||||
id: 'status' | 'name' | 'startedAt' | 'finishedAt' | 'runId' | 'delete';
|
||||
label: string;
|
||||
minWidth?: number;
|
||||
align?: 'right';
|
||||
@@ -25,12 +25,11 @@ interface Column {
|
||||
|
||||
export const columns: readonly Column[] = [
|
||||
{ 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: 'finishedAt', label: 'Finished at', minWidth: 80 },
|
||||
{ id: 'duration', label: 'Duration', minWidth: 80 },
|
||||
{ id: 'runId', label: 'Run id', minWidth: 80 },
|
||||
{ id: 'task', label: 'Task', minWidth: 80 },
|
||||
{ id: 'runId', label: 'Run ID', minWidth: 80 },
|
||||
// { id: 'task', label: 'Task', minWidth: 80 },
|
||||
{ id: 'delete', label: 'Delete', minWidth: 80 },
|
||||
];
|
||||
|
||||
@@ -40,8 +39,7 @@ export interface Data {
|
||||
name: string;
|
||||
startedAt: string;
|
||||
finishedAt: string;
|
||||
duration: string;
|
||||
task: string;
|
||||
// task: string;
|
||||
log: string;
|
||||
runId: string;
|
||||
interpreterSettings: RunSettings;
|
||||
@@ -78,7 +76,6 @@ export const RunsTable = (
|
||||
if (runs) {
|
||||
const parsedRows: Data[] = [];
|
||||
runs.map((run: any, index) => {
|
||||
// const run = JSON.parse(run);
|
||||
parsedRows.push({
|
||||
id: index,
|
||||
...run,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { TextField, Typography } from "@mui/material";
|
||||
import { WarningText } from "../atoms/texts";
|
||||
import NotificationImportantIcon from "@mui/icons-material/NotificationImportant";
|
||||
import FlagIcon from '@mui/icons-material/Flag';
|
||||
import { DoneAll } from '@mui/icons-material'
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
interface SaveRecordingProps {
|
||||
@@ -68,26 +69,32 @@ export const SaveRecording = ({ fileName }: SaveRecordingProps) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button sx={{
|
||||
width: '100px',
|
||||
background: 'white',
|
||||
color: 'rgba(0,128,0,0.7)',
|
||||
'&:hover': { background: 'white', color: 'green' },
|
||||
padding: '11px',
|
||||
<IconButton sx={{
|
||||
width: '140px',
|
||||
background: 'green',
|
||||
color: 'white',
|
||||
'&:hover': { background: 'green', color: 'white' },
|
||||
padding: '13px',
|
||||
marginRight: '10px',
|
||||
borderRadius: '5px',
|
||||
fontFamily: '"Roboto","Helvetica","Arial",sans-serif',
|
||||
fontWeight: '500',
|
||||
fontSize: '0.875rem',
|
||||
lineHeight: '1.75',
|
||||
letterSpacing: '0.02857em',
|
||||
}} onClick={() => setOpenModal(true)}>
|
||||
<FlagIcon sx={{ marginRight: '3px' }} /> FINISH
|
||||
</Button>
|
||||
<DoneAll sx={{ marginRight: '5px' }} /> Finish
|
||||
</IconButton>
|
||||
|
||||
<GenericModal isOpen={openModal} onClose={() => setOpenModal(false)} modalStyle={modalStyle}>
|
||||
<form onSubmit={handleSaveRecording} style={{ paddingTop: '50px', display: 'flex', flexDirection: 'column' }} >
|
||||
<Typography>Save the recording as:</Typography>
|
||||
<form onSubmit={handleSaveRecording} style={{ paddingTop: '20px', display: 'flex', flexDirection: 'column' }} >
|
||||
<Typography variant="h5">Save the robot as</Typography>
|
||||
<TextField
|
||||
required
|
||||
sx={{ width: '250px', paddingBottom: '10px', margin: '15px 0px' }}
|
||||
onChange={handleChangeOfTitle}
|
||||
id="title"
|
||||
label="Recording title"
|
||||
label="Robot Name"
|
||||
variant="outlined"
|
||||
defaultValue={recordingName ? recordingName : null}
|
||||
/>
|
||||
@@ -97,7 +104,7 @@ export const SaveRecording = ({ fileName }: SaveRecordingProps) => {
|
||||
<Button color="error" variant="contained" onClick={saveRecording}>Confirm</Button>
|
||||
<WarningText>
|
||||
<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>
|
||||
</React.Fragment>)
|
||||
: <Button type="submit" variant="contained">Save</Button>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useState, useCallback, useEffect, } from 'react';
|
||||
import type { SyntheticEvent, } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import type { SyntheticEvent } from 'react';
|
||||
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
|
||||
|
||||
import { NavBarForm, NavBarInput } from "../atoms/form";
|
||||
import { UrlFormButton } from "../atoms/buttons/buttons";
|
||||
import { useSocketStore } from '../../context/socket';
|
||||
import { Socket } from "socket.io-client";
|
||||
|
||||
// TODO: Bring back REFRESHHHHHHH
|
||||
type Props = {
|
||||
currentAddress: string;
|
||||
handleRefresh: (socket: Socket) => void;
|
||||
@@ -18,45 +18,43 @@ export const UrlForm = ({
|
||||
handleRefresh,
|
||||
setCurrentAddress,
|
||||
}: Props) => {
|
||||
// states:
|
||||
const [address, setAddress] = useState<string>(currentAddress);
|
||||
// context:
|
||||
const { socket } = useSocketStore();
|
||||
|
||||
const areSameAddresses = address === currentAddress;
|
||||
const lastSubmittedRef = useRef<string>('');
|
||||
|
||||
const onChange = useCallback((event: SyntheticEvent): void => {
|
||||
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 => {
|
||||
event.preventDefault();
|
||||
let url = 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!`);
|
||||
}
|
||||
}
|
||||
submitForm(address);
|
||||
};
|
||||
|
||||
// Sync internal state with currentAddress prop when it changes and auto-submit once
|
||||
useEffect(() => {
|
||||
setAddress(currentAddress)
|
||||
}, [currentAddress]);
|
||||
setAddress(currentAddress);
|
||||
if (currentAddress !== '' && currentAddress !== lastSubmittedRef.current) {
|
||||
submitForm(currentAddress);
|
||||
}
|
||||
}, [currentAddress, submitForm]);
|
||||
|
||||
return (
|
||||
<NavBarForm onSubmit={onSubmit}>
|
||||
@@ -70,4 +68,4 @@ export const UrlForm = ({
|
||||
</UrlFormButton>
|
||||
</NavBarForm>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -89,8 +89,9 @@ const ApiKeyManager = () => {
|
||||
|
||||
return (
|
||||
<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 ? (
|
||||
<TableContainer component={Paper} sx={{ width: '100%', overflow: 'hidden' }}>
|
||||
<Table>
|
||||
@@ -106,18 +107,18 @@ const ApiKeyManager = () => {
|
||||
<TableCell>{apiKeyName}</TableCell>
|
||||
<TableCell>{showKey ? `${apiKey?.substring(0, 10)}...` : '***************'}</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip title="Copy API Key">
|
||||
<Tooltip title="Copy">
|
||||
<IconButton onClick={copyToClipboard}>
|
||||
<ContentCopy />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={showKey ? 'Hide API Key' : 'Show API Key'}>
|
||||
<Tooltip title={showKey ? 'Hide' : 'Show'}>
|
||||
<IconButton onClick={() => setShowKey(!showKey)}>
|
||||
<Visibility />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete API Key">
|
||||
<IconButton onClick={deleteApiKey} color="error">
|
||||
<Tooltip title="Delete">
|
||||
<IconButton onClick={deleteApiKey}>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
@@ -129,7 +130,7 @@ const ApiKeyManager = () => {
|
||||
) : (
|
||||
<>
|
||||
<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
|
||||
</Button>
|
||||
</>
|
||||
|
||||
@@ -6,7 +6,9 @@ import { useBrowserDimensionsStore } from "../../context/browserDimensions";
|
||||
import { BrowserTabs } from "../molecules/BrowserTabs";
|
||||
import { useSocketStore } from "../../context/socket";
|
||||
import { getCurrentTabs, getCurrentUrl, interpretCurrentRecording } from "../../api/recording";
|
||||
import { Box } from '@mui/material';
|
||||
|
||||
// TODO: Tab !show currentUrl after recordingUrl global state
|
||||
export const BrowserContent = () => {
|
||||
const { width } = useBrowserDimensionsStore();
|
||||
const { socket } = useSocketStore();
|
||||
@@ -113,7 +115,7 @@ export const BrowserContent = () => {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<BrowserContentWrapper>
|
||||
<>
|
||||
<BrowserTabs
|
||||
tabs={tabs}
|
||||
handleTabChange={handleTabChange}
|
||||
@@ -124,14 +126,13 @@ export const BrowserContent = () => {
|
||||
/>
|
||||
<BrowserNavBar
|
||||
// todo: use width from browser dimension once fixed
|
||||
browserWidth={1270}
|
||||
browserWidth={900}
|
||||
handleUrlChanged={handleUrlChanged}
|
||||
/>
|
||||
<BrowserWindow/>
|
||||
</BrowserContentWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const BrowserContentWrapper = styled.div`
|
||||
grid-area: browser;
|
||||
`;
|
||||
@@ -63,7 +63,7 @@ export const LeftSidePanel = (
|
||||
if (id) {
|
||||
fetchWorkflow(id, workflowHandler);
|
||||
}
|
||||
}, (1000 * 60 * 15));
|
||||
}, (900 * 60 * 15));
|
||||
return () => clearInterval(interval)
|
||||
}, [id]);
|
||||
|
||||
|
||||
@@ -19,10 +19,11 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
|
||||
<Paper
|
||||
sx={{
|
||||
height: 'auto',
|
||||
maxWidth: 'fit-content',
|
||||
backgroundColor: 'lightgray',
|
||||
width: '250px',
|
||||
backgroundColor: 'white',
|
||||
paddingTop: '2rem',
|
||||
}}
|
||||
variant="outlined"
|
||||
>
|
||||
<Box sx={{
|
||||
width: '100%',
|
||||
@@ -38,7 +39,7 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
|
||||
<Tab sx={{
|
||||
alignItems: 'baseline',
|
||||
fontSize: 'medium',
|
||||
}} value="recordings" label="Recordings" />
|
||||
}} value="recordings" label="Robots" />
|
||||
<Tab sx={{
|
||||
alignItems: 'baseline',
|
||||
fontSize: 'medium',
|
||||
|
||||
@@ -8,8 +8,7 @@ const FormContainer = styled(Box)({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '16px',
|
||||
padding: '20px',
|
||||
borderRadius: '8px',
|
||||
marginLeft: '30px'
|
||||
});
|
||||
|
||||
const FormControl = styled(Box)({
|
||||
@@ -85,10 +84,10 @@ const ProxyForm: React.FC = () => {
|
||||
|
||||
return (
|
||||
<FormContainer>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Typography variant="subtitle1" gutterBottom style={{ marginBottom: '20px', marginTop: '20px' }}>
|
||||
Proxy Configuration
|
||||
</Typography>
|
||||
<Typography variant="h6" gutterBottom component="div" style={{ marginTop: '20px' }}>
|
||||
Proxy Configuration
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ maxWidth: 400, width: '100%' }}>
|
||||
<FormControl>
|
||||
<TextField
|
||||
label="Proxy Server URL"
|
||||
@@ -98,9 +97,9 @@ const ProxyForm: React.FC = () => {
|
||||
fullWidth
|
||||
required
|
||||
error={!!errors.server_url}
|
||||
helperText={errors.server_url || `Proxy to be used for all requests.
|
||||
HTTP and SOCKS proxies are supported, for example http://myproxy.com:3128 or
|
||||
socks5://myproxy.com:3128. Short form myproxy.com:3128 is considered an HTTP proxy.`}
|
||||
helperText={errors.server_url || `Proxy to be used for all robots. HTTP and SOCKS proxies are supported.
|
||||
Example http://myproxy.com:3128 or socks5://myproxy.com:3128.
|
||||
Short form myproxy.com:3128 is considered an HTTP proxy.`}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
@@ -147,7 +146,7 @@ const ProxyForm: React.FC = () => {
|
||||
>
|
||||
Add Proxy
|
||||
</Button>
|
||||
</form>
|
||||
</Box>
|
||||
</FormContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,6 +20,8 @@ interface GlobalInfo {
|
||||
setRecordingId: (newId: string | null) => void;
|
||||
recordingName: string;
|
||||
setRecordingName: (recordingName: string) => void;
|
||||
recordingUrl: string;
|
||||
setRecordingUrl: (recordingUrl: string) => void;
|
||||
};
|
||||
|
||||
class GlobalInfoStore implements Partial<GlobalInfo> {
|
||||
@@ -35,6 +37,7 @@ class GlobalInfoStore implements Partial<GlobalInfo> {
|
||||
recordings: string[] = [];
|
||||
rerenderRuns = false;
|
||||
recordingName = '';
|
||||
recordingUrl = 'https://';
|
||||
};
|
||||
|
||||
const globalInfoStore = new GlobalInfoStore();
|
||||
@@ -51,6 +54,7 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => {
|
||||
const [recordingLength, setRecordingLength] = useState<number>(globalInfoStore.recordingLength);
|
||||
const [recordingId, setRecordingId] = useState<string | null>(globalInfoStore.recordingId);
|
||||
const [recordingName, setRecordingName] = useState<string>(globalInfoStore.recordingName);
|
||||
const [recordingUrl, setRecordingUrl] = useState<string>(globalInfoStore.recordingUrl);
|
||||
|
||||
const notify = (severity: 'error' | 'warning' | 'info' | 'success', message: string) => {
|
||||
setNotification({ severity, message, isOpen: true });
|
||||
@@ -86,7 +90,9 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => {
|
||||
recordingId,
|
||||
setRecordingId,
|
||||
recordingName,
|
||||
setRecordingName
|
||||
setRecordingName,
|
||||
recordingUrl,
|
||||
setRecordingUrl,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -104,7 +104,6 @@ const Login = () => {
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Redirect to Register */}
|
||||
<Typography variant="body2" align="center">
|
||||
Don’t have an account?{' '}
|
||||
<Link to="/register" style={{ textDecoration: 'none', color: '#1976d2' }}>
|
||||
|
||||
@@ -158,7 +158,7 @@ export const MainPage = ({ handleEditRecording }: MainPageProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack direction='row' spacing={0} sx={{ minHeight: '800px' }}>
|
||||
<Stack direction='row' spacing={0} sx={{ minHeight: '900px' }}>
|
||||
<MainMenu value={content} handleChangeContent={setContent} />
|
||||
{DisplayContent()}
|
||||
</Stack>
|
||||
|
||||
@@ -50,64 +50,66 @@ const Register = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Container maxWidth="sm" sx={{ mt: 8 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography component="h1" variant="h5">
|
||||
Create an account
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
mt: 5,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
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>
|
||||
<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>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user