133
src/components/molecules/AddWhatCondModal.tsx
Normal file
133
src/components/molecules/AddWhatCondModal.tsx
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import { WhereWhatPair } from "@wbr-project/wbr-interpret";
|
||||||
|
import { GenericModal } from "../atoms/GenericModal";
|
||||||
|
import { modalStyle } from "./AddWhereCondModal";
|
||||||
|
import { Button, MenuItem, TextField, Typography } from "@mui/material";
|
||||||
|
import React, { useRef } from "react";
|
||||||
|
import { Dropdown as MuiDropdown } from "../atoms/DropdownMui";
|
||||||
|
import { KeyValueForm } from "./KeyValueForm";
|
||||||
|
import { ClearButton } from "../atoms/buttons/ClearButton";
|
||||||
|
import { useSocketStore } from "../../context/socket";
|
||||||
|
|
||||||
|
interface AddWhatCondModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
pair: WhereWhatPair;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddWhatCondModal = ({isOpen, onClose, pair, index}: AddWhatCondModalProps) => {
|
||||||
|
const [action, setAction] = React.useState<string>('');
|
||||||
|
const [objectIndex, setObjectIndex] = React.useState<number>(0);
|
||||||
|
const [args, setArgs] = React.useState<({type: string, value: (string|number|object|unknown)})[]>([]);
|
||||||
|
|
||||||
|
const objectRefs = useRef<({getObject: () => object}|unknown)[]>([]);
|
||||||
|
|
||||||
|
const {socket} = useSocketStore();
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
const argsArray: (string|number|object|unknown)[] = [];
|
||||||
|
args.map((arg, index) => {
|
||||||
|
switch (arg.type) {
|
||||||
|
case 'string':
|
||||||
|
case 'number':
|
||||||
|
argsArray[index] = arg.value;
|
||||||
|
break;
|
||||||
|
case 'object':
|
||||||
|
// @ts-ignore
|
||||||
|
argsArray[index] = objectRefs.current[arg.value].getObject();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setArgs([]);
|
||||||
|
onClose();
|
||||||
|
pair.what.push({
|
||||||
|
// @ts-ignore
|
||||||
|
action,
|
||||||
|
args: argsArray,
|
||||||
|
})
|
||||||
|
socket?.emit('updatePair', {index: index-1, pair: pair});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GenericModal isOpen={isOpen} onClose={() => {
|
||||||
|
setArgs([]);
|
||||||
|
onClose();
|
||||||
|
}} modalStyle={modalStyle}>
|
||||||
|
<div>
|
||||||
|
<Typography sx={{margin: '20px 0px'}}>Add what condition:</Typography>
|
||||||
|
<div style={{margin:'8px'}}>
|
||||||
|
<Typography>Action:</Typography>
|
||||||
|
<TextField
|
||||||
|
size='small'
|
||||||
|
type="string"
|
||||||
|
onChange={(e) => setAction(e.target.value)}
|
||||||
|
value={action}
|
||||||
|
label='action'
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Typography>Add new argument of type:</Typography>
|
||||||
|
<Button onClick={() => setArgs([...args,{type: 'string', value: null}]) }>string</Button>
|
||||||
|
<Button onClick={() => setArgs([...args,{type: 'number', value: null}]) }>number</Button>
|
||||||
|
<Button onClick={() => {
|
||||||
|
setArgs([...args,{type: 'object', value: objectIndex}])
|
||||||
|
setObjectIndex(objectIndex+1);
|
||||||
|
} }>object</Button>
|
||||||
|
</div>
|
||||||
|
<Typography>args:</Typography>
|
||||||
|
{args.map((arg, index) => {
|
||||||
|
// @ts-ignore
|
||||||
|
return (
|
||||||
|
<div style={{border:'solid 1px gray', padding: '10px', display:'flex', flexDirection:'row', alignItems:'center' }}
|
||||||
|
key={`wrapper-for-${arg.type}-${index}`}>
|
||||||
|
<ClearButton handleClick={() => {
|
||||||
|
args.splice(index,1);
|
||||||
|
setArgs([...args]);
|
||||||
|
}}/>
|
||||||
|
<Typography sx={{margin: '5px'}} key={`number-argument-${arg.type}-${index}`}>{index}: </Typography>
|
||||||
|
{arg.type === 'string' ?
|
||||||
|
<TextField
|
||||||
|
size='small'
|
||||||
|
type="string"
|
||||||
|
onChange={(e) => setArgs([
|
||||||
|
...args.slice(0, index),
|
||||||
|
{type: arg.type, value: e.target.value},
|
||||||
|
...args.slice(index + 1)
|
||||||
|
])}
|
||||||
|
value={args[index].value || ''}
|
||||||
|
label="string"
|
||||||
|
key={`arg-${arg.type}-${index}`}
|
||||||
|
/> : arg.type === 'number' ?
|
||||||
|
<TextField
|
||||||
|
key={`arg-${arg.type}-${index}`}
|
||||||
|
size='small'
|
||||||
|
type="number"
|
||||||
|
onChange={(e) => setArgs([
|
||||||
|
...args.slice(0, index),
|
||||||
|
{type: arg.type, value: Number(e.target.value)},
|
||||||
|
...args.slice(index + 1)
|
||||||
|
])}
|
||||||
|
value={args[index].value || ''}
|
||||||
|
label="number"
|
||||||
|
/> :
|
||||||
|
<KeyValueForm ref={el =>
|
||||||
|
//@ts-ignore
|
||||||
|
objectRefs.current[arg.value] = el} key={`arg-${arg.type}-${index}`}/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)})}
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
display: "table-cell",
|
||||||
|
float: "right",
|
||||||
|
marginRight: "15px",
|
||||||
|
marginTop: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{"Add Condition"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</GenericModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
151
src/components/molecules/AddWhereCondModal.tsx
Normal file
151
src/components/molecules/AddWhereCondModal.tsx
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import { Dropdown as MuiDropdown } from "../atoms/DropdownMui";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
MenuItem,
|
||||||
|
Typography
|
||||||
|
} from "@mui/material";
|
||||||
|
import React, { useRef } from "react";
|
||||||
|
import { GenericModal } from "../atoms/GenericModal";
|
||||||
|
import { WhereWhatPair } from "@wbr-project/wbr-interpret";
|
||||||
|
import { SelectChangeEvent } from "@mui/material/Select/Select";
|
||||||
|
import { DisplayConditionSettings } from "./DisplayWhereConditionSettings";
|
||||||
|
import { useSocketStore } from "../../context/socket";
|
||||||
|
|
||||||
|
interface AddWhereCondModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
pair: WhereWhatPair;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddWhereCondModal = ({isOpen, onClose, pair, index}: AddWhereCondModalProps) => {
|
||||||
|
const [whereProp, setWhereProp] = React.useState<string>('');
|
||||||
|
const [additionalSettings, setAdditionalSettings] = React.useState<string>('');
|
||||||
|
const [newValue, setNewValue] = React.useState<any>('');
|
||||||
|
const [checked, setChecked] = React.useState<boolean[]>(new Array(Object.keys(pair.where).length).fill(false));
|
||||||
|
|
||||||
|
const keyValueFormRef = useRef<{getObject: () => object}>(null);
|
||||||
|
|
||||||
|
const {socket} = useSocketStore();
|
||||||
|
|
||||||
|
const handlePropSelect = (event: SelectChangeEvent<string>) => {
|
||||||
|
setWhereProp(event.target.value);
|
||||||
|
switch (event.target.value) {
|
||||||
|
case 'url': setNewValue(''); break;
|
||||||
|
case 'selectors': setNewValue(['']); break;
|
||||||
|
case 'default': return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
switch (whereProp) {
|
||||||
|
case 'url':
|
||||||
|
if (additionalSettings === 'string'){
|
||||||
|
pair.where.url = newValue;
|
||||||
|
} else {
|
||||||
|
pair.where.url = { $regex: newValue };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'selectors':
|
||||||
|
pair.where.selectors = newValue;
|
||||||
|
break;
|
||||||
|
case 'cookies':
|
||||||
|
pair.where.cookies = keyValueFormRef.current?.getObject() as Record<string,string>
|
||||||
|
break;
|
||||||
|
case 'before':
|
||||||
|
pair.where.$before = newValue;
|
||||||
|
break;
|
||||||
|
case 'after':
|
||||||
|
pair.where.$after = newValue;
|
||||||
|
break;
|
||||||
|
case 'boolean':
|
||||||
|
const booleanArr = [];
|
||||||
|
const deleteKeys: string[] = [];
|
||||||
|
for (let i = 0; i < checked.length; i++) {
|
||||||
|
if (checked[i]) {
|
||||||
|
if (Object.keys(pair.where)[i]) {
|
||||||
|
//@ts-ignore
|
||||||
|
if (pair.where[Object.keys(pair.where)[i]]) {
|
||||||
|
booleanArr.push({
|
||||||
|
//@ts-ignore
|
||||||
|
[Object.keys(pair.where)[i]]: pair.where[Object.keys(pair.where)[i]]});
|
||||||
|
}
|
||||||
|
deleteKeys.push(Object.keys(pair.where)[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
deleteKeys.forEach((key: string) => delete pair.where[key]);
|
||||||
|
//@ts-ignore
|
||||||
|
pair.where[`$${additionalSettings}`] = booleanArr;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
setWhereProp('');
|
||||||
|
setAdditionalSettings('');
|
||||||
|
setNewValue('');
|
||||||
|
socket?.emit('updatePair', {index: index-1, pair: pair});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GenericModal isOpen={isOpen} onClose={() => {
|
||||||
|
setWhereProp('');
|
||||||
|
setAdditionalSettings('');
|
||||||
|
setNewValue('');
|
||||||
|
onClose();
|
||||||
|
}} modalStyle={modalStyle}>
|
||||||
|
<div>
|
||||||
|
<Typography sx={{margin: '20px 0px'}}>Add where condition:</Typography>
|
||||||
|
<div style={{margin:'8px'}}>
|
||||||
|
<MuiDropdown
|
||||||
|
id="whereProp"
|
||||||
|
label="Condition"
|
||||||
|
value={whereProp}
|
||||||
|
handleSelect={handlePropSelect}>
|
||||||
|
<MenuItem value="url">url</MenuItem>
|
||||||
|
<MenuItem value="selectors">selectors</MenuItem>
|
||||||
|
<MenuItem value="cookies">cookies</MenuItem>
|
||||||
|
<MenuItem value="before">before</MenuItem>
|
||||||
|
<MenuItem value="after">after</MenuItem>
|
||||||
|
<MenuItem value="boolean">boolean logic</MenuItem>
|
||||||
|
</MuiDropdown>
|
||||||
|
</div>
|
||||||
|
{whereProp ?
|
||||||
|
<div style={{margin: '8px'}}>
|
||||||
|
<DisplayConditionSettings
|
||||||
|
whereProp={whereProp} additionalSettings={additionalSettings} setAdditionalSettings={setAdditionalSettings}
|
||||||
|
newValue={newValue} setNewValue={setNewValue} checked={checked} setChecked={setChecked}
|
||||||
|
keyValueFormRef={keyValueFormRef} whereKeys={Object.keys(pair.where)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
display: "table-cell",
|
||||||
|
float: "right",
|
||||||
|
marginRight: "15px",
|
||||||
|
marginTop: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{"Add Condition"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
</GenericModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const modalStyle = {
|
||||||
|
top: '40%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
width: '30%',
|
||||||
|
backgroundColor: 'background.paper',
|
||||||
|
p: 4,
|
||||||
|
height:'fit-content',
|
||||||
|
display:'block',
|
||||||
|
padding: '20px',
|
||||||
|
};
|
||||||
94
src/components/molecules/ColapsibleRow.tsx
Normal file
94
src/components/molecules/ColapsibleRow.tsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import * as React from "react";
|
||||||
|
import TableRow from "@mui/material/TableRow";
|
||||||
|
import TableCell from "@mui/material/TableCell";
|
||||||
|
import { Box, Collapse, IconButton, Typography } from "@mui/material";
|
||||||
|
import { DeleteForever, KeyboardArrowDown, KeyboardArrowUp } from "@mui/icons-material";
|
||||||
|
import { deleteRunFromStorage } from "../../api/storage";
|
||||||
|
import { columns, Data } from "./RunsTable";
|
||||||
|
import { RunContent } from "./RunContent";
|
||||||
|
|
||||||
|
interface CollapsibleRowProps {
|
||||||
|
row: Data;
|
||||||
|
handleDelete: () => void;
|
||||||
|
isOpen: boolean;
|
||||||
|
currentLog: string;
|
||||||
|
abortRunHandler: () => void;
|
||||||
|
runningRecordingName: string;
|
||||||
|
}
|
||||||
|
export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRunHandler,runningRecordingName }: CollapsibleRowProps) => {
|
||||||
|
const [open, setOpen] = useState(isOpen);
|
||||||
|
|
||||||
|
const logEndRef = useRef<HTMLDivElement|null>(null);
|
||||||
|
|
||||||
|
const scrollToLogBottom = () => {
|
||||||
|
if (logEndRef.current) {
|
||||||
|
logEndRef.current.scrollIntoView({ behavior: "smooth" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAbort = () => {
|
||||||
|
abortRunHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToLogBottom();
|
||||||
|
}, [currentLog])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<TableRow sx={{ '& > *': { borderBottom: 'unset' } }} hover role="checkbox" tabIndex={-1} key={row.id}>
|
||||||
|
<TableCell>
|
||||||
|
<IconButton
|
||||||
|
aria-label="expand row"
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(!open);
|
||||||
|
scrollToLogBottom();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{open ? <KeyboardArrowUp /> : <KeyboardArrowDown />}
|
||||||
|
</IconButton>
|
||||||
|
</TableCell>
|
||||||
|
{columns.map((column) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const value : any = row[column.id];
|
||||||
|
if (value !== undefined) {
|
||||||
|
return (
|
||||||
|
<TableCell key={column.id} align={column.align}>
|
||||||
|
{value}
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
switch (column.id) {
|
||||||
|
case 'delete':
|
||||||
|
return (
|
||||||
|
<TableCell key={column.id} align={column.align}>
|
||||||
|
<IconButton aria-label="add" size= "small" onClick={() => {
|
||||||
|
deleteRunFromStorage(`${row.name}_${row.runId}`).then((result: boolean) => {
|
||||||
|
if (result) {
|
||||||
|
handleDelete();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}} sx={{'&:hover': { color: '#1976d2', backgroundColor: 'transparent' }}}>
|
||||||
|
<DeleteForever/>
|
||||||
|
</IconButton>
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
|
||||||
|
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||||
|
<RunContent row={row} abortRunHandler={handleAbort} currentLog={currentLog}
|
||||||
|
logEndRef={logEndRef} interpretationInProgress={runningRecordingName === row.name} />
|
||||||
|
</Collapse>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
126
src/components/molecules/DisplayWhereConditionSettings.tsx
Normal file
126
src/components/molecules/DisplayWhereConditionSettings.tsx
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Dropdown as MuiDropdown } from "../atoms/DropdownMui";
|
||||||
|
import { Checkbox, FormControlLabel, FormGroup, MenuItem, Stack, TextField } from "@mui/material";
|
||||||
|
import { AddButton } from "../atoms/buttons/AddButton";
|
||||||
|
import { RemoveButton } from "../atoms/buttons/RemoveButton";
|
||||||
|
import { KeyValueForm } from "./KeyValueForm";
|
||||||
|
import { WarningText } from "../atoms/texts";
|
||||||
|
|
||||||
|
interface DisplayConditionSettingsProps {
|
||||||
|
whereProp: string;
|
||||||
|
additionalSettings: string;
|
||||||
|
setAdditionalSettings: (value: any) => void;
|
||||||
|
newValue: any;
|
||||||
|
setNewValue: (value: any) => void;
|
||||||
|
keyValueFormRef: React.RefObject<{getObject: () => object}>;
|
||||||
|
whereKeys: string[];
|
||||||
|
checked: boolean[];
|
||||||
|
setChecked: (value: boolean[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DisplayConditionSettings = (
|
||||||
|
{whereProp, setAdditionalSettings, additionalSettings,
|
||||||
|
setNewValue, newValue, keyValueFormRef, whereKeys, checked, setChecked}
|
||||||
|
: DisplayConditionSettingsProps) => {
|
||||||
|
switch (whereProp) {
|
||||||
|
case 'url':
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<MuiDropdown
|
||||||
|
id="url"
|
||||||
|
label="type"
|
||||||
|
value={additionalSettings}
|
||||||
|
handleSelect={(e) => setAdditionalSettings(e.target.value)}>
|
||||||
|
<MenuItem value="string">string</MenuItem>
|
||||||
|
<MenuItem value="regex">regex</MenuItem>
|
||||||
|
</MuiDropdown>
|
||||||
|
{ additionalSettings ? <TextField
|
||||||
|
size='small'
|
||||||
|
type="string"
|
||||||
|
onChange={(e) => setNewValue(e.target.value)}
|
||||||
|
value={newValue}
|
||||||
|
/> : null}
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
case 'selectors':
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Stack direction='column' spacing={2}>
|
||||||
|
{
|
||||||
|
newValue.map((selector: string, index: number) => {
|
||||||
|
return <TextField
|
||||||
|
key={`whereProp-selector-${index}`}
|
||||||
|
size='small'
|
||||||
|
type="string"
|
||||||
|
onChange={(e) => setNewValue([
|
||||||
|
...newValue.slice(0, index),
|
||||||
|
e.target.value,
|
||||||
|
...newValue.slice(index + 1)
|
||||||
|
])}/>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Stack>
|
||||||
|
<AddButton handleClick={() => setNewValue([...newValue, ''])}/>
|
||||||
|
<RemoveButton handleClick={()=> {
|
||||||
|
const arr = newValue;
|
||||||
|
arr.splice(-1);
|
||||||
|
setNewValue([...arr]);
|
||||||
|
}}/>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
case 'cookies':
|
||||||
|
return <KeyValueForm ref={keyValueFormRef}/>
|
||||||
|
case 'before':
|
||||||
|
return <TextField
|
||||||
|
label='pair id'
|
||||||
|
size='small'
|
||||||
|
type="string"
|
||||||
|
onChange={(e) => setNewValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
case 'after':
|
||||||
|
return <TextField
|
||||||
|
label='pair id'
|
||||||
|
size='small'
|
||||||
|
type="string"
|
||||||
|
onChange={(e) => setNewValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
case 'boolean':
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<MuiDropdown
|
||||||
|
id="boolean"
|
||||||
|
label="operator"
|
||||||
|
value={additionalSettings}
|
||||||
|
handleSelect={(e) => setAdditionalSettings(e.target.value)}>
|
||||||
|
<MenuItem value="and">and</MenuItem>
|
||||||
|
<MenuItem value="or">or</MenuItem>
|
||||||
|
</MuiDropdown>
|
||||||
|
<FormGroup>
|
||||||
|
{
|
||||||
|
whereKeys.map((key: string, index: number) => {
|
||||||
|
return (
|
||||||
|
<FormControlLabel control={
|
||||||
|
<Checkbox
|
||||||
|
checked={checked[index]}
|
||||||
|
onChange={() => setChecked([
|
||||||
|
...checked.slice(0, index),
|
||||||
|
!checked[index],
|
||||||
|
...checked.slice(index + 1)
|
||||||
|
])}
|
||||||
|
key={`checkbox-${key}-${index}`}
|
||||||
|
/>
|
||||||
|
} label={key} key={`control-label-form-${key}-${index}`}/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</FormGroup>
|
||||||
|
<WarningText>
|
||||||
|
Choose at least 2 where conditions. Nesting of boolean operators
|
||||||
|
is possible by adding more conditions.
|
||||||
|
</WarningText>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
178
src/components/molecules/InterpretationButtons.tsx
Normal file
178
src/components/molecules/InterpretationButtons.tsx
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import { Box, Button, IconButton, Stack, Typography } from "@mui/material";
|
||||||
|
import { PauseCircle, PlayCircle, StopCircle } from "@mui/icons-material";
|
||||||
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
|
import { interpretCurrentRecording, stopCurrentInterpretation } from "../../api/recording";
|
||||||
|
import { useSocketStore } from "../../context/socket";
|
||||||
|
import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||||
|
import { GenericModal } from "../atoms/GenericModal";
|
||||||
|
import { WhereWhatPair } from "@wbr-project/wbr-interpret";
|
||||||
|
import HelpIcon from '@mui/icons-material/Help';
|
||||||
|
|
||||||
|
interface InterpretationButtonsProps {
|
||||||
|
enableStepping: (isPaused: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InterpretationInfo {
|
||||||
|
running: boolean;
|
||||||
|
isPaused: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const interpretationInfo: InterpretationInfo = {
|
||||||
|
running: false,
|
||||||
|
isPaused: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsProps) => {
|
||||||
|
const [info, setInfo] = React.useState<InterpretationInfo>(interpretationInfo);
|
||||||
|
const [decisionModal, setDecisionModal] = useState<{
|
||||||
|
pair: WhereWhatPair | null,
|
||||||
|
actionType: string,
|
||||||
|
selector: string,
|
||||||
|
action: string,
|
||||||
|
open:boolean
|
||||||
|
}>({ pair: null, actionType: '', selector: '', action: '', open: false} );
|
||||||
|
|
||||||
|
const { socket } = useSocketStore();
|
||||||
|
const { notify } = useGlobalInfoStore();
|
||||||
|
|
||||||
|
const finishedHandler = useCallback(() => {
|
||||||
|
setInfo({...info, isPaused: false});
|
||||||
|
enableStepping(false);
|
||||||
|
}, [info, enableStepping]);
|
||||||
|
|
||||||
|
const breakpointHitHandler = useCallback(() => {
|
||||||
|
setInfo({running: false, isPaused: true});
|
||||||
|
notify('warning', 'Please restart the interpretation, after updating the recording');
|
||||||
|
enableStepping(true);
|
||||||
|
}, [info, enableStepping]);
|
||||||
|
|
||||||
|
const decisionHandler = useCallback(
|
||||||
|
({pair, actionType, lastData}
|
||||||
|
: {pair: WhereWhatPair | null, actionType: string, lastData: { selector: string, action: string }}) => {
|
||||||
|
const {selector, action} = lastData;
|
||||||
|
setDecisionModal((prevState) => {
|
||||||
|
return {
|
||||||
|
pair,
|
||||||
|
actionType,
|
||||||
|
selector,
|
||||||
|
action,
|
||||||
|
open: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [decisionModal]);
|
||||||
|
|
||||||
|
const handleDecision = (decision: boolean) => {
|
||||||
|
const {pair, actionType} = decisionModal;
|
||||||
|
socket?.emit('decision', {pair, actionType, decision});
|
||||||
|
setDecisionModal({pair: null, actionType: '', selector: '', action: '', open: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDescription = () => {
|
||||||
|
switch (decisionModal.actionType){
|
||||||
|
case 'customAction':
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Typography>
|
||||||
|
Do you want to use the previously recorded selector
|
||||||
|
as a where condition for matching the action?
|
||||||
|
</Typography>
|
||||||
|
<Box style={{marginTop: '4px'}}>
|
||||||
|
[previous action: <b>{decisionModal.action}</b>]
|
||||||
|
<pre>{decisionModal.selector}</pre>
|
||||||
|
</Box>
|
||||||
|
</React.Fragment>);
|
||||||
|
default: return null;}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (socket) {
|
||||||
|
socket.on('finished', finishedHandler);
|
||||||
|
socket.on('breakpointHit', breakpointHitHandler);
|
||||||
|
socket.on('decision', decisionHandler);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
socket?.off('finished', finishedHandler);
|
||||||
|
socket?.off('breakpointHit', breakpointHitHandler);
|
||||||
|
socket?.off('decision', decisionHandler);
|
||||||
|
}
|
||||||
|
}, [socket, finishedHandler, breakpointHitHandler]);
|
||||||
|
|
||||||
|
const handlePlay = async () => {
|
||||||
|
if (info.isPaused) {
|
||||||
|
socket?.emit("resume");
|
||||||
|
setInfo({running: true, isPaused: false});
|
||||||
|
enableStepping(false);
|
||||||
|
} else {
|
||||||
|
setInfo({...info, running: true});
|
||||||
|
const finished = await interpretCurrentRecording();
|
||||||
|
setInfo({...info, running: false});
|
||||||
|
if (finished) {
|
||||||
|
notify('info', 'Interpretation finished');
|
||||||
|
} else {
|
||||||
|
notify('error', 'Interpretation failed to start');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStop = async () => {
|
||||||
|
setInfo({ running: false, isPaused: false });
|
||||||
|
enableStepping(false);
|
||||||
|
await stopCurrentInterpretation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePause = async () => {
|
||||||
|
if (info.running) {
|
||||||
|
socket?.emit("pause");
|
||||||
|
setInfo({ running: false, isPaused: true });
|
||||||
|
notify('warning', 'Please restart the interpretation, after updating the recording');
|
||||||
|
enableStepping(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack direction="row" spacing={3}
|
||||||
|
sx={{ marginTop: '10px', marginBottom: '5px', justifyContent: 'space-evenly',}} >
|
||||||
|
<IconButton disabled={!info.running} sx={{display:'grid', '&:hover': { color: '#1976d2', backgroundColor: 'transparent' }}}
|
||||||
|
aria-label="pause" size="small" title="Pause" onClick={handlePause}>
|
||||||
|
<PauseCircle sx={{ fontSize: 30, justifySelf:'center' }}/>
|
||||||
|
Pause
|
||||||
|
</IconButton>
|
||||||
|
<IconButton disabled={info.running} sx={{display:'grid', '&:hover': { color: '#1976d2', backgroundColor: 'transparent' }}}
|
||||||
|
aria-label="play" size="small" title="Play" onClick={handlePlay}>
|
||||||
|
<PlayCircle sx={{ fontSize: 30, justifySelf:'center' }}/>
|
||||||
|
{info.isPaused ? 'Resume' : 'Start'}
|
||||||
|
</IconButton>
|
||||||
|
<IconButton disabled={!info.running && !info.isPaused} sx={{display:'grid', '&:hover': { color: '#1976d2', backgroundColor: 'transparent' }}}
|
||||||
|
aria-label="stop" size="small" title="Stop" onClick={handleStop}>
|
||||||
|
<StopCircle sx={{ fontSize: 30, justifySelf:'center' }}/>
|
||||||
|
Stop
|
||||||
|
</IconButton>
|
||||||
|
<GenericModal onClose={() => {}} isOpen={decisionModal.open} canBeClosed={false}
|
||||||
|
modalStyle={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
width: 500,
|
||||||
|
background: 'white',
|
||||||
|
border: '2px solid #000',
|
||||||
|
boxShadow: '24',
|
||||||
|
height:'fit-content',
|
||||||
|
display:'block',
|
||||||
|
overflow:'scroll',
|
||||||
|
padding: '5px 25px 10px 25px',
|
||||||
|
}}>
|
||||||
|
<div style={{padding: '15px'}}>
|
||||||
|
<HelpIcon/>
|
||||||
|
{
|
||||||
|
handleDescription()
|
||||||
|
}
|
||||||
|
<div style={{float: 'right'}}>
|
||||||
|
<Button onClick={() => handleDecision(true)} color='success'>yes</Button>
|
||||||
|
<Button onClick={() => handleDecision(false)} color='error'>no</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</GenericModal>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
98
src/components/molecules/InterpretationLog.tsx
Normal file
98
src/components/molecules/InterpretationLog.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import Accordion from '@mui/material/Accordion';
|
||||||
|
import AccordionDetails from '@mui/material/AccordionDetails';
|
||||||
|
import AccordionSummary from '@mui/material/AccordionSummary';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
|
import Highlight from 'react-highlight'
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { useSocketStore } from "../../context/socket";
|
||||||
|
|
||||||
|
export const InterpretationLog = () => {
|
||||||
|
const [expanded, setExpanded] = useState<boolean>(false);
|
||||||
|
const [log, setLog] = useState<string>('');
|
||||||
|
|
||||||
|
const logEndRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const handleChange = (isExpanded: boolean) => (event: React.SyntheticEvent) => {
|
||||||
|
setExpanded(isExpanded);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { socket } = useSocketStore();
|
||||||
|
|
||||||
|
const scrollLogToBottom = () => {
|
||||||
|
if (logEndRef.current) {
|
||||||
|
logEndRef.current.scrollIntoView({ behavior: "smooth" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLog = useCallback((msg: string, date: boolean = true) => {
|
||||||
|
if (!date){
|
||||||
|
setLog((prevState) => prevState + '\n' + msg);
|
||||||
|
} else {
|
||||||
|
setLog((prevState) => prevState + '\n' + `[${new Date().toLocaleString()}] ` + msg);
|
||||||
|
}
|
||||||
|
scrollLogToBottom();
|
||||||
|
}, [log, scrollLogToBottom])
|
||||||
|
|
||||||
|
const handleSerializableCallback = useCallback((data: string) => {
|
||||||
|
setLog((prevState) =>
|
||||||
|
prevState + '\n' + '---------- Serializable output data received ----------' + '\n'
|
||||||
|
+ JSON.stringify(data, null, 2) + '\n' + '--------------------------------------------------');
|
||||||
|
scrollLogToBottom();
|
||||||
|
}, [log, scrollLogToBottom])
|
||||||
|
|
||||||
|
const handleBinaryCallback = useCallback(({data, mimetype}: any) => {
|
||||||
|
setLog((prevState) =>
|
||||||
|
prevState + '\n' + '---------- Binary output data received ----------' + '\n'
|
||||||
|
+ `mimetype: ${mimetype}` + '\n' + `data: ${JSON.stringify(data)}` + '\n'
|
||||||
|
+ '------------------------------------------------');
|
||||||
|
scrollLogToBottom();
|
||||||
|
}, [log, scrollLogToBottom])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
socket?.on('log', handleLog);
|
||||||
|
socket?.on('serializableCallback', handleSerializableCallback);
|
||||||
|
socket?.on('binaryCallback', handleBinaryCallback);
|
||||||
|
return () => {
|
||||||
|
socket?.off('log', handleLog);
|
||||||
|
socket?.off('serializableCallback', handleSerializableCallback);
|
||||||
|
socket?.off('binaryCallback', handleBinaryCallback);
|
||||||
|
}
|
||||||
|
}, [socket, handleLog])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Accordion
|
||||||
|
expanded={expanded}
|
||||||
|
onChange={handleChange(!expanded)}
|
||||||
|
style={{background: '#3f4853', color: 'white', borderRadius: '0px'}}
|
||||||
|
>
|
||||||
|
<AccordionSummary
|
||||||
|
expandIcon={<ExpandMoreIcon sx={{color: 'white'}}/>}
|
||||||
|
aria-controls="panel1bh-content"
|
||||||
|
id="panel1bh-header"
|
||||||
|
>
|
||||||
|
<Typography sx={{ width: '33%', flexShrink: 0 }}>
|
||||||
|
Interpretation Log
|
||||||
|
</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails sx={{
|
||||||
|
background: '#19171c',
|
||||||
|
overflowY: 'scroll',
|
||||||
|
width: '100%',
|
||||||
|
aspectRatio: '4/1',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<Highlight className="javascript">
|
||||||
|
{log}
|
||||||
|
</Highlight>
|
||||||
|
<div style={{ float:"left", clear: "both" }}
|
||||||
|
ref={logEndRef}/>
|
||||||
|
</div>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
39
src/components/molecules/KeyValueForm.tsx
Normal file
39
src/components/molecules/KeyValueForm.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
|
||||||
|
import { KeyValuePair } from "../atoms/KeyValuePair";
|
||||||
|
import { AddButton } from "../atoms/buttons/AddButton";
|
||||||
|
import { RemoveButton } from "../atoms/buttons/RemoveButton";
|
||||||
|
|
||||||
|
export const KeyValueForm = forwardRef((props, ref) => {
|
||||||
|
const [numberOfPairs, setNumberOfPairs] = React.useState<number>(1);
|
||||||
|
const keyValuePairRefs = useRef<{getKeyValuePair: () => { key: string, value: string }}[]>([]);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
getObject() {
|
||||||
|
let reducedObject = {};
|
||||||
|
for (let i = 0; i < numberOfPairs; i++) {
|
||||||
|
const keyValuePair = keyValuePairRefs.current[i]?.getKeyValuePair();
|
||||||
|
if (keyValuePair) {
|
||||||
|
reducedObject = {
|
||||||
|
...reducedObject,
|
||||||
|
[keyValuePair.key]: keyValuePair.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reducedObject;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
new Array(numberOfPairs).fill(1).map((_, index) => {
|
||||||
|
return <KeyValuePair keyLabel={`key ${index + 1}`} valueLabel={`value ${index + 1}`} key={`keyValuePair-${index}`}
|
||||||
|
//@ts-ignore
|
||||||
|
ref={el => keyValuePairRefs.current[index] = el}/>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
<AddButton handleClick={() => setNumberOfPairs(numberOfPairs + 1)} hoverEffect={false}/>
|
||||||
|
<RemoveButton handleClick={() => setNumberOfPairs(numberOfPairs - 1)}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
105
src/components/molecules/LeftSidePanelContent.tsx
Normal file
105
src/components/molecules/LeftSidePanelContent.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import { Pair } from "./Pair";
|
||||||
|
import { WhereWhatPair, WorkflowFile } from "@wbr-project/wbr-interpret";
|
||||||
|
import { useSocketStore } from "../../context/socket";
|
||||||
|
import { Add } from "@mui/icons-material";
|
||||||
|
import { Socket } from "socket.io-client";
|
||||||
|
import { AddButton } from "../atoms/buttons/AddButton";
|
||||||
|
import { AddPair } from "../../api/workflow";
|
||||||
|
import { GenericModal } from "../atoms/GenericModal";
|
||||||
|
import { PairEditForm } from "./PairEditForm";
|
||||||
|
import { Fab, Tooltip, Typography } from "@mui/material";
|
||||||
|
|
||||||
|
interface LeftSidePanelContentProps {
|
||||||
|
workflow: WorkflowFile;
|
||||||
|
updateWorkflow: (workflow: WorkflowFile) => void;
|
||||||
|
recordingName: string;
|
||||||
|
handleSelectPairForEdit: (pair: WhereWhatPair, index: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LeftSidePanelContent = ({ workflow, updateWorkflow, recordingName, handleSelectPairForEdit}: LeftSidePanelContentProps) => {
|
||||||
|
const [activeId, setActiveId] = React.useState<number>(0);
|
||||||
|
const [breakpoints, setBreakpoints] = React.useState<boolean[]>([]);
|
||||||
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
|
|
||||||
|
const { socket } = useSocketStore();
|
||||||
|
|
||||||
|
const activePairIdHandler = useCallback((data: string, socket: Socket) => {
|
||||||
|
setActiveId(parseInt(data) + 1);
|
||||||
|
// -1 is specially emitted when the interpretation finishes
|
||||||
|
if (parseInt(data) === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
socket.emit('activeIndex', data);
|
||||||
|
}, [activeId])
|
||||||
|
|
||||||
|
const addPair = (pair: WhereWhatPair, index: number) => {
|
||||||
|
AddPair((index - 1), pair).then((updatedWorkflow) => {
|
||||||
|
updateWorkflow(updatedWorkflow);
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
setShowEditModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
socket?.on("activePairId", (data) => activePairIdHandler(data, socket));
|
||||||
|
return () => {
|
||||||
|
socket?.off("activePairId", (data) => activePairIdHandler(data, socket));
|
||||||
|
}
|
||||||
|
}, [socket, setActiveId]);
|
||||||
|
|
||||||
|
|
||||||
|
const handleBreakpointClick = (id: number) => {
|
||||||
|
setBreakpoints(oldBreakpoints => {
|
||||||
|
const newArray = [...oldBreakpoints, ...Array(workflow.workflow.length - oldBreakpoints.length).fill(false)];
|
||||||
|
newArray[id] = !newArray[id];
|
||||||
|
socket?.emit("breakpoints", newArray);
|
||||||
|
return newArray;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddPair = () => {
|
||||||
|
setShowEditModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Tooltip title='Add pair' placement='left' arrow>
|
||||||
|
<div style={{ float: 'right'}}>
|
||||||
|
<AddButton
|
||||||
|
handleClick={handleAddPair}
|
||||||
|
title=''
|
||||||
|
hoverEffect={false}
|
||||||
|
style={{color: 'white', background: '#1976d2'}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
<GenericModal
|
||||||
|
isOpen={showEditModal}
|
||||||
|
onClose={() => setShowEditModal(false)}
|
||||||
|
>
|
||||||
|
<PairEditForm
|
||||||
|
onSubmitOfPair={addPair}
|
||||||
|
numberOfPairs={workflow.workflow.length}
|
||||||
|
/>
|
||||||
|
</GenericModal>
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
workflow.workflow.map((pair, i, workflow, ) =>
|
||||||
|
<Pair
|
||||||
|
handleBreakpoint={() => handleBreakpointClick(i)}
|
||||||
|
isActive={ activeId === i + 1}
|
||||||
|
key={workflow.length - i}
|
||||||
|
index={workflow.length - i}
|
||||||
|
pair={pair}
|
||||||
|
updateWorkflow={updateWorkflow}
|
||||||
|
numberOfPairs={workflow.length}
|
||||||
|
handleSelectPairForEdit={handleSelectPairForEdit}
|
||||||
|
/>)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
86
src/components/molecules/LeftSidePanelSettings.tsx
Normal file
86
src/components/molecules/LeftSidePanelSettings.tsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Button, MenuItem, TextField, Typography } from "@mui/material";
|
||||||
|
import { Dropdown } from "../atoms/DropdownMui";
|
||||||
|
import { RunSettings } from "./RunSettings";
|
||||||
|
import { useSocketStore } from "../../context/socket";
|
||||||
|
|
||||||
|
interface LeftSidePanelSettingsProps {
|
||||||
|
params: any[]
|
||||||
|
settings: RunSettings,
|
||||||
|
setSettings: (setting: RunSettings) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LeftSidePanelSettings = ({params, settings, setSettings}: LeftSidePanelSettingsProps) => {
|
||||||
|
const { socket } = useSocketStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection:'column', alignItems: 'flex-start'}}>
|
||||||
|
{ params.length !== 0 && (
|
||||||
|
<React.Fragment>
|
||||||
|
<Typography>Parameters:</Typography>
|
||||||
|
{ params?.map((item: string, index: number) => {
|
||||||
|
return <TextField
|
||||||
|
sx={{margin: '15px 0px'}}
|
||||||
|
value={settings.params ? settings.params[item] : ''}
|
||||||
|
key={`param-${index}`}
|
||||||
|
type="string"
|
||||||
|
label={item}
|
||||||
|
required
|
||||||
|
onChange={(e) => setSettings(
|
||||||
|
{
|
||||||
|
...settings,
|
||||||
|
params: settings.params
|
||||||
|
? {
|
||||||
|
...settings.params,
|
||||||
|
[item]: e.target.value,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
[item]: e.target.value,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
}) }
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
<Typography sx={{margin: '15px 0px'}}>Interpreter:</Typography>
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
label="maxConcurrency"
|
||||||
|
required
|
||||||
|
onChange={(e) => setSettings(
|
||||||
|
{
|
||||||
|
...settings,
|
||||||
|
maxConcurrency: parseInt(e.target.value),
|
||||||
|
})}
|
||||||
|
defaultValue={settings.maxConcurrency}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
sx={{margin: '15px 0px'}}
|
||||||
|
type="number"
|
||||||
|
label="maxRepeats"
|
||||||
|
required
|
||||||
|
onChange={(e) => setSettings(
|
||||||
|
{
|
||||||
|
...settings,
|
||||||
|
maxRepeats: parseInt(e.target.value),
|
||||||
|
})}
|
||||||
|
defaultValue={settings.maxRepeats}
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
id="debug"
|
||||||
|
label="debug"
|
||||||
|
value={settings.debug?.toString()}
|
||||||
|
handleSelect={(e) => setSettings(
|
||||||
|
{
|
||||||
|
...settings,
|
||||||
|
debug: e.target.value === "true",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<MenuItem value="true">true</MenuItem>
|
||||||
|
<MenuItem value="false">false</MenuItem>
|
||||||
|
</Dropdown>
|
||||||
|
<Button sx={{margin: '15px 0px'}} variant='contained'
|
||||||
|
onClick={() => socket?.emit('settings', settings)}>change</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
110
src/components/molecules/NavBar.tsx
Normal file
110
src/components/molecules/NavBar.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { stopRecording } from "../../api/recording";
|
||||||
|
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 MeetingRoomIcon from '@mui/icons-material/MeetingRoom';
|
||||||
|
|
||||||
|
interface NavBarProps {
|
||||||
|
newRecording: () => void;
|
||||||
|
recordingName: string;
|
||||||
|
isRecording: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NavBar = ({newRecording, recordingName, isRecording}:NavBarProps) => {
|
||||||
|
|
||||||
|
const { notify, browserId, setBrowserId, recordingLength } = useGlobalInfoStore();
|
||||||
|
|
||||||
|
// If recording is in progress, the resources and change page view by setting browserId to null
|
||||||
|
// else it won't affect the page
|
||||||
|
const goToMainMenu = async() => {
|
||||||
|
if (browserId) {
|
||||||
|
await stopRecording(browserId);
|
||||||
|
notify('warning', 'Current Recording was terminated');
|
||||||
|
setBrowserId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNewRecording = async () => {
|
||||||
|
if (browserId) {
|
||||||
|
setBrowserId(null);
|
||||||
|
await stopRecording(browserId);
|
||||||
|
}
|
||||||
|
newRecording();
|
||||||
|
notify('info', 'New Recording started');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavBarWrapper>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
}}>
|
||||||
|
<RecordingIcon/>
|
||||||
|
<div style={{padding: '11px'}}><ProjectName>Browser Recorder</ProjectName></div>
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
}}>
|
||||||
|
<IconButton
|
||||||
|
aria-label="new"
|
||||||
|
size={"small"}
|
||||||
|
onClick={handleNewRecording}
|
||||||
|
sx={{
|
||||||
|
width: isRecording ? '100px' : '130px',
|
||||||
|
borderRadius: '5px',
|
||||||
|
padding: '8px',
|
||||||
|
background: 'white',
|
||||||
|
color: 'rgba(255,0,0,0.7)',
|
||||||
|
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>
|
||||||
|
{
|
||||||
|
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 }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</NavBarWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NavBarWrapper = styled.div`
|
||||||
|
grid-area: navbar;
|
||||||
|
background-color: #3f4853;
|
||||||
|
padding:5px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ProjectName = styled.b`
|
||||||
|
color: white;
|
||||||
|
font-size: 1.3em;
|
||||||
|
`;
|
||||||
181
src/components/molecules/Pair.tsx
Normal file
181
src/components/molecules/Pair.tsx
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import React, { FC, useState } from 'react';
|
||||||
|
import { Stack, Button, IconButton, Tooltip, Chip, Badge } from "@mui/material";
|
||||||
|
import { AddPair, deletePair, UpdatePair } from "../../api/workflow";
|
||||||
|
import { WorkflowFile } from "@wbr-project/wbr-interpret";
|
||||||
|
import { ClearButton } from "../atoms/buttons/ClearButton";
|
||||||
|
import { GenericModal } from "../atoms/GenericModal";
|
||||||
|
import { PairEditForm } from "./PairEditForm";
|
||||||
|
import { PairDisplayDiv } from "../atoms/PairDisplayDiv";
|
||||||
|
import { EditButton } from "../atoms/buttons/EditButton";
|
||||||
|
import { BreakpointButton } from "../atoms/buttons/BreakpointButton";
|
||||||
|
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { LoadingButton } from "@mui/lab";
|
||||||
|
|
||||||
|
type WhereWhatPair = WorkflowFile["workflow"][number];
|
||||||
|
|
||||||
|
|
||||||
|
interface PairProps {
|
||||||
|
handleBreakpoint: () => void;
|
||||||
|
isActive: boolean;
|
||||||
|
index: number;
|
||||||
|
pair: WhereWhatPair;
|
||||||
|
updateWorkflow: (workflow: WorkflowFile) => void;
|
||||||
|
numberOfPairs: number;
|
||||||
|
handleSelectPairForEdit: (pair: WhereWhatPair, index: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const Pair: FC<PairProps> = (
|
||||||
|
{
|
||||||
|
handleBreakpoint, isActive, index,
|
||||||
|
pair, updateWorkflow, numberOfPairs,
|
||||||
|
handleSelectPairForEdit
|
||||||
|
}) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [edit, setEdit] = useState(false);
|
||||||
|
const [breakpoint, setBreakpoint] = useState(false);
|
||||||
|
|
||||||
|
const enableEdit = () => setEdit(true);
|
||||||
|
const disableEdit = () => setEdit(false);
|
||||||
|
|
||||||
|
const handleOpen = () => setOpen(true);
|
||||||
|
const handleClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
disableEdit();
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
deletePair(index - 1).then((updatedWorkflow) => {
|
||||||
|
updateWorkflow(updatedWorkflow);
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (pair: WhereWhatPair, newIndex: number) => {
|
||||||
|
if (newIndex !== index){
|
||||||
|
AddPair((newIndex - 1), pair).then((updatedWorkflow) => {
|
||||||
|
updateWorkflow(updatedWorkflow);
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
UpdatePair((index - 1), pair).then((updatedWorkflow) => {
|
||||||
|
updateWorkflow(updatedWorkflow);
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBreakpointClick = () => {
|
||||||
|
setBreakpoint(!breakpoint);
|
||||||
|
handleBreakpoint();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PairWrapper isActive={isActive}>
|
||||||
|
<Stack direction="row">
|
||||||
|
<div style={{display: 'flex', maxWidth:'20px', alignItems:'center', justifyContent: 'center', }}>
|
||||||
|
{isActive ? <LoadingButton loading variant="text"/>
|
||||||
|
: breakpoint ? <BreakpointButton changeColor={true} handleClick={handleBreakpointClick}/>
|
||||||
|
: <BreakpointButton handleClick={handleBreakpointClick}/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<Badge badgeContent={pair.what.length} color="primary">
|
||||||
|
<Button sx={{
|
||||||
|
position: 'relative',
|
||||||
|
color: 'black',
|
||||||
|
padding: '5px 20px',
|
||||||
|
fontSize: '1rem',
|
||||||
|
textTransform: 'none',
|
||||||
|
}} variant='text' key={`pair-${index}`}
|
||||||
|
onClick={() => handleSelectPairForEdit(pair, index)}>
|
||||||
|
index: {index}
|
||||||
|
</Button>
|
||||||
|
</Badge>
|
||||||
|
<Stack direction="row" spacing={0}
|
||||||
|
sx={{
|
||||||
|
color: 'inherit',
|
||||||
|
"&:hover": {
|
||||||
|
color: 'inherit',
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<Tooltip title="View" placement='right' arrow>
|
||||||
|
<div>
|
||||||
|
<ViewButton
|
||||||
|
handleClick={handleOpen}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip title="Raw edit" placement='right' arrow>
|
||||||
|
<div>
|
||||||
|
<EditButton
|
||||||
|
handleClick={() => {
|
||||||
|
enableEdit();
|
||||||
|
handleOpen();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Delete" placement='right' arrow>
|
||||||
|
<div>
|
||||||
|
<ClearButton handleClick={handleDelete}/>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<GenericModal isOpen={open} onClose={handleClose}>
|
||||||
|
{ edit
|
||||||
|
?
|
||||||
|
<PairEditForm
|
||||||
|
onSubmitOfPair={handleEdit}
|
||||||
|
numberOfPairs={numberOfPairs}
|
||||||
|
index={index.toString()}
|
||||||
|
where={pair.where ? JSON.stringify(pair.where) : undefined}
|
||||||
|
what={pair.what ? JSON.stringify(pair.what) : undefined}
|
||||||
|
id={pair.id}
|
||||||
|
/>
|
||||||
|
:
|
||||||
|
<div>
|
||||||
|
<PairDisplayDiv
|
||||||
|
index={index.toString()}
|
||||||
|
pair={pair}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</GenericModal>
|
||||||
|
</PairWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ViewButtonProps {
|
||||||
|
handleClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ViewButton = ({handleClick}: ViewButtonProps) => {
|
||||||
|
return (
|
||||||
|
<IconButton aria-label="add" size={"small"} onClick={handleClick}
|
||||||
|
sx={{color: 'inherit', '&:hover': { color: '#1976d2', backgroundColor: 'transparent' }}}>
|
||||||
|
<VisibilityIcon/>
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const PairWrapper = styled.div<{ isActive: boolean }>`
|
||||||
|
background-color: ${({ isActive }) => isActive ? 'rgba(255, 0, 0, 0.1)' : 'transparent' };
|
||||||
|
border: ${({ isActive }) => isActive ? 'solid 2px red' : 'none' };
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 98%;
|
||||||
|
color: gray;
|
||||||
|
&:hover {
|
||||||
|
color: dimgray;
|
||||||
|
background: ${({ isActive }) => isActive ? 'rgba(255, 0, 0, 0.1)' : 'transparent' };
|
||||||
|
}
|
||||||
|
`;
|
||||||
310
src/components/molecules/PairDetail.tsx
Normal file
310
src/components/molecules/PairDetail.tsx
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
import React, { useLayoutEffect, useRef, useState } from 'react';
|
||||||
|
import { WhereWhatPair } from "@wbr-project/wbr-interpret";
|
||||||
|
import { Box, Button, IconButton, MenuItem, Stack, TextField, Tooltip, Typography } from "@mui/material";
|
||||||
|
import { Close, KeyboardArrowDown, KeyboardArrowUp } from "@mui/icons-material";
|
||||||
|
import TreeView from '@mui/lab/TreeView';
|
||||||
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
|
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||||
|
import TreeItem from '@mui/lab/TreeItem';
|
||||||
|
import { AddButton } from "../atoms/buttons/AddButton";
|
||||||
|
import { WarningText } from "../atoms/texts";
|
||||||
|
import NotificationImportantIcon from '@mui/icons-material/NotificationImportant';
|
||||||
|
import { RemoveButton } from "../atoms/buttons/RemoveButton";
|
||||||
|
import { AddWhereCondModal } from "./AddWhereCondModal";
|
||||||
|
import { UpdatePair } from "../../api/workflow";
|
||||||
|
import { useSocketStore } from "../../context/socket";
|
||||||
|
import { AddWhatCondModal } from "./AddWhatCondModal";
|
||||||
|
|
||||||
|
interface PairDetailProps {
|
||||||
|
pair: WhereWhatPair | null;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PairDetail = ({ pair, index }: PairDetailProps) => {
|
||||||
|
const [pairIsSelected, setPairIsSelected] = useState(false);
|
||||||
|
const [collapseWhere, setCollapseWhere] = useState(true);
|
||||||
|
const [collapseWhat, setCollapseWhat] = useState(true);
|
||||||
|
const [rerender, setRerender] = useState(false);
|
||||||
|
const [expanded, setExpanded] = React.useState<string[]>(
|
||||||
|
pair ? Object.keys(pair.where).map((key, index) => `${key}-${index}`) : []
|
||||||
|
);
|
||||||
|
const [addWhereCondOpen, setAddWhereCondOpen] = useState(false);
|
||||||
|
const [addWhatCondOpen, setAddWhatCondOpen] = useState(false);
|
||||||
|
|
||||||
|
const { socket } = useSocketStore();
|
||||||
|
|
||||||
|
const handleCollapseWhere = () => {
|
||||||
|
setCollapseWhere(!collapseWhere);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCollapseWhat = () => {
|
||||||
|
setCollapseWhat(!collapseWhat);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToggle = (event: React.SyntheticEvent, nodeIds: string[]) => {
|
||||||
|
setExpanded(nodeIds);
|
||||||
|
};
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (pair) {
|
||||||
|
setPairIsSelected(true);
|
||||||
|
}
|
||||||
|
}, [pair])
|
||||||
|
|
||||||
|
const handleChangeValue = (value: any, where: boolean, keys: (string|number)[]) => {
|
||||||
|
// a moving reference to internal objects within pair.where or pair.what
|
||||||
|
let schema: any = where ? pair?.where : pair?.what;
|
||||||
|
const length = keys.length;
|
||||||
|
for(let i = 0; i < length-1; i++) {
|
||||||
|
const elem = keys[i];
|
||||||
|
if( !schema[elem] ) schema[elem] = {}
|
||||||
|
schema = schema[elem];
|
||||||
|
}
|
||||||
|
|
||||||
|
schema[keys[length-1]] = value;
|
||||||
|
if (pair && socket) {
|
||||||
|
socket.emit('updatePair', {index: index-1, pair: pair});
|
||||||
|
}
|
||||||
|
setRerender(!rerender);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const DisplayValueContent = (value: any, keys: (string|number)[], where: boolean = true) => {
|
||||||
|
switch (typeof(value)) {
|
||||||
|
case 'string':
|
||||||
|
return <TextField
|
||||||
|
size='small'
|
||||||
|
type="string"
|
||||||
|
onChange={(e) => {
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(e.target.value);
|
||||||
|
handleChangeValue(obj, where, keys);
|
||||||
|
} catch (error) {
|
||||||
|
const num = Number(e.target.value);
|
||||||
|
if (!isNaN(num)) {
|
||||||
|
handleChangeValue(num, where, keys);
|
||||||
|
}
|
||||||
|
handleChangeValue(e.target.value, where, keys)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
defaultValue={value}
|
||||||
|
key={`text-field-${keys.join('-')}-${where}`}
|
||||||
|
/>
|
||||||
|
case 'number':
|
||||||
|
return <TextField
|
||||||
|
size='small'
|
||||||
|
type="number"
|
||||||
|
onChange={(e) => handleChangeValue(Number(e.target.value), where, keys)}
|
||||||
|
defaultValue={value}
|
||||||
|
key={`text-field-${keys.join('-')}-${where}`}
|
||||||
|
/>
|
||||||
|
case 'object':
|
||||||
|
if (value) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{
|
||||||
|
value.map((element, index) => {
|
||||||
|
return DisplayValueContent(element, [...keys, index], where);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
<AddButton handleClick={()=> {
|
||||||
|
let prevValue:any = where ? pair?.where : pair?.what;
|
||||||
|
for (const key of keys) {
|
||||||
|
prevValue = prevValue[key];
|
||||||
|
}
|
||||||
|
handleChangeValue([...prevValue, ''], where, keys);
|
||||||
|
setRerender(!rerender);
|
||||||
|
}} hoverEffect={false}/>
|
||||||
|
<RemoveButton handleClick={()=> {
|
||||||
|
let prevValue:any = where ? pair?.where : pair?.what;
|
||||||
|
for (const key of keys) {
|
||||||
|
prevValue = prevValue[key];
|
||||||
|
}
|
||||||
|
prevValue.splice(-1);
|
||||||
|
handleChangeValue(prevValue, where, keys);
|
||||||
|
setRerender(!rerender);
|
||||||
|
}}/>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<TreeView
|
||||||
|
defaultCollapseIcon={<ExpandMoreIcon />}
|
||||||
|
defaultExpandIcon={<ChevronRightIcon />}
|
||||||
|
sx={{ flexGrow: 1, overflowY: 'auto' }}
|
||||||
|
key={`tree-view-nested-${keys.join('-')}-${where}`}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
Object.keys(value).map((key2, index) =>
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<TreeItem nodeId={`${key2}-${index}`} label={`${key2}:`} key={`${key2}-${index}`}>
|
||||||
|
{ DisplayValueContent(value[key2], [...keys, key2], where) }
|
||||||
|
</TreeItem>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</TreeView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{ pair &&
|
||||||
|
<React.Fragment>
|
||||||
|
<AddWhatCondModal isOpen={addWhatCondOpen} onClose={() => setAddWhatCondOpen(false)}
|
||||||
|
pair={pair} index={index}/>
|
||||||
|
<AddWhereCondModal isOpen={addWhereCondOpen} onClose={() => setAddWhereCondOpen(false)}
|
||||||
|
pair={pair} index={index}/>
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
pairIsSelected
|
||||||
|
? (
|
||||||
|
<div style={{padding: '10px', overflow: 'hidden'}}>
|
||||||
|
<Typography>Pair number: {index}</Typography>
|
||||||
|
<TextField
|
||||||
|
size='small'
|
||||||
|
label='id'
|
||||||
|
onChange={(e) => {
|
||||||
|
if (pair && socket) {
|
||||||
|
socket.emit('updatePair', {index: index-1, pair: pair});
|
||||||
|
pair.id = e.target.value;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
value={pair ? pair.id ? pair.id : '' : ''}
|
||||||
|
/>
|
||||||
|
<Stack spacing={0} direction='row' sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
background: 'lightGray',
|
||||||
|
}}>
|
||||||
|
<CollapseButton
|
||||||
|
handleClick={handleCollapseWhere}
|
||||||
|
isCollapsed={collapseWhere}
|
||||||
|
/>
|
||||||
|
<Typography>Where</Typography>
|
||||||
|
<Tooltip title='Add where condition' placement='right'>
|
||||||
|
<div>
|
||||||
|
<AddButton handleClick={()=> {
|
||||||
|
setAddWhereCondOpen(true);
|
||||||
|
}} style={{color:'rgba(0, 0, 0, 0.54)', background:'transparent'}}/>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</Stack>
|
||||||
|
{(collapseWhere && pair && pair.where)
|
||||||
|
?
|
||||||
|
<React.Fragment>
|
||||||
|
{ Object.keys(pair.where).map((key, index) => {
|
||||||
|
return (
|
||||||
|
<TreeView
|
||||||
|
expanded={expanded}
|
||||||
|
defaultCollapseIcon={<ExpandMoreIcon />}
|
||||||
|
defaultExpandIcon={<ChevronRightIcon />}
|
||||||
|
sx={{ flexGrow: 1, overflowY: 'auto' }}
|
||||||
|
onNodeToggle={handleToggle}
|
||||||
|
key={`tree-view-${key}-${index}`}
|
||||||
|
>
|
||||||
|
<TreeItem nodeId={`${key}-${index}`} label={`${key}:`} key={`${key}-${index}`}>
|
||||||
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
DisplayValueContent(pair.where[key], [key])
|
||||||
|
}
|
||||||
|
</TreeItem>
|
||||||
|
</TreeView>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</React.Fragment>
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
<Stack spacing={0} direction='row' sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
background: 'lightGray',
|
||||||
|
}}>
|
||||||
|
<CollapseButton
|
||||||
|
handleClick={handleCollapseWhat}
|
||||||
|
isCollapsed={collapseWhat}
|
||||||
|
/>
|
||||||
|
<Typography>What</Typography>
|
||||||
|
|
||||||
|
<Tooltip title='Add what condition' placement='right'>
|
||||||
|
<div>
|
||||||
|
<AddButton handleClick={()=> {
|
||||||
|
setAddWhatCondOpen(true);
|
||||||
|
}} style={{color:'rgba(0, 0, 0, 0.54)', background:'transparent'}}/>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</Stack>
|
||||||
|
{(collapseWhat && pair && pair.what)
|
||||||
|
?(
|
||||||
|
<React.Fragment>
|
||||||
|
{ Object.keys(pair.what).map((key, index) => {
|
||||||
|
return (
|
||||||
|
<TreeView
|
||||||
|
defaultCollapseIcon={<ExpandMoreIcon />}
|
||||||
|
defaultExpandIcon={<ChevronRightIcon />}
|
||||||
|
sx={{ flexGrow: 1, overflowY: 'auto' }}
|
||||||
|
key={`tree-view-2-${key}-${index}`}
|
||||||
|
>
|
||||||
|
<TreeItem nodeId={`${key}-${index}`} label={`${pair.what[index].action}`}>
|
||||||
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
DisplayValueContent(pair.what[key], [key], false)
|
||||||
|
}
|
||||||
|
<Tooltip title='remove action' placement='left'>
|
||||||
|
<div style={{float:'right'}}>
|
||||||
|
<CloseButton handleClick={() => {
|
||||||
|
//@ts-ignore
|
||||||
|
pair.what.splice(key, 1);
|
||||||
|
setRerender(!rerender);
|
||||||
|
}}/>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</TreeItem>
|
||||||
|
</TreeView>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: <WarningText>
|
||||||
|
<NotificationImportantIcon color="warning"/>
|
||||||
|
No pair from the left side panel was selected.
|
||||||
|
</WarningText>
|
||||||
|
}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CollapseButtonProps {
|
||||||
|
handleClick: () => void;
|
||||||
|
isCollapsed?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CollapseButton = ({handleClick, isCollapsed } : CollapseButtonProps) => {
|
||||||
|
return (
|
||||||
|
<IconButton aria-label="add" size={"small"} onClick={handleClick}>
|
||||||
|
{ isCollapsed ? <KeyboardArrowDown/> : <KeyboardArrowUp/>}
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const CloseButton = ({handleClick } : CollapseButtonProps) => {
|
||||||
|
return (
|
||||||
|
<IconButton aria-label="add" size={"small"} onClick={handleClick}
|
||||||
|
sx={{'&:hover': { color: '#1976d2', backgroundColor: 'white' }}}>
|
||||||
|
<Close/>
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
161
src/components/molecules/PairEditForm.tsx
Normal file
161
src/components/molecules/PairEditForm.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import { Button, TextField, Typography } from "@mui/material";
|
||||||
|
import React, { FC } from "react";
|
||||||
|
import { Preprocessor, WhereWhatPair } from "@wbr-project/wbr-interpret";
|
||||||
|
|
||||||
|
interface PairProps {
|
||||||
|
index: string;
|
||||||
|
id?: string;
|
||||||
|
where: string | null;
|
||||||
|
what: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PairEditFormProps {
|
||||||
|
onSubmitOfPair: (value: WhereWhatPair, index: number) => void;
|
||||||
|
numberOfPairs: number;
|
||||||
|
index?: string;
|
||||||
|
where?: string;
|
||||||
|
what?: string;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PairEditForm: FC<PairEditFormProps> = (
|
||||||
|
{
|
||||||
|
onSubmitOfPair,
|
||||||
|
numberOfPairs,
|
||||||
|
index,
|
||||||
|
where,
|
||||||
|
what,
|
||||||
|
id,
|
||||||
|
}) => {
|
||||||
|
const [pairProps, setPairProps] = React.useState<PairProps>({
|
||||||
|
where: where || null,
|
||||||
|
what: what || null,
|
||||||
|
index: index || "1",
|
||||||
|
id: id || '',
|
||||||
|
});
|
||||||
|
const [errors, setErrors] = React.useState<PairProps>({
|
||||||
|
where: null,
|
||||||
|
what: null,
|
||||||
|
index: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { id, value } = event.target;
|
||||||
|
if (id === 'index') {
|
||||||
|
if (parseInt(value, 10) < 1) {
|
||||||
|
setErrors({ ...errors, index: 'Index must be greater than 0' });
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
setErrors({ ...errors, index: '' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setPairProps({ ...pairProps, [id]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateAndSubmit = (event: React.SyntheticEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
let whereFromPair, whatFromPair;
|
||||||
|
// validate where
|
||||||
|
whereFromPair = {
|
||||||
|
where: pairProps.where && pairProps.where !== '{"url":"","selectors":[""] }'
|
||||||
|
? JSON.parse(pairProps.where)
|
||||||
|
: {},
|
||||||
|
what: [],
|
||||||
|
};
|
||||||
|
const validationError = Preprocessor.validateWorkflow({workflow: [whereFromPair]});
|
||||||
|
setErrors({ ...errors, where: null });
|
||||||
|
if (validationError) {
|
||||||
|
setErrors({ ...errors, where: validationError.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// validate what
|
||||||
|
whatFromPair = {
|
||||||
|
where: {},
|
||||||
|
what: pairProps.what && pairProps.what !== '[{"action":"","args":[""] }]'
|
||||||
|
? JSON.parse(pairProps.what): [],
|
||||||
|
};
|
||||||
|
const validationErrorWhat = Preprocessor.validateWorkflow({workflow: [whatFromPair]});
|
||||||
|
setErrors({ ...errors, "what": null });
|
||||||
|
if (validationErrorWhat) {
|
||||||
|
setErrors({ ...errors, what: validationErrorWhat.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//validate index
|
||||||
|
const index = parseInt(pairProps?.index, 10);
|
||||||
|
if (index > (numberOfPairs + 1)) {
|
||||||
|
if (numberOfPairs === 0) {
|
||||||
|
setErrors(prevState => ({
|
||||||
|
...prevState,
|
||||||
|
index: 'Index of the first pair must be 1'
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
setErrors(prevState => ({
|
||||||
|
...prevState,
|
||||||
|
index: `Index must be in the range 1-${numberOfPairs + 1}`
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setErrors({ ...errors, index: '' });
|
||||||
|
}
|
||||||
|
// submit the pair
|
||||||
|
onSubmitOfPair(pairProps.id
|
||||||
|
? {
|
||||||
|
id: pairProps.id,
|
||||||
|
where: whereFromPair?.where || {},
|
||||||
|
what: whatFromPair?.what || [],
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
where: whereFromPair?.where || {},
|
||||||
|
what: whatFromPair?.what || [],
|
||||||
|
}
|
||||||
|
, index);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={validateAndSubmit}
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
padding: "0px 30px 0px 30px",
|
||||||
|
marginTop: "36px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{marginBottom:'30px'}} variant='h5'>Raw pair edit form:</Typography>
|
||||||
|
<TextField sx={{
|
||||||
|
display:"block",
|
||||||
|
marginBottom: "20px"
|
||||||
|
}} id="index" label="Index" type="number"
|
||||||
|
InputProps={{ inputProps: { min: 1 } }}
|
||||||
|
InputLabelProps={{
|
||||||
|
shrink: true,
|
||||||
|
}} defaultValue={pairProps.index}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
error={!!errors.index} helperText={errors.index}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<TextField sx={{
|
||||||
|
marginBottom: "20px"
|
||||||
|
}} id="id" label="Id" type="string"
|
||||||
|
defaultValue={pairProps.id}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<TextField multiline sx={{marginBottom: "20px"}}
|
||||||
|
id="where" label="Where" variant="outlined" onChange={handleInputChange}
|
||||||
|
defaultValue={ where || '{"url":"","selectors":[""]}' }
|
||||||
|
error={!!errors.where} helperText={errors.where}/>
|
||||||
|
<TextField multiline sx={{marginBottom: "20px"}}
|
||||||
|
id="what" label="What" variant="outlined" onChange={handleInputChange}
|
||||||
|
defaultValue={ what || '[{"action":"","args":[""]}]' }
|
||||||
|
error={!!errors.what} helperText={errors.what}/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
sx={{ padding: "8px 20px", }}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
220
src/components/molecules/RecordingsTable.tsx
Normal file
220
src/components/molecules/RecordingsTable.tsx
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
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 TablePagination from '@mui/material/TablePagination';
|
||||||
|
import TableRow from '@mui/material/TableRow';
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { WorkflowFile } from "@wbr-project/wbr-interpret";
|
||||||
|
import { IconButton } from "@mui/material";
|
||||||
|
import { Assignment, DeleteForever, Edit, PlayCircle } from "@mui/icons-material";
|
||||||
|
import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||||
|
import { deleteRecordingFromStorage, getStoredRecordings } from "../../api/storage";
|
||||||
|
|
||||||
|
interface Column {
|
||||||
|
id: 'interpret' | 'name' | 'create_date' | 'edit' | 'pairs' | 'update_date'| 'delete';
|
||||||
|
label: string;
|
||||||
|
minWidth?: number;
|
||||||
|
align?: 'right';
|
||||||
|
format?: (value: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: readonly Column[] = [
|
||||||
|
{ id: 'interpret', label: 'Run', minWidth: 80 },
|
||||||
|
{ id: 'name', label: 'Name', minWidth: 80 },
|
||||||
|
{
|
||||||
|
id: 'create_date',
|
||||||
|
label: 'Created at',
|
||||||
|
minWidth: 80,
|
||||||
|
//format: (value: string) => value.toLocaleString('en-US'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'edit',
|
||||||
|
label: 'Edit',
|
||||||
|
minWidth: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pairs',
|
||||||
|
label: 'Pairs',
|
||||||
|
minWidth: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'update_date',
|
||||||
|
label: 'Updated at',
|
||||||
|
minWidth: 80,
|
||||||
|
//format: (value: string) => value.toLocaleString('en-US'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'delete',
|
||||||
|
label: 'Delete',
|
||||||
|
minWidth: 80,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface Data {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
create_date: string;
|
||||||
|
pairs: number;
|
||||||
|
update_date: string;
|
||||||
|
content: WorkflowFile;
|
||||||
|
params: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RecordingsTableProps {
|
||||||
|
handleEditRecording: (fileName:string) => void;
|
||||||
|
handleRunRecording: (fileName:string, params: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RecordingsTable = ({ handleEditRecording, handleRunRecording }: RecordingsTableProps) => {
|
||||||
|
const [page, setPage] = React.useState(0);
|
||||||
|
const [rowsPerPage, setRowsPerPage] = React.useState(10);
|
||||||
|
const [rows, setRows] = React.useState<Data[]>([]);
|
||||||
|
|
||||||
|
const { notify, setRecordings } = useGlobalInfoStore();
|
||||||
|
|
||||||
|
const handleChangePage = (event: unknown, newPage: number) => {
|
||||||
|
setPage(newPage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setRowsPerPage(+event.target.value);
|
||||||
|
setPage(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchRecordings = async () => {
|
||||||
|
const recordings = await getStoredRecordings();
|
||||||
|
if (recordings) {
|
||||||
|
const parsedRows: Data[] = [];
|
||||||
|
recordings.map((recording, index) => {
|
||||||
|
const parsedRecording = JSON.parse(recording);
|
||||||
|
if (parsedRecording.recording_meta) {
|
||||||
|
parsedRows.push({
|
||||||
|
id: index,
|
||||||
|
...parsedRecording.recording_meta,
|
||||||
|
content: parsedRecording.recording
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setRecordings(parsedRows.map((recording) => recording.name));
|
||||||
|
setRows(parsedRows);
|
||||||
|
} else {
|
||||||
|
console.log('No recordings found.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect( () => {
|
||||||
|
if (rows.length === 0) {
|
||||||
|
fetchRecordings();
|
||||||
|
}
|
||||||
|
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<TableContainer component={Paper} sx={{ width: '100%', overflow: 'hidden' }}>
|
||||||
|
<Table stickyHeader aria-label="sticky table">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
{columns.map((column) => (
|
||||||
|
<TableCell
|
||||||
|
key={column.id}
|
||||||
|
align={column.align}
|
||||||
|
style={{ minWidth: column.minWidth }}
|
||||||
|
>
|
||||||
|
{column.label}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{rows.length !== 0 ? rows
|
||||||
|
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
||||||
|
.map((row) => {
|
||||||
|
return (
|
||||||
|
<TableRow hover role="checkbox" tabIndex={-1} key={row.id}>
|
||||||
|
{columns.map((column) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const value : any = row[column.id];
|
||||||
|
if (value !== undefined) {
|
||||||
|
return (
|
||||||
|
<TableCell key={column.id} align={column.align}>
|
||||||
|
{value}
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
switch (column.id) {
|
||||||
|
case 'interpret':
|
||||||
|
return (
|
||||||
|
<TableCell key={column.id} align={column.align}>
|
||||||
|
<InterpretButton handleInterpret={() => handleRunRecording(row.name, row.params || [])}/>
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
case 'edit':
|
||||||
|
return (
|
||||||
|
<TableCell key={column.id} align={column.align}>
|
||||||
|
<IconButton aria-label="add" size= "small" onClick={() => {
|
||||||
|
handleEditRecording(row.name);
|
||||||
|
}} sx={{'&:hover': { color: '#1976d2', backgroundColor: 'transparent' }}}>
|
||||||
|
<Edit/>
|
||||||
|
</IconButton>
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
case 'delete':
|
||||||
|
return (
|
||||||
|
<TableCell key={column.id} align={column.align}>
|
||||||
|
<IconButton aria-label="add" size= "small" onClick={() => {
|
||||||
|
deleteRecordingFromStorage(row.name).then((result: boolean) => {
|
||||||
|
if (result) {
|
||||||
|
setRows([]);
|
||||||
|
notify('success', 'Recording deleted successfully');
|
||||||
|
fetchRecordings();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}} sx={{'&:hover': { color: '#1976d2', backgroundColor: 'transparent' }}}>
|
||||||
|
<DeleteForever/>
|
||||||
|
</IconButton>
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: null }
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
<TablePagination
|
||||||
|
rowsPerPageOptions={[10, 25, 50]}
|
||||||
|
component="div"
|
||||||
|
count={rows ? rows.length : 0}
|
||||||
|
rowsPerPage={rowsPerPage}
|
||||||
|
page={page}
|
||||||
|
onPageChange={handleChangePage}
|
||||||
|
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InterpretButtonProps {
|
||||||
|
handleInterpret: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InterpretButton = ( {handleInterpret}:InterpretButtonProps) => {
|
||||||
|
return (
|
||||||
|
<IconButton aria-label="add" size= "small" onClick={() => {
|
||||||
|
handleInterpret();
|
||||||
|
}}
|
||||||
|
sx={{'&:hover': { color: '#1976d2', backgroundColor: 'transparent' }}}>
|
||||||
|
<PlayCircle/>
|
||||||
|
</IconButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
170
src/components/molecules/RunContent.tsx
Normal file
170
src/components/molecules/RunContent.tsx
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import { Box, Tabs, Typography, Tab } from "@mui/material";
|
||||||
|
import Highlight from "react-highlight";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import * as React from "react";
|
||||||
|
import { Data } from "./RunsTable";
|
||||||
|
import { TabPanel, TabContext } from "@mui/lab";
|
||||||
|
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 AssignmentIcon from '@mui/icons-material/Assignment';
|
||||||
|
|
||||||
|
interface RunContentProps {
|
||||||
|
row: Data,
|
||||||
|
currentLog: string,
|
||||||
|
interpretationInProgress: boolean,
|
||||||
|
logEndRef: React.RefObject<HTMLDivElement>,
|
||||||
|
abortRunHandler: () => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RunContent = ({row, currentLog, interpretationInProgress, logEndRef, abortRunHandler}: RunContentProps) => {
|
||||||
|
const [tab, setTab] = React.useState<string>('log');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTab(tab);
|
||||||
|
}, [interpretationInProgress])
|
||||||
|
|
||||||
|
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="Input" value='input' />
|
||||||
|
<Tab label="Output" value='output' />
|
||||||
|
</Tabs>
|
||||||
|
</Box>
|
||||||
|
<TabPanel value='log'>
|
||||||
|
<Box sx={{ margin: 1,
|
||||||
|
background: '#19171c',
|
||||||
|
overflowY: 'scroll',
|
||||||
|
overflowX: 'scroll',
|
||||||
|
width: '700px',
|
||||||
|
height: 'fit-content',
|
||||||
|
maxHeight: '450px',
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<Highlight className="javascript">
|
||||||
|
{interpretationInProgress ? currentLog : row.log}
|
||||||
|
</Highlight>
|
||||||
|
<div style={{ float:"left", clear: "both" }}
|
||||||
|
ref={logEndRef}/>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
{interpretationInProgress ? <Button
|
||||||
|
color="error"
|
||||||
|
onClick={abortRunHandler}
|
||||||
|
>
|
||||||
|
Stop
|
||||||
|
</Button> : null}
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value='input' sx={{width: '700px'}}>
|
||||||
|
<Typography variant='h6' sx={{display:'flex', alignItems:'center'}}>
|
||||||
|
<SettingsIcon sx={{marginRight: '15px'}}/>
|
||||||
|
Interpreter settings
|
||||||
|
</Typography>
|
||||||
|
{
|
||||||
|
Object.keys(row.interpreterSettings).map((setting, index) => {
|
||||||
|
if (setting === 'params') {
|
||||||
|
return (
|
||||||
|
<div key={`settings-${setting}-${index}`}>
|
||||||
|
<Typography variant='h6' sx={{display:'flex', alignItems:'center'}} key={`setting-${index}`}>
|
||||||
|
<AssignmentIcon sx={{marginRight: '15px'}}/>
|
||||||
|
Recording parameters
|
||||||
|
</Typography>
|
||||||
|
{
|
||||||
|
Object.keys(row.interpreterSettings.params).map((param, index) => {
|
||||||
|
return (
|
||||||
|
<Typography key={`recording-params-item-${index}`} sx={{margin: '10px'}}>
|
||||||
|
{/*@ts-ignore*/}
|
||||||
|
{param}: {row.interpreterSettings.params[param].toString()}
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Typography key={`interpreter-settings-item-${index}`} sx={{margin: '10px'}}>
|
||||||
|
{/*@ts-ignore*/}
|
||||||
|
{setting}: {row.interpreterSettings[setting].toString()}
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value='output' sx={{width: '700px'}}>
|
||||||
|
{ !row || !row.serializableOutput || !row.binaryOutput
|
||||||
|
|| (Object.keys(row.serializableOutput).length === 0 && Object.keys(row.binaryOutput).length === 0)
|
||||||
|
? <Typography>The output is empty.</Typography> : null }
|
||||||
|
|
||||||
|
{row.serializableOutput &&
|
||||||
|
Object.keys(row.serializableOutput).length !== 0 &&
|
||||||
|
<div>
|
||||||
|
<Typography variant='h6' sx={{display:'flex', alignItems:'center'}}>
|
||||||
|
<ArticleIcon sx={{marginRight: '15px'}}/>
|
||||||
|
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>
|
||||||
|
</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>
|
||||||
|
}
|
||||||
|
{row.binaryOutput
|
||||||
|
&& Object.keys(row.binaryOutput).length !== 0 &&
|
||||||
|
<div>
|
||||||
|
<Typography variant='h6' sx={{display:'flex', alignItems:'center'}}>
|
||||||
|
<ImageIcon sx={{marginRight:'15px'}}/>
|
||||||
|
Binary output</Typography>
|
||||||
|
{ Object.keys(row.binaryOutput).map((key) => {
|
||||||
|
try {
|
||||||
|
const binaryBuffer = JSON.parse(row.binaryOutput[key].data);
|
||||||
|
const b64 = Buffer.from(binaryBuffer.data).toString('base64');
|
||||||
|
return (
|
||||||
|
<Box key={`number-of-binary-output-${key}`} sx={{
|
||||||
|
width: 'max-content',
|
||||||
|
}}>
|
||||||
|
<Typography key={`binary-output-key-${key}`}>
|
||||||
|
{key}:
|
||||||
|
<a href={`data:${row.binaryOutput[key].mimetype};base64,${b64}`}
|
||||||
|
download={key} style={{margin:'10px'}}>Download</a>
|
||||||
|
</Typography>
|
||||||
|
<img key={`image-${key}`} src={`data:${row.binaryOutput[key].mimetype};base64,${b64}`}
|
||||||
|
alt={key} height='auto' width='700px'/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
return <Typography key={`number-of-binary-output-${key}`}>
|
||||||
|
{key}: The image failed to render
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</TabPanel>
|
||||||
|
</TabContext>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
114
src/components/molecules/RunSettings.tsx
Normal file
114
src/components/molecules/RunSettings.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { GenericModal } from "../atoms/GenericModal";
|
||||||
|
import { MenuItem, TextField, Typography } from "@mui/material";
|
||||||
|
import { Dropdown } from "../atoms/DropdownMui";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import { modalStyle } from "./AddWhereCondModal";
|
||||||
|
|
||||||
|
interface RunSettingsProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
handleStart: (settings: RunSettings) => void;
|
||||||
|
handleClose: () => void;
|
||||||
|
isTask: boolean;
|
||||||
|
params?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RunSettings {
|
||||||
|
maxConcurrency: number;
|
||||||
|
maxRepeats: number;
|
||||||
|
debug: boolean;
|
||||||
|
params?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RunSettingsModal = ({ isOpen, handleStart, handleClose, isTask, params }: RunSettingsProps) => {
|
||||||
|
|
||||||
|
const [settings, setSettings] = React.useState<RunSettings>({
|
||||||
|
maxConcurrency: 1,
|
||||||
|
maxRepeats: 1,
|
||||||
|
debug: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GenericModal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={handleClose}
|
||||||
|
modalStyle={modalStyle}
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
marginLeft: '65px',
|
||||||
|
}}>
|
||||||
|
{ isTask
|
||||||
|
?
|
||||||
|
(
|
||||||
|
<React.Fragment>
|
||||||
|
<Typography sx={{ margin: '20px 0px' }} >Recording parameters:</Typography>
|
||||||
|
{ params?.map((item, index) => {
|
||||||
|
return <TextField
|
||||||
|
sx={{ marginBottom: '15px' }}
|
||||||
|
key={`param-${index}`}
|
||||||
|
type="string"
|
||||||
|
label={item}
|
||||||
|
required
|
||||||
|
onChange={(e) => setSettings(
|
||||||
|
{
|
||||||
|
...settings,
|
||||||
|
params: settings.params
|
||||||
|
? {
|
||||||
|
...settings.params,
|
||||||
|
[item]: e.target.value,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
[item]: e.target.value,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
}) }
|
||||||
|
</React.Fragment>)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
<Typography sx={{ margin: '20px 0px' }} >Interpreter settings:</Typography>
|
||||||
|
<TextField
|
||||||
|
sx={{ marginBottom: '15px' }}
|
||||||
|
type="number"
|
||||||
|
label="maxConcurrency"
|
||||||
|
required
|
||||||
|
onChange={(e) => setSettings(
|
||||||
|
{
|
||||||
|
...settings,
|
||||||
|
maxConcurrency: parseInt(e.target.value),
|
||||||
|
})}
|
||||||
|
defaultValue={settings.maxConcurrency}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
sx={{ marginBottom: '15px' }}
|
||||||
|
type="number"
|
||||||
|
label="maxRepeats"
|
||||||
|
required
|
||||||
|
onChange={(e) => setSettings(
|
||||||
|
{
|
||||||
|
...settings,
|
||||||
|
maxRepeats: parseInt(e.target.value),
|
||||||
|
})}
|
||||||
|
defaultValue={settings.maxRepeats}
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
id="debug"
|
||||||
|
label="debug"
|
||||||
|
value={settings.debug?.toString()}
|
||||||
|
handleSelect={(e) => setSettings(
|
||||||
|
{
|
||||||
|
...settings,
|
||||||
|
debug: e.target.value === "true",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<MenuItem value="true">true</MenuItem>
|
||||||
|
<MenuItem value="false">false</MenuItem>
|
||||||
|
</Dropdown>
|
||||||
|
<Button onClick={() => handleStart(settings)}>Start</Button>
|
||||||
|
</div>
|
||||||
|
</GenericModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
153
src/components/molecules/RunsTable.tsx
Normal file
153
src/components/molecules/RunsTable.tsx
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
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 TablePagination from '@mui/material/TablePagination';
|
||||||
|
import TableRow from '@mui/material/TableRow';
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||||
|
import { getStoredRuns } from "../../api/storage";
|
||||||
|
import { RunSettings } from "./RunSettings";
|
||||||
|
import { CollapsibleRow } from "./ColapsibleRow";
|
||||||
|
|
||||||
|
interface Column {
|
||||||
|
id: 'status' | 'name' | 'startedAt' | 'finishedAt' | 'duration' | 'task' | 'runId' | 'delete';
|
||||||
|
label: string;
|
||||||
|
minWidth?: number;
|
||||||
|
align?: 'right';
|
||||||
|
format?: (value: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const columns: readonly Column[] = [
|
||||||
|
{ id: 'status', label: 'Status', minWidth: 80 },
|
||||||
|
{ id: 'name', label: '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: 'delete', label: 'Delete', minWidth: 80 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface Data {
|
||||||
|
id: number;
|
||||||
|
status: string;
|
||||||
|
name: string;
|
||||||
|
startedAt: string;
|
||||||
|
finishedAt: string;
|
||||||
|
duration: string;
|
||||||
|
task: string;
|
||||||
|
log: string;
|
||||||
|
runId: string;
|
||||||
|
interpreterSettings: RunSettings;
|
||||||
|
serializableOutput: any;
|
||||||
|
binaryOutput: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RunsTableProps {
|
||||||
|
currentInterpretationLog: string;
|
||||||
|
abortRunHandler: () => void;
|
||||||
|
runId: string;
|
||||||
|
runningRecordingName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RunsTable = (
|
||||||
|
{ currentInterpretationLog, abortRunHandler, runId, runningRecordingName }: RunsTableProps) => {
|
||||||
|
const [page, setPage] = useState(0);
|
||||||
|
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||||
|
const [rows, setRows] = useState<Data[]>([]);
|
||||||
|
|
||||||
|
const { notify, rerenderRuns, setRerenderRuns } = useGlobalInfoStore();
|
||||||
|
|
||||||
|
const handleChangePage = (event: unknown, newPage: number) => {
|
||||||
|
setPage(newPage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setRowsPerPage(+event.target.value);
|
||||||
|
setPage(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchRuns = async () => {
|
||||||
|
const runs = await getStoredRuns();
|
||||||
|
if (runs) {
|
||||||
|
const parsedRows: Data[] = [];
|
||||||
|
runs.map((run, index) => {
|
||||||
|
const parsedRun = JSON.parse(run);
|
||||||
|
parsedRows.push({
|
||||||
|
id: index,
|
||||||
|
...parsedRun,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setRows(parsedRows);
|
||||||
|
} else {
|
||||||
|
console.log('No runs found.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect( () => {
|
||||||
|
if (rows.length === 0 || rerenderRuns) {
|
||||||
|
fetchRuns();
|
||||||
|
setRerenderRuns(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [rerenderRuns]);
|
||||||
|
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
setRows([]);
|
||||||
|
notify('success', 'Run deleted successfully');
|
||||||
|
fetchRuns();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<TableContainer component={Paper} sx={{ width: '100%', overflow: 'hidden' }}>
|
||||||
|
<Table stickyHeader aria-label="sticky table" >
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell />
|
||||||
|
{columns.map((column) => (
|
||||||
|
<TableCell
|
||||||
|
key={column.id}
|
||||||
|
align={column.align}
|
||||||
|
style={{ minWidth: column.minWidth }}
|
||||||
|
>
|
||||||
|
{column.label}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{rows.length !== 0 ? rows
|
||||||
|
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
||||||
|
.map((row, index) =>
|
||||||
|
<CollapsibleRow
|
||||||
|
row={row}
|
||||||
|
handleDelete={handleDelete}
|
||||||
|
key={`row-${row.id}`}
|
||||||
|
isOpen={runId === row.runId && runningRecordingName === row.name}
|
||||||
|
currentLog={currentInterpretationLog}
|
||||||
|
abortRunHandler={abortRunHandler}
|
||||||
|
runningRecordingName={runningRecordingName}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
: null }
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
<TablePagination
|
||||||
|
rowsPerPageOptions={[10, 25, 50]}
|
||||||
|
component="div"
|
||||||
|
count={rows ? rows.length : 0}
|
||||||
|
rowsPerPage={rowsPerPage}
|
||||||
|
page={page}
|
||||||
|
onPageChange={handleChangePage}
|
||||||
|
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
125
src/components/molecules/SaveRecording.tsx
Normal file
125
src/components/molecules/SaveRecording.tsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { IconButton, Button, Box, LinearProgress, Tooltip } from "@mui/material";
|
||||||
|
import { GenericModal } from "../atoms/GenericModal";
|
||||||
|
import { stopRecording } from "../../api/recording";
|
||||||
|
import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||||
|
import { useSocketStore } from "../../context/socket";
|
||||||
|
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';
|
||||||
|
|
||||||
|
interface SaveRecordingProps {
|
||||||
|
fileName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SaveRecording = ({fileName}: SaveRecordingProps) => {
|
||||||
|
|
||||||
|
const [openModal, setOpenModal] = useState<boolean>(false);
|
||||||
|
const [needConfirm, setNeedConfirm] = useState<boolean>(false);
|
||||||
|
const [recordingName, setRecordingName] = useState<string>(fileName);
|
||||||
|
const [waitingForSave, setWaitingForSave] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const { browserId, setBrowserId, notify, recordings } = useGlobalInfoStore();
|
||||||
|
const { socket } = useSocketStore();
|
||||||
|
|
||||||
|
const handleChangeOfTitle = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { value } = event.target;
|
||||||
|
if (needConfirm) {
|
||||||
|
setNeedConfirm(false);
|
||||||
|
}
|
||||||
|
setRecordingName(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveRecording = async (event: React.SyntheticEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (recordings.includes(recordingName)) {
|
||||||
|
if (needConfirm) { return; }
|
||||||
|
setNeedConfirm(true);
|
||||||
|
} else {
|
||||||
|
await saveRecording();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const exitRecording = useCallback(async() => {
|
||||||
|
notify('success', 'Recording saved successfully');
|
||||||
|
if (browserId) {
|
||||||
|
await stopRecording(browserId);
|
||||||
|
}
|
||||||
|
setBrowserId(null);
|
||||||
|
}, [setBrowserId, browserId, notify]);
|
||||||
|
|
||||||
|
// notifies backed to save the recording in progress,
|
||||||
|
// releases resources and changes the view for main page by clearing the global browserId
|
||||||
|
const saveRecording = async () => {
|
||||||
|
socket?.emit('save', recordingName)
|
||||||
|
setWaitingForSave(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
socket?.on('fileSaved', exitRecording);
|
||||||
|
return () => {
|
||||||
|
socket?.off('fileSaved', exitRecording);
|
||||||
|
}
|
||||||
|
}, [socket, exitRecording]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button sx={{
|
||||||
|
width:'100px',
|
||||||
|
background: 'white',
|
||||||
|
color: 'rgba(0,128,0,0.7)',
|
||||||
|
'&:hover': {background: 'white', color: 'green'},
|
||||||
|
padding: '11px',
|
||||||
|
marginRight: '10px',
|
||||||
|
}} onClick={() => setOpenModal(true)}>
|
||||||
|
<FlagIcon sx={{marginRight:'3px'}}/> FINISH
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<GenericModal isOpen={openModal} onClose={() => setOpenModal(false)} modalStyle={modalStyle}>
|
||||||
|
<form onSubmit={handleSaveRecording} style={{paddingTop:'50px', display: 'flex', flexDirection: 'column'}} >
|
||||||
|
<Typography>Save the recording as:</Typography>
|
||||||
|
<TextField
|
||||||
|
required
|
||||||
|
sx={{width: '250px', paddingBottom: '10px', margin: '15px 0px'}}
|
||||||
|
onChange={handleChangeOfTitle}
|
||||||
|
id="title"
|
||||||
|
label="Recording title"
|
||||||
|
variant="outlined"
|
||||||
|
defaultValue={recordingName ? recordingName : null}
|
||||||
|
/>
|
||||||
|
{ needConfirm
|
||||||
|
?
|
||||||
|
(<React.Fragment>
|
||||||
|
<Button color="error" variant="contained" onClick={saveRecording}>Confirm</Button>
|
||||||
|
<WarningText>
|
||||||
|
<NotificationImportantIcon color="warning"/>
|
||||||
|
Recording already exists, please confirm the recording's overwrite.
|
||||||
|
</WarningText>
|
||||||
|
</React.Fragment>)
|
||||||
|
: <Button type="submit" variant="contained">Save</Button>
|
||||||
|
}
|
||||||
|
{ waitingForSave &&
|
||||||
|
<Tooltip title='Optimizing and saving the workflow' placement={"bottom"}>
|
||||||
|
<Box sx={{ width: '100%' }}>
|
||||||
|
<LinearProgress/>
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
</form>
|
||||||
|
</GenericModal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalStyle = {
|
||||||
|
top: '25%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
width: '20%',
|
||||||
|
backgroundColor: 'background.paper',
|
||||||
|
p: 4,
|
||||||
|
height:'fit-content',
|
||||||
|
display:'block',
|
||||||
|
padding: '20px',
|
||||||
|
};
|
||||||
38
src/components/molecules/SidePanelHeader.tsx
Normal file
38
src/components/molecules/SidePanelHeader.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import React, { FC, useState } from 'react';
|
||||||
|
import { InterpretationButtons } from "./InterpretationButtons";
|
||||||
|
import { AddButton } from "../atoms/buttons/AddButton";
|
||||||
|
import { GenericModal } from "../atoms/GenericModal";
|
||||||
|
import { PairEditForm } from "./PairEditForm";
|
||||||
|
import { WhereWhatPair, WorkflowFile } from "@wbr-project/wbr-interpret";
|
||||||
|
import { AddPair } from "../../api/workflow";
|
||||||
|
import { Button, Stack } from "@mui/material";
|
||||||
|
import { FastForward } from "@mui/icons-material";
|
||||||
|
import { useSocketStore } from "../../context/socket";
|
||||||
|
import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||||
|
|
||||||
|
export const SidePanelHeader = () => {
|
||||||
|
|
||||||
|
const [steppingIsDisabled, setSteppingIsDisabled] = useState(true);
|
||||||
|
|
||||||
|
const { socket } = useSocketStore();
|
||||||
|
|
||||||
|
const handleStep = () => {
|
||||||
|
socket?.emit('step');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{width: 'inherit'}}>
|
||||||
|
<InterpretationButtons enableStepping={(isPaused) => setSteppingIsDisabled(!isPaused)}/>
|
||||||
|
<Button
|
||||||
|
variant='outlined'
|
||||||
|
disabled={steppingIsDisabled}
|
||||||
|
onClick={handleStep}
|
||||||
|
sx={{marginLeft:'15px'}}
|
||||||
|
>
|
||||||
|
step
|
||||||
|
<FastForward/>
|
||||||
|
</Button>
|
||||||
|
<hr/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
61
src/components/molecules/ToggleButton.tsx
Normal file
61
src/components/molecules/ToggleButton.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import React, { FC } from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
interface ToggleButtonProps {
|
||||||
|
isChecked?: boolean;
|
||||||
|
onChange: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ToggleButton: FC<ToggleButtonProps> = ({ isChecked = false, onChange }) => (
|
||||||
|
<CheckBoxWrapper>
|
||||||
|
<CheckBox id="checkbox" type="checkbox" onClick={onChange} checked={isChecked}/>
|
||||||
|
<CheckBoxLabel htmlFor="checkbox"/>
|
||||||
|
</CheckBoxWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
const CheckBoxWrapper = styled.div`
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
const CheckBoxLabel = styled.label`
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 42px;
|
||||||
|
height: 26px;
|
||||||
|
border-radius: 15px;
|
||||||
|
background: #bebebe;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
margin: 3px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 1px 3px 3px 1px rgba(0, 0, 0, 0.2);
|
||||||
|
transition: 0.2s;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const CheckBox = styled.input`
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 1;
|
||||||
|
border-radius: 15px;
|
||||||
|
width: 42px;
|
||||||
|
height: 26px;
|
||||||
|
|
||||||
|
&:checked + ${CheckBoxLabel} {
|
||||||
|
background: #2196F3;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
margin-left: 21px;
|
||||||
|
transition: 0.2s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
135
src/components/organisms/LeftSidePanel.tsx
Normal file
135
src/components/organisms/LeftSidePanel.tsx
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { Box, Paper, Tab, Tabs } from "@mui/material";
|
||||||
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
|
import { getActiveWorkflow, getParamsOfActiveWorkflow } from "../../api/workflow";
|
||||||
|
import { useSocketStore } from '../../context/socket';
|
||||||
|
import { WhereWhatPair, WorkflowFile } from "@wbr-project/wbr-interpret";
|
||||||
|
import { SidePanelHeader } from "../molecules/SidePanelHeader";
|
||||||
|
import { emptyWorkflow } from "../../shared/constants";
|
||||||
|
import { LeftSidePanelContent } from "../molecules/LeftSidePanelContent";
|
||||||
|
import { useBrowserDimensionsStore } from "../../context/browserDimensions";
|
||||||
|
import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||||
|
import { TabContext, TabPanel } from "@mui/lab";
|
||||||
|
import { LeftSidePanelSettings } from "../molecules/LeftSidePanelSettings";
|
||||||
|
import { RunSettings } from "../molecules/RunSettings";
|
||||||
|
|
||||||
|
const fetchWorkflow = (id: string, callback: (response: WorkflowFile) => void) => {
|
||||||
|
getActiveWorkflow(id).then(
|
||||||
|
(response ) => {
|
||||||
|
if (response){
|
||||||
|
callback(response);
|
||||||
|
} else {
|
||||||
|
throw new Error("No workflow found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).catch((error) => {console.log(error.message)})
|
||||||
|
};
|
||||||
|
|
||||||
|
interface LeftSidePanelProps {
|
||||||
|
sidePanelRef: HTMLDivElement | null;
|
||||||
|
alreadyHasScrollbar: boolean;
|
||||||
|
recordingName: string;
|
||||||
|
handleSelectPairForEdit: (pair:WhereWhatPair, index:number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LeftSidePanel = (
|
||||||
|
{ sidePanelRef, alreadyHasScrollbar, recordingName, handleSelectPairForEdit }: LeftSidePanelProps) => {
|
||||||
|
|
||||||
|
const [workflow, setWorkflow] = useState<WorkflowFile>(emptyWorkflow);
|
||||||
|
const [hasScrollbar, setHasScrollbar] = useState<boolean>(alreadyHasScrollbar);
|
||||||
|
const [tab, setTab] = useState<string>('recording');
|
||||||
|
const [params, setParams] = useState<string[]>([]);
|
||||||
|
const [settings, setSettings] = React.useState<RunSettings>({
|
||||||
|
maxConcurrency: 1,
|
||||||
|
maxRepeats: 1,
|
||||||
|
debug: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { id, socket } = useSocketStore();
|
||||||
|
const { setWidth, width } = useBrowserDimensionsStore();
|
||||||
|
const { setRecordingLength } = useGlobalInfoStore();
|
||||||
|
|
||||||
|
const workflowHandler = useCallback((data: WorkflowFile) => {
|
||||||
|
setWorkflow(data);
|
||||||
|
setRecordingLength(data.workflow.length);
|
||||||
|
}, [workflow])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// fetch the workflow every time the id changes
|
||||||
|
if (id) {
|
||||||
|
fetchWorkflow(id, workflowHandler);
|
||||||
|
}
|
||||||
|
// fetch workflow in 15min intervals
|
||||||
|
let interval = setInterval(() =>{
|
||||||
|
if (id) {
|
||||||
|
fetchWorkflow(id, workflowHandler);
|
||||||
|
}}, (1000 * 60 * 15));
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (socket) {
|
||||||
|
socket.on("workflow", workflowHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sidePanelRef) {
|
||||||
|
const workflowListHeight = sidePanelRef.clientHeight;
|
||||||
|
const innerHeightWithoutNavbar = window.innerHeight - 70;
|
||||||
|
if (innerHeightWithoutNavbar <= workflowListHeight) {
|
||||||
|
if (!hasScrollbar) {
|
||||||
|
setWidth(width - 10);
|
||||||
|
setHasScrollbar(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (hasScrollbar && !alreadyHasScrollbar) {
|
||||||
|
setWidth(width + 10);
|
||||||
|
setHasScrollbar(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket?.off('workflow', workflowHandler);
|
||||||
|
}
|
||||||
|
}, [socket, workflowHandler]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
sx={{
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
backgroundColor: 'lightgray',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SidePanelHeader/>
|
||||||
|
<TabContext value={tab}>
|
||||||
|
<Tabs value={tab} onChange={(e, newTab) => setTab(newTab)}>
|
||||||
|
<Tab label="Recording" value='recording' />
|
||||||
|
<Tab label="Settings" value='settings' onClick={() => {
|
||||||
|
getParamsOfActiveWorkflow(id).then((response) => {
|
||||||
|
if (response) {
|
||||||
|
setParams(response);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}/>
|
||||||
|
</Tabs>
|
||||||
|
<TabPanel value='recording' sx={{padding: '0px'}}>
|
||||||
|
<LeftSidePanelContent
|
||||||
|
workflow={workflow}
|
||||||
|
updateWorkflow={setWorkflow}
|
||||||
|
recordingName={recordingName}
|
||||||
|
handleSelectPairForEdit={handleSelectPairForEdit}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value='settings'>
|
||||||
|
<LeftSidePanelSettings params={params}
|
||||||
|
settings={settings} setSettings={setSettings}/>
|
||||||
|
</TabPanel>
|
||||||
|
</TabContext>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
|
||||||
|
};
|
||||||
51
src/components/organisms/MainMenu.tsx
Normal file
51
src/components/organisms/MainMenu.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import Tabs from '@mui/material/Tabs';
|
||||||
|
import Tab from '@mui/material/Tab';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import { Paper } from "@mui/material";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
interface MainMenuProps {
|
||||||
|
value: string;
|
||||||
|
handleChangeContent: (newValue: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenuProps) => {
|
||||||
|
|
||||||
|
const handleChange = (event: React.SyntheticEvent, newValue: string) => {
|
||||||
|
handleChangeContent(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
sx={{
|
||||||
|
height: 'auto',
|
||||||
|
maxWidth: 'fit-content',
|
||||||
|
backgroundColor: 'lightgray',
|
||||||
|
paddingTop: '2rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{
|
||||||
|
width: '100%',
|
||||||
|
paddingBottom: '22rem',
|
||||||
|
}}>
|
||||||
|
<Tabs
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
textColor="primary"
|
||||||
|
indicatorColor="primary"
|
||||||
|
orientation="vertical"
|
||||||
|
>
|
||||||
|
<Tab sx={{
|
||||||
|
alignItems: 'baseline',
|
||||||
|
fontSize:'medium',
|
||||||
|
}} value="recordings" label="Recordings" />
|
||||||
|
<Tab sx={{
|
||||||
|
alignItems: 'baseline',
|
||||||
|
fontSize:'medium',
|
||||||
|
}} value="runs" label="Runs" />
|
||||||
|
</Tabs>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
52
src/components/organisms/Recordings.tsx
Normal file
52
src/components/organisms/Recordings.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { RecordingsTable } from "../molecules/RecordingsTable";
|
||||||
|
import { Grid } from "@mui/material";
|
||||||
|
import { RunSettings, RunSettingsModal } from "../molecules/RunSettings";
|
||||||
|
|
||||||
|
interface RecordingsProps {
|
||||||
|
handleEditRecording: (fileName: string) => void;
|
||||||
|
handleRunRecording: (settings: RunSettings) => void;
|
||||||
|
setFileName: (fileName: string) => void;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Recordings = ({ handleEditRecording, handleRunRecording, setFileName }: RecordingsProps) => {
|
||||||
|
const [runSettingsAreOpen, setRunSettingsAreOpen] = useState(false);
|
||||||
|
const [params, setParams] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const handleSettingsAndRun = (fileName: string, params: string[]) => {
|
||||||
|
if (params.length === 0) {
|
||||||
|
setRunSettingsAreOpen(true);
|
||||||
|
setFileName(fileName);
|
||||||
|
} else {
|
||||||
|
setParams(params);
|
||||||
|
setRunSettingsAreOpen(true);
|
||||||
|
setFileName(fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setParams([]);
|
||||||
|
setRunSettingsAreOpen(false);
|
||||||
|
setFileName('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<RunSettingsModal isOpen={runSettingsAreOpen}
|
||||||
|
handleClose={handleClose}
|
||||||
|
handleStart={ (settings) => handleRunRecording(settings) }
|
||||||
|
isTask={params.length !== 0}
|
||||||
|
params={params}
|
||||||
|
/>
|
||||||
|
<Grid container direction="column" sx={{ padding: '30px'}}>
|
||||||
|
<Grid item xs>
|
||||||
|
<RecordingsTable
|
||||||
|
handleEditRecording={handleEditRecording}
|
||||||
|
handleRunRecording={handleSettingsAndRun}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
106
src/components/organisms/RightSidePanel.tsx
Normal file
106
src/components/organisms/RightSidePanel.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Button, MenuItem, Paper, Stack, Tabs, Tab } from "@mui/material";
|
||||||
|
import { Dropdown as MuiDropdown } from '../atoms/DropdownMui';
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { ActionSettings } from "../molecules/ActionSettings";
|
||||||
|
import { SelectChangeEvent } from "@mui/material/Select/Select";
|
||||||
|
import { SimpleBox } from "../atoms/Box";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||||
|
import { PairDetail } from "../molecules/PairDetail";
|
||||||
|
import { PairForEdit } from "../../pages/RecordingPage";
|
||||||
|
|
||||||
|
interface RightSidePanelProps {
|
||||||
|
pairForEdit: PairForEdit;
|
||||||
|
changeBrowserDimensions: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RightSidePanel = ({pairForEdit, changeBrowserDimensions}: RightSidePanelProps) => {
|
||||||
|
|
||||||
|
const [content, setContent] = useState<string>('action');
|
||||||
|
const [action, setAction] = React.useState<string>('');
|
||||||
|
const [isSettingsDisplayed, setIsSettingsDisplayed] = React.useState<boolean>(false);
|
||||||
|
|
||||||
|
const { lastAction } = useGlobalInfoStore();
|
||||||
|
|
||||||
|
const handleChange = (event: React.SyntheticEvent, newValue: string) => {
|
||||||
|
setContent(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleActionSelect = (event: SelectChangeEvent) => {
|
||||||
|
const { value } = event.target;
|
||||||
|
setAction(value);
|
||||||
|
setIsSettingsDisplayed(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (content !== 'detail' && pairForEdit.pair !== null) {
|
||||||
|
setContent('detail');
|
||||||
|
}
|
||||||
|
}, [pairForEdit])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
alignItems: "center",
|
||||||
|
}}>
|
||||||
|
<Button onClick={() => {
|
||||||
|
changeBrowserDimensions();
|
||||||
|
}}>resize browser</Button>
|
||||||
|
<SimpleBox height={60} width='100%' background='lightGray' radius='0%'>
|
||||||
|
<Typography sx={{ padding: '10px' }}>
|
||||||
|
Last action:
|
||||||
|
{` ${lastAction}`}
|
||||||
|
</Typography>
|
||||||
|
</SimpleBox>
|
||||||
|
|
||||||
|
<Tabs value={content} onChange={handleChange} centered>
|
||||||
|
<Tab label="Actions" value="action" />
|
||||||
|
<Tab label="Pair detail" value="detail"/>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
{content === 'action' ? (
|
||||||
|
<React.Fragment>
|
||||||
|
<ActionDescription>Type of action:</ActionDescription>
|
||||||
|
<ActionTypeWrapper>
|
||||||
|
<MuiDropdown
|
||||||
|
id="action"
|
||||||
|
label="Action"
|
||||||
|
value={action}
|
||||||
|
handleSelect={handleActionSelect}>
|
||||||
|
<MenuItem value="mouse.click">click on coordinates</MenuItem>
|
||||||
|
<MenuItem value="enqueueLinks">enqueueLinks</MenuItem>
|
||||||
|
<MenuItem value="scrape">scrape</MenuItem>
|
||||||
|
<MenuItem value="scrapeSchema">scrapeSchema</MenuItem>
|
||||||
|
<MenuItem value="screenshot">screenshot</MenuItem>
|
||||||
|
<MenuItem value="script">script</MenuItem>
|
||||||
|
<MenuItem value="scroll">scroll</MenuItem>
|
||||||
|
</MuiDropdown>
|
||||||
|
</ActionTypeWrapper>
|
||||||
|
|
||||||
|
{isSettingsDisplayed &&
|
||||||
|
<ActionSettings action={action}/>
|
||||||
|
}
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
: <PairDetail pair={pairForEdit.pair} index={pairForEdit.index}/>
|
||||||
|
}
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ActionTypeWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ActionDescription = styled.p`
|
||||||
|
margin-left: 15px;
|
||||||
|
`;
|
||||||
27
src/components/organisms/Runs.tsx
Normal file
27
src/components/organisms/Runs.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { Grid } from "@mui/material";
|
||||||
|
import { RunsTable } from "../molecules/RunsTable";
|
||||||
|
|
||||||
|
interface RunsProps {
|
||||||
|
currentInterpretationLog: string;
|
||||||
|
abortRunHandler: () => void;
|
||||||
|
runId: string;
|
||||||
|
runningRecordingName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Runs = (
|
||||||
|
{ currentInterpretationLog, abortRunHandler, runId, runningRecordingName }: RunsProps) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container direction="column" sx={{ padding: '30px'}}>
|
||||||
|
<Grid item xs>
|
||||||
|
<RunsTable
|
||||||
|
currentInterpretationLog={currentInterpretationLog}
|
||||||
|
abortRunHandler={abortRunHandler}
|
||||||
|
runId={runId}
|
||||||
|
runningRecordingName={runningRecordingName}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
124
src/pages/MainPage.tsx
Normal file
124
src/pages/MainPage.tsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import React, { useCallback, useEffect } from 'react';
|
||||||
|
import { MainMenu } from "../components/organisms/MainMenu";
|
||||||
|
import { Grid, Stack } from "@mui/material";
|
||||||
|
import { Recordings } from "../components/organisms/Recordings";
|
||||||
|
import { Runs } from "../components/organisms/Runs";
|
||||||
|
import { useGlobalInfoStore } from "../context/globalInfo";
|
||||||
|
import { createRunForStoredRecording, interpretStoredRecording, notifyAboutAbort } from "../api/storage";
|
||||||
|
import { io, Socket } from "socket.io-client";
|
||||||
|
import { stopRecording } from "../api/recording";
|
||||||
|
import { RunSettings } from "../components/molecules/RunSettings";
|
||||||
|
|
||||||
|
interface MainPageProps {
|
||||||
|
handleEditRecording: (fileName: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateRunResponse {
|
||||||
|
browserId: string;
|
||||||
|
runId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MainPage = ({ handleEditRecording }: MainPageProps) => {
|
||||||
|
|
||||||
|
const [content, setContent] = React.useState('recordings');
|
||||||
|
const [sockets, setSockets] = React.useState<Socket[]>([]);
|
||||||
|
const [runningRecordingName, setRunningRecordingName] = React.useState('');
|
||||||
|
const [currentInterpretationLog, setCurrentInterpretationLog] = React.useState('');
|
||||||
|
const [ids, setIds] = React.useState<CreateRunResponse>({
|
||||||
|
browserId: '',
|
||||||
|
runId: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
let aborted = false;
|
||||||
|
|
||||||
|
const { notify, setRerenderRuns } = useGlobalInfoStore();
|
||||||
|
|
||||||
|
const abortRunHandler = (runId: string) => {
|
||||||
|
aborted = true;
|
||||||
|
notifyAboutAbort(runningRecordingName, runId).then(async (response) => {
|
||||||
|
if (response) {
|
||||||
|
notify('success', `Interpretation of ${runningRecordingName} aborted successfully`);
|
||||||
|
await stopRecording(ids.browserId);
|
||||||
|
} else {
|
||||||
|
notify('error', `Failed to abort the interpretation ${runningRecordingName} recording`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const setFileName = (fileName: string) => {
|
||||||
|
setRunningRecordingName(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const readyForRunHandler = useCallback( (browserId: string, runId: string) => {
|
||||||
|
interpretStoredRecording(runningRecordingName, runId).then( async (interpretation: boolean) => {
|
||||||
|
if (!aborted) {
|
||||||
|
if (interpretation) {
|
||||||
|
notify('success', `Interpretation of ${runningRecordingName} succeeded`);
|
||||||
|
} else {
|
||||||
|
notify('success', `Failed to interpret ${runningRecordingName} recording`);
|
||||||
|
// destroy the created browser
|
||||||
|
await stopRecording(browserId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setRunningRecordingName('');
|
||||||
|
setCurrentInterpretationLog('');
|
||||||
|
setRerenderRuns(true);
|
||||||
|
})
|
||||||
|
}, [runningRecordingName, aborted, currentInterpretationLog, notify, setRerenderRuns]);
|
||||||
|
|
||||||
|
const debugMessageHandler = useCallback((msg: string) => {
|
||||||
|
setCurrentInterpretationLog((prevState) =>
|
||||||
|
prevState + '\n' + `[${new Date().toLocaleString()}] ` + msg);
|
||||||
|
}, [currentInterpretationLog])
|
||||||
|
|
||||||
|
const handleRunRecording = useCallback((settings: RunSettings) => {
|
||||||
|
createRunForStoredRecording(runningRecordingName, settings).then(({browserId, runId}: CreateRunResponse) => {
|
||||||
|
setIds({browserId, runId});
|
||||||
|
const socket =
|
||||||
|
io(`http://localhost:8080/${browserId}`, {
|
||||||
|
transports: ["websocket"],
|
||||||
|
rejectUnauthorized: false
|
||||||
|
});
|
||||||
|
setSockets(sockets => [...sockets, socket]);
|
||||||
|
socket.on('ready-for-run', () => readyForRunHandler(browserId, runId));
|
||||||
|
socket.on('debugMessage', debugMessageHandler);
|
||||||
|
setContent('runs');
|
||||||
|
if (browserId) {
|
||||||
|
notify('info', `Running recording: ${runningRecordingName}`);
|
||||||
|
} else {
|
||||||
|
notify('error', `Failed to run recording: ${runningRecordingName}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return (socket: Socket, browserId: string, runId: string) => {
|
||||||
|
socket.off('ready-for-run', () => readyForRunHandler(browserId, runId));
|
||||||
|
socket.off('debugMessage', debugMessageHandler);
|
||||||
|
}
|
||||||
|
}, [runningRecordingName, sockets, ids, readyForRunHandler, debugMessageHandler])
|
||||||
|
|
||||||
|
const DisplayContent = () => {
|
||||||
|
switch (content) {
|
||||||
|
case 'recordings':
|
||||||
|
return <Recordings
|
||||||
|
handleEditRecording={handleEditRecording}
|
||||||
|
handleRunRecording={handleRunRecording}
|
||||||
|
setFileName={setFileName}
|
||||||
|
/>;
|
||||||
|
case 'runs':
|
||||||
|
return <Runs
|
||||||
|
currentInterpretationLog={currentInterpretationLog}
|
||||||
|
abortRunHandler={() => abortRunHandler(ids.runId)}
|
||||||
|
runId={ids.runId}
|
||||||
|
runningRecordingName={runningRecordingName}
|
||||||
|
/>;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack direction='row' spacing={0} sx={{minHeight: '800px'}}>
|
||||||
|
<MainMenu value={content} handleChangeContent={setContent}/>
|
||||||
|
{ DisplayContent() }
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
75
src/pages/PageWrappper.tsx
Normal file
75
src/pages/PageWrappper.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { NavBar } from "../components/molecules/NavBar";
|
||||||
|
import { SocketProvider } from "../context/socket";
|
||||||
|
import { BrowserDimensionsProvider } from "../context/browserDimensions";
|
||||||
|
import { RecordingPage } from "./RecordingPage";
|
||||||
|
import { MainPage } from "./MainPage";
|
||||||
|
import { useGlobalInfoStore } from "../context/globalInfo";
|
||||||
|
import { getActiveBrowserId } from "../api/recording";
|
||||||
|
import { AlertSnackbar } from "../components/atoms/AlertSnackbar";
|
||||||
|
import { InterpretationLog } from "../components/molecules/InterpretationLog";
|
||||||
|
|
||||||
|
|
||||||
|
export const PageWrapper = () => {
|
||||||
|
|
||||||
|
const [recordingName, setRecordingName] = useState('');
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const { browserId, setBrowserId, notification } = useGlobalInfoStore();
|
||||||
|
|
||||||
|
const handleNewRecording = () => {
|
||||||
|
setBrowserId('new-recording');
|
||||||
|
setRecordingName('');
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEditRecording = (fileName: string) => {
|
||||||
|
setRecordingName(fileName);
|
||||||
|
setBrowserId('new-recording');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNotification = (): boolean=> {
|
||||||
|
if (notification.isOpen && !open){
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
return notification.isOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const isRecordingInProgress = async() => {
|
||||||
|
const id = await getActiveBrowserId();
|
||||||
|
if (id) {
|
||||||
|
setBrowserId(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isRecordingInProgress();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<SocketProvider>
|
||||||
|
<React.Fragment>
|
||||||
|
<NavBar newRecording={handleNewRecording} recordingName={recordingName} isRecording={!!browserId}/>
|
||||||
|
{browserId
|
||||||
|
? (
|
||||||
|
<BrowserDimensionsProvider>
|
||||||
|
<React.Fragment>
|
||||||
|
<RecordingPage recordingName={recordingName}/>
|
||||||
|
<InterpretationLog/>
|
||||||
|
</React.Fragment>
|
||||||
|
</BrowserDimensionsProvider>
|
||||||
|
)
|
||||||
|
: <MainPage
|
||||||
|
handleEditRecording={handleEditRecording}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</React.Fragment>
|
||||||
|
</SocketProvider>
|
||||||
|
{ isNotification() ?
|
||||||
|
<AlertSnackbar severity={notification.severity}
|
||||||
|
message={notification.message}
|
||||||
|
isOpen={notification.isOpen}/>
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user