feat: create recorder ui directory

This commit is contained in:
amhsirak
2025-01-09 20:09:46 +05:30
parent 403345b78e
commit 2d0d18d0b2
18 changed files with 10 additions and 10 deletions

View File

@@ -0,0 +1,134 @@
import { WhereWhatPair } from "maxun-core";
import { GenericModal } from "../ui/GenericModal";
import { modalStyle } from "./AddWhereCondModal";
import { Button, MenuItem, TextField, Typography } from "@mui/material";
import React, { useRef } from "react";
import { Dropdown as MuiDropdown } from "../ui/DropdownMui";
import { KeyValueForm } from "./KeyValueForm";
import { ClearButton } from "../ui/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>
)
}

View File

@@ -0,0 +1,152 @@
import { Dropdown as MuiDropdown } from "../ui/DropdownMui";
import {
Button,
MenuItem,
Typography
} from "@mui/material";
import React, { useRef } from "react";
import { GenericModal } from "../ui/GenericModal";
import { WhereWhatPair } from "maxun-core";
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',
};

View File

@@ -0,0 +1,126 @@
import React from "react";
import { Dropdown as MuiDropdown } from "../ui/DropdownMui";
import { Checkbox, FormControlLabel, FormGroup, MenuItem, Stack, TextField } from "@mui/material";
import { AddButton } from "../ui/buttons/AddButton";
import { RemoveButton } from "../ui/buttons/RemoveButton";
import { KeyValueForm } from "./KeyValueForm";
import { WarningText } from "../ui/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;
}
}

View File

@@ -0,0 +1,39 @@
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
import { KeyValuePair } from "./KeyValuePair";
import { AddButton } from "../ui/buttons/AddButton";
import { RemoveButton } from "../ui/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>
);
});

View File

@@ -0,0 +1,52 @@
import React, { forwardRef, useImperativeHandle } from "react";
import { Box, TextField } from "@mui/material";
interface KeyValueFormProps {
keyLabel?: string;
valueLabel?: string;
}
export const KeyValuePair = forwardRef(({ keyLabel, valueLabel }: KeyValueFormProps, ref) => {
const [key, setKey] = React.useState<string>('');
const [value, setValue] = React.useState<string | number>('');
useImperativeHandle(ref, () => ({
getKeyValuePair() {
return { key, value };
}
}));
return (
<Box
component="form"
sx={{
'& > :not(style)': { m: 1, width: '100px' },
}}
noValidate
autoComplete="off"
>
<TextField
id="outlined-name"
label={keyLabel || "Key"}
value={key}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setKey(event.target.value)}
size="small"
required
/>
<TextField
id="outlined-name"
label={valueLabel || "Value"}
value={value}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
const num = Number(event.target.value);
if (isNaN(num)) {
setValue(event.target.value);
}
else {
setValue(num);
}
}}
size="small"
required
/>
</Box>
);
});

View File

@@ -5,11 +5,11 @@ import { useSocketStore } from '../../context/socket';
import { WhereWhatPair, WorkflowFile } from "maxun-core";
import { SidePanelHeader } from "./SidePanelHeader";
import { emptyWorkflow } from "../../shared/constants";
import { LeftSidePanelContent } from "../molecules/LeftSidePanelContent";
import { LeftSidePanelContent } from "./LeftSidePanelContent";
import { useBrowserDimensionsStore } from "../../context/browserDimensions";
import { useGlobalInfoStore } from "../../context/globalInfo";
import { TabContext, TabPanel } from "@mui/lab";
import { LeftSidePanelSettings } from "../molecules/LeftSidePanelSettings";
import { LeftSidePanelSettings } from "./LeftSidePanelSettings";
import { RunSettings } from "../run/RunSettings";
const fetchWorkflow = (id: string, callback: (response: WorkflowFile) => void) => {

View 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 "maxun-core";
import { useSocketStore } from "../../context/socket";
import { Add } from "@mui/icons-material";
import { Socket } from "socket.io-client";
import { AddButton } from "../ui/buttons/AddButton";
import { AddPair } from "../../api/workflow";
import { GenericModal } from "../ui/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>
);
};

View File

@@ -0,0 +1,86 @@
import React from "react";
import { Button, MenuItem, TextField, Typography } from "@mui/material";
import { Dropdown } from "../ui/DropdownMui";
import { RunSettings } from "../run/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>
);
}

View File

@@ -0,0 +1,181 @@
import React, { FC, useState } from 'react';
import { Stack, Button, IconButton, Tooltip, Badge } from "@mui/material";
import { AddPair, deletePair, UpdatePair } from "../../api/workflow";
import { WorkflowFile } from "maxun-core";
import { ClearButton } from "../ui/buttons/ClearButton";
import { GenericModal } from "../ui/GenericModal";
import { PairEditForm } from "./PairEditForm";
import { PairDisplayDiv } from "./PairDisplayDiv";
import { EditButton } from "../ui/buttons/EditButton";
import { BreakpointButton } from "../ui/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'};
}
`;

View File

@@ -0,0 +1,309 @@
import React, { useLayoutEffect, useRef, useState } from 'react';
import { WhereWhatPair } from "maxun-core";
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 "../ui/buttons/AddButton";
import { WarningText } from "../ui/texts";
import NotificationImportantIcon from '@mui/icons-material/NotificationImportant';
import { RemoveButton } from "../ui/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>
);
}

View File

@@ -0,0 +1,42 @@
import React, { FC } from 'react';
import Typography from '@mui/material/Typography';
import { WhereWhatPair } from "maxun-core";
import styled from "styled-components";
interface PairDisplayDivProps {
index: string;
pair: WhereWhatPair;
}
export const PairDisplayDiv: FC<PairDisplayDivProps> = ({ index, pair }) => {
return (
<div>
<Typography sx={{ marginBottom: '10px', marginTop: '25px' }} id="pair-index" variant="h6" component="h2">
{`Index: ${index}`}
{pair.id ? `, Id: ${pair.id}` : ''}
</Typography>
<Typography id="where-title" variant="h6" component="h2">
{"Where:"}
</Typography>
<DescriptionWrapper id="where-description">
<pre>{JSON.stringify(pair?.where, undefined, 2)}</pre>
</DescriptionWrapper>
<Typography id="what-title" variant="h6" component="h2">
{"What:"}
</Typography>
<DescriptionWrapper id="what-description">
<pre>{JSON.stringify(pair?.what, undefined, 2)}</pre>
</DescriptionWrapper>
</div>
);
}
const DescriptionWrapper = styled.div`
margin: 0;
font-family: "Roboto","Helvetica","Arial",sans-serif;
font-weight: 400;
font-size: 1rem;
line-height: 1.5;
letter-spacing: 0.00938em;
`;

View File

@@ -0,0 +1,161 @@
import { Button, TextField, Typography } from "@mui/material";
import React, { FC } from "react";
import { Preprocessor, WhereWhatPair } from "maxun-core";
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>
);
};