From 558fe67fb87d3864448d83e7c5d35aa867501376 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Mon, 24 Jun 2024 22:31:52 +0530 Subject: [PATCH 01/27] feat: what condition modal --- src/components/molecules/AddWhatCondModal.tsx | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 src/components/molecules/AddWhatCondModal.tsx diff --git a/src/components/molecules/AddWhatCondModal.tsx b/src/components/molecules/AddWhatCondModal.tsx new file mode 100644 index 00000000..714c0fef --- /dev/null +++ b/src/components/molecules/AddWhatCondModal.tsx @@ -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(''); + const [objectIndex, setObjectIndex] = React.useState(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 ( + { + setArgs([]); + onClose(); + }} modalStyle={modalStyle}> +
+ Add what condition: +
+ Action: + setAction(e.target.value)} + value={action} + label='action' + /> +
+ Add new argument of type: + + + +
+ args: + {args.map((arg, index) => { + // @ts-ignore + return ( +
+ { + args.splice(index,1); + setArgs([...args]); + }}/> + {index}: + {arg.type === 'string' ? + 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' ? + setArgs([ + ...args.slice(0, index), + {type: arg.type, value: Number(e.target.value)}, + ...args.slice(index + 1) + ])} + value={args[index].value || ''} + label="number" + /> : + + //@ts-ignore + objectRefs.current[arg.value] = el} key={`arg-${arg.type}-${index}`}/> + } +
+ )})} + +
+
+
+ ) +} From ccba3730d9d43aadece01d0dfb770117d6717859 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Mon, 24 Jun 2024 22:32:11 +0530 Subject: [PATCH 02/27] feat: where condition modal --- .../molecules/AddWhereCondModal.tsx | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 src/components/molecules/AddWhereCondModal.tsx diff --git a/src/components/molecules/AddWhereCondModal.tsx b/src/components/molecules/AddWhereCondModal.tsx new file mode 100644 index 00000000..182a8dd6 --- /dev/null +++ b/src/components/molecules/AddWhereCondModal.tsx @@ -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(''); + const [additionalSettings, setAdditionalSettings] = React.useState(''); + const [newValue, setNewValue] = React.useState(''); + const [checked, setChecked] = React.useState(new Array(Object.keys(pair.where).length).fill(false)); + + const keyValueFormRef = useRef<{getObject: () => object}>(null); + + const {socket} = useSocketStore(); + + const handlePropSelect = (event: SelectChangeEvent) => { + 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 + 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 ( + { + setWhereProp(''); + setAdditionalSettings(''); + setNewValue(''); + onClose(); + }} modalStyle={modalStyle}> +
+ Add where condition: +
+ + url + selectors + cookies + before + after + boolean logic + +
+ {whereProp ? +
+ + +
+ : null} +
+
+ ) +} + +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', +}; From 105b8afb662883bd92041dad7b85f5bccf37b608 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Mon, 24 Jun 2024 22:32:34 +0530 Subject: [PATCH 03/27] feat: row collapse --- src/components/molecules/ColapsibleRow.tsx | 94 ++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 src/components/molecules/ColapsibleRow.tsx diff --git a/src/components/molecules/ColapsibleRow.tsx b/src/components/molecules/ColapsibleRow.tsx new file mode 100644 index 00000000..bec7c0cf --- /dev/null +++ b/src/components/molecules/ColapsibleRow.tsx @@ -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(null); + + const scrollToLogBottom = () => { + if (logEndRef.current) { + logEndRef.current.scrollIntoView({ behavior: "smooth" }); + } + } + + const handleAbort = () => { + abortRunHandler(); + } + + useEffect(() => { + scrollToLogBottom(); + }, [currentLog]) + + return ( + + *': { borderBottom: 'unset' } }} hover role="checkbox" tabIndex={-1} key={row.id}> + + { + setOpen(!open); + scrollToLogBottom(); + }} + > + {open ? : } + + + {columns.map((column) => { + // @ts-ignore + const value : any = row[column.id]; + if (value !== undefined) { + return ( + + {value} + + ); + } else { + switch (column.id) { + case 'delete': + return ( + + { + deleteRunFromStorage(`${row.name}_${row.runId}`).then((result: boolean) => { + if (result) { + handleDelete(); + } + }) + }} sx={{'&:hover': { color: '#1976d2', backgroundColor: 'transparent' }}}> + + + + ); + default: + return null; + } + } + })} + + + + + + + + + + ); +} From e84a3740af9eb4f9b9427fc7eebd38e876227b14 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Mon, 24 Jun 2024 22:33:01 +0530 Subject: [PATCH 04/27] feat: where condition settings --- .../DisplayWhereConditionSettings.tsx | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 src/components/molecules/DisplayWhereConditionSettings.tsx diff --git a/src/components/molecules/DisplayWhereConditionSettings.tsx b/src/components/molecules/DisplayWhereConditionSettings.tsx new file mode 100644 index 00000000..98d63459 --- /dev/null +++ b/src/components/molecules/DisplayWhereConditionSettings.tsx @@ -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 ( + + setAdditionalSettings(e.target.value)}> + string + regex + + { additionalSettings ? setNewValue(e.target.value)} + value={newValue} + /> : null} + + ) + case 'selectors': + return ( + + + { + newValue.map((selector: string, index: number) => { + return setNewValue([ + ...newValue.slice(0, index), + e.target.value, + ...newValue.slice(index + 1) + ])}/> + }) + } + + setNewValue([...newValue, ''])}/> + { + const arr = newValue; + arr.splice(-1); + setNewValue([...arr]); + }}/> + + ) + case 'cookies': + return + case 'before': + return setNewValue(e.target.value)} + /> + case 'after': + return setNewValue(e.target.value)} + /> + case 'boolean': + return ( + + setAdditionalSettings(e.target.value)}> + and + or + + + { + whereKeys.map((key: string, index: number) => { + return ( + setChecked([ + ...checked.slice(0, index), + !checked[index], + ...checked.slice(index + 1) + ])} + key={`checkbox-${key}-${index}`} + /> + } label={key} key={`control-label-form-${key}-${index}`}/> + ) + }) + } + + + Choose at least 2 where conditions. Nesting of boolean operators + is possible by adding more conditions. + + + ) + default: + return null; + } +} From 4c5a6b81241efb25cfd99f4b272089d47ca5dc18 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Mon, 24 Jun 2024 22:33:26 +0530 Subject: [PATCH 05/27] feat: interpretation button --- .../molecules/InterpretationButtons.tsx | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 src/components/molecules/InterpretationButtons.tsx diff --git a/src/components/molecules/InterpretationButtons.tsx b/src/components/molecules/InterpretationButtons.tsx new file mode 100644 index 00000000..bedb85af --- /dev/null +++ b/src/components/molecules/InterpretationButtons.tsx @@ -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); + 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 ( + + + Do you want to use the previously recorded selector + as a where condition for matching the action? + + + [previous action: {decisionModal.action}] +
{decisionModal.selector}
+
+
); + 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 ( + + + + Pause + + + + {info.isPaused ? 'Resume' : 'Start'} + + + + Stop + + {}} 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', + }}> +
+ + { + handleDescription() + } +
+ + +
+
+
+
+ ); +}; From 59d8f01631b30cbf8a7a2bc1bd399629d4e37696 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Mon, 24 Jun 2024 22:33:50 +0530 Subject: [PATCH 06/27] feat: toggle button --- src/components/molecules/ToggleButton.tsx | 61 +++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/components/molecules/ToggleButton.tsx diff --git a/src/components/molecules/ToggleButton.tsx b/src/components/molecules/ToggleButton.tsx new file mode 100644 index 00000000..a0917427 --- /dev/null +++ b/src/components/molecules/ToggleButton.tsx @@ -0,0 +1,61 @@ +import React, { FC } from "react"; +import styled from "styled-components"; + +interface ToggleButtonProps { + isChecked?: boolean; + onChange: () => void; +}; + +export const ToggleButton: FC = ({ isChecked = false, onChange }) => ( + + + + +); + +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; + } + } +`; From 6e9bc20b5269516d7bce2cd4d124cb245ca0aa50 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Mon, 24 Jun 2024 22:34:11 +0530 Subject: [PATCH 07/27] feat: key value form --- src/components/molecules/KeyValueForm.tsx | 39 +++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/components/molecules/KeyValueForm.tsx diff --git a/src/components/molecules/KeyValueForm.tsx b/src/components/molecules/KeyValueForm.tsx new file mode 100644 index 00000000..c6dd374a --- /dev/null +++ b/src/components/molecules/KeyValueForm.tsx @@ -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(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 ( +
+ { + new Array(numberOfPairs).fill(1).map((_, index) => { + return keyValuePairRefs.current[index] = el}/> + }) + } + setNumberOfPairs(numberOfPairs + 1)} hoverEffect={false}/> + setNumberOfPairs(numberOfPairs - 1)}/> +
+ ); +}); From a1e8cfe8552f38eb34a39f7e32785f5cc1ad0d93 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Mon, 24 Jun 2024 22:34:40 +0530 Subject: [PATCH 08/27] feat: interpretation log --- .../molecules/InterpretationLog.tsx | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 src/components/molecules/InterpretationLog.tsx diff --git a/src/components/molecules/InterpretationLog.tsx b/src/components/molecules/InterpretationLog.tsx new file mode 100644 index 00000000..18fed72b --- /dev/null +++ b/src/components/molecules/InterpretationLog.tsx @@ -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(false); + const [log, setLog] = useState(''); + + const logEndRef = useRef(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 ( +
+ + } + aria-controls="panel1bh-content" + id="panel1bh-header" + > + + Interpretation Log + + + +
+ + {log} + +
+
+ + +
+ ); +} From cca6e211fa326603b874ac5b6752db41acabdb70 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Mon, 24 Jun 2024 22:34:54 +0530 Subject: [PATCH 09/27] feat: navbar --- src/components/molecules/NavBar.tsx | 110 ++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 src/components/molecules/NavBar.tsx diff --git a/src/components/molecules/NavBar.tsx b/src/components/molecules/NavBar.tsx new file mode 100644 index 00000000..011277c1 --- /dev/null +++ b/src/components/molecules/NavBar.tsx @@ -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 ( + +
+ +
Browser Recorder
+
+
+ + {isRecording ? 'NEW' : 'RECORD'} + + { + recordingLength > 0 + ? + :null + } + { isRecording ? + : null } +
+ +
+ ); +}; + +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; +`; From ef1889b7f58c7b4ada36b5d77e1ee9486a92eb8f Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Mon, 24 Jun 2024 22:36:03 +0530 Subject: [PATCH 10/27] feat: pair edit form --- src/components/molecules/PairEditForm.tsx | 161 ++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 src/components/molecules/PairEditForm.tsx diff --git a/src/components/molecules/PairEditForm.tsx b/src/components/molecules/PairEditForm.tsx new file mode 100644 index 00000000..815acfdd --- /dev/null +++ b/src/components/molecules/PairEditForm.tsx @@ -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 = ( + { + onSubmitOfPair, + numberOfPairs, + index, + where, + what, + id, + }) => { + const [pairProps, setPairProps] = React.useState({ + where: where || null, + what: what || null, + index: index || "1", + id: id || '', + }); + const [errors, setErrors] = React.useState({ + where: null, + what: null, + index: '', + }); + + const handleInputChange = (event: React.ChangeEvent) => { + 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 ( +
+ Raw pair edit form: + + + + + + + ); +}; From 21ed09f32b728db57818643f4a82f6d93131e42f Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Mon, 24 Jun 2024 22:36:17 +0530 Subject: [PATCH 11/27] feat: pairs --- src/components/molecules/PairDetail.tsx | 310 ++++++++++++++++++++++++ 1 file changed, 310 insertions(+) create mode 100644 src/components/molecules/PairDetail.tsx diff --git a/src/components/molecules/PairDetail.tsx b/src/components/molecules/PairDetail.tsx new file mode 100644 index 00000000..9d37bb6d --- /dev/null +++ b/src/components/molecules/PairDetail.tsx @@ -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( + 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 { + 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 handleChangeValue(Number(e.target.value), where, keys)} + defaultValue={value} + key={`text-field-${keys.join('-')}-${where}`} + /> + case 'object': + if (value) { + if (Array.isArray(value)) { + return ( + + { + value.map((element, index) => { + return DisplayValueContent(element, [...keys, index], where); + }) + } + { + let prevValue:any = where ? pair?.where : pair?.what; + for (const key of keys) { + prevValue = prevValue[key]; + } + handleChangeValue([...prevValue, ''], where, keys); + setRerender(!rerender); + }} hoverEffect={false}/> + { + 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); + }}/> + + ) + } else { + return ( + } + defaultExpandIcon={} + sx={{ flexGrow: 1, overflowY: 'auto' }} + key={`tree-view-nested-${keys.join('-')}-${where}`} + > + { + Object.keys(value).map((key2, index) => + { + return ( + + { DisplayValueContent(value[key2], [...keys, key2], where) } + + ) + }) + } + + ) + } + } + break; + default: + return null; + } + } + + return ( + + { pair && + + setAddWhatCondOpen(false)} + pair={pair} index={index}/> + setAddWhereCondOpen(false)} + pair={pair} index={index}/> + + } + { + pairIsSelected + ? ( +
+ Pair number: {index} + { + if (pair && socket) { + socket.emit('updatePair', {index: index-1, pair: pair}); + pair.id = e.target.value; + } + }} + value={pair ? pair.id ? pair.id : '' : ''} + /> + + + Where + +
+ { + setAddWhereCondOpen(true); + }} style={{color:'rgba(0, 0, 0, 0.54)', background:'transparent'}}/> +
+
+
+ {(collapseWhere && pair && pair.where) + ? + + { Object.keys(pair.where).map((key, index) => { + return ( + } + defaultExpandIcon={} + sx={{ flexGrow: 1, overflowY: 'auto' }} + onNodeToggle={handleToggle} + key={`tree-view-${key}-${index}`} + > + + { + // @ts-ignore + DisplayValueContent(pair.where[key], [key]) + } + + + ); + })} + + : null + } + + + What + + +
+ { + setAddWhatCondOpen(true); + }} style={{color:'rgba(0, 0, 0, 0.54)', background:'transparent'}}/> +
+
+
+ {(collapseWhat && pair && pair.what) + ?( + + { Object.keys(pair.what).map((key, index) => { + return ( + } + defaultExpandIcon={} + sx={{ flexGrow: 1, overflowY: 'auto' }} + key={`tree-view-2-${key}-${index}`} + > + + { + // @ts-ignore + DisplayValueContent(pair.what[key], [key], false) + } + +
+ { + //@ts-ignore + pair.what.splice(key, 1); + setRerender(!rerender); + }}/> +
+
+
+
+ ); + })} +
+ ) + : null + } +
+ ) + : + + No pair from the left side panel was selected. + + } +
+ ); +} + +interface CollapseButtonProps { + handleClick: () => void; + isCollapsed?: boolean; +} + +const CollapseButton = ({handleClick, isCollapsed } : CollapseButtonProps) => { + return ( + + { isCollapsed ? : } + + ); +} + +const CloseButton = ({handleClick } : CollapseButtonProps) => { + return ( + + + + ); +} From 62583fbe1c564f18eeaa11bb7e106ed981145d17 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Mon, 24 Jun 2024 22:36:35 +0530 Subject: [PATCH 12/27] feat: display pair --- src/components/molecules/Pair.tsx | 181 ++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 src/components/molecules/Pair.tsx diff --git a/src/components/molecules/Pair.tsx b/src/components/molecules/Pair.tsx new file mode 100644 index 00000000..7f501719 --- /dev/null +++ b/src/components/molecules/Pair.tsx @@ -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 = ( + { + 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 ( + + +
+ {isActive ? + : breakpoint ? + : + } +
+ + + + + +
+ +
+
+ + +
+ { + enableEdit(); + handleOpen(); + }} + /> +
+
+ +
+ +
+
+
+
+ + { edit + ? + + : +
+ +
+ } +
+
+ ); +}; + +interface ViewButtonProps { + handleClick: () => void; +} + +const ViewButton = ({handleClick}: ViewButtonProps) => { + return ( + + + + ); +} + + +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' }; + } +`; From fc6f5c2bfe0ff714a60b496ccd5c4d848d9743e8 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Mon, 24 Jun 2024 22:36:52 +0530 Subject: [PATCH 13/27] feat: save recording --- src/components/molecules/SaveRecording.tsx | 125 +++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 src/components/molecules/SaveRecording.tsx diff --git a/src/components/molecules/SaveRecording.tsx b/src/components/molecules/SaveRecording.tsx new file mode 100644 index 00000000..b4e175bb --- /dev/null +++ b/src/components/molecules/SaveRecording.tsx @@ -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(false); + const [needConfirm, setNeedConfirm] = useState(false); + const [recordingName, setRecordingName] = useState(fileName); + const [waitingForSave, setWaitingForSave] = useState(false); + + const { browserId, setBrowserId, notify, recordings } = useGlobalInfoStore(); + const { socket } = useSocketStore(); + + const handleChangeOfTitle = (event: React.ChangeEvent) => { + 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 ( +
+ + + setOpenModal(false)} modalStyle={modalStyle}> +
+ Save the recording as: + + { needConfirm + ? + ( + + + + Recording already exists, please confirm the recording's overwrite. + + ) + : + } + { waitingForSave && + + + + + + } + +
+
+ ); +} + +const modalStyle = { + top: '25%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: '20%', + backgroundColor: 'background.paper', + p: 4, + height:'fit-content', + display:'block', + padding: '20px', +}; From 5fbc1beaaf3bb587dcd8eb087bfe95f7d611d976 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Mon, 24 Jun 2024 22:37:49 +0530 Subject: [PATCH 14/27] feat: recordings --- src/components/molecules/RecordingsTable.tsx | 220 +++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 src/components/molecules/RecordingsTable.tsx diff --git a/src/components/molecules/RecordingsTable.tsx b/src/components/molecules/RecordingsTable.tsx new file mode 100644 index 00000000..df389a41 --- /dev/null +++ b/src/components/molecules/RecordingsTable.tsx @@ -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([]); + + const { notify, setRecordings } = useGlobalInfoStore(); + + const handleChangePage = (event: unknown, newPage: number) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + 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 ( + + + + + + {columns.map((column) => ( + + {column.label} + + ))} + + + + {rows.length !== 0 ? rows + .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + .map((row) => { + return ( + + {columns.map((column) => { + // @ts-ignore + const value : any = row[column.id]; + if (value !== undefined) { + return ( + + {value} + + ); + } else { + switch (column.id) { + case 'interpret': + return ( + + handleRunRecording(row.name, row.params || [])}/> + + ); + case 'edit': + return ( + + { + handleEditRecording(row.name); + }} sx={{'&:hover': { color: '#1976d2', backgroundColor: 'transparent' }}}> + + + + ); + case 'delete': + return ( + + { + deleteRecordingFromStorage(row.name).then((result: boolean) => { + if (result) { + setRows([]); + notify('success', 'Recording deleted successfully'); + fetchRecordings(); + } + }) + }} sx={{'&:hover': { color: '#1976d2', backgroundColor: 'transparent' }}}> + + + + ); + default: + return null; + } + } + })} + + ); + }) + : null } + +
+
+ +
+ ); +} + +interface InterpretButtonProps { + handleInterpret: () => void; +} + +const InterpretButton = ( {handleInterpret}:InterpretButtonProps) => { + return ( + { + handleInterpret(); + }} + sx={{'&:hover': { color: '#1976d2', backgroundColor: 'transparent' }}}> + + + ) +} From 0a63e802a9f163ac39823c1adc4d4b815ecb5a04 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Mon, 24 Jun 2024 22:38:29 +0530 Subject: [PATCH 15/27] feat: run content --- src/components/molecules/RunContent.tsx | 170 ++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 src/components/molecules/RunContent.tsx diff --git a/src/components/molecules/RunContent.tsx b/src/components/molecules/RunContent.tsx new file mode 100644 index 00000000..8c493faf --- /dev/null +++ b/src/components/molecules/RunContent.tsx @@ -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, + abortRunHandler: () => void, +} + +export const RunContent = ({row, currentLog, interpretationInProgress, logEndRef, abortRunHandler}: RunContentProps) => { + const [tab, setTab] = React.useState('log'); + + useEffect(() => { + setTab(tab); + }, [interpretationInProgress]) + + return ( + + + + setTab(newTab)} aria-label="run-content-tabs"> + + + + + + + +
+ + {interpretationInProgress ? currentLog : row.log} + +
+
+ + {interpretationInProgress ? : null} + + + + + Interpreter settings + + { + Object.keys(row.interpreterSettings).map((setting, index) => { + if (setting === 'params') { + return ( +
+ + + Recording parameters + + { + Object.keys(row.interpreterSettings.params).map((param, index) => { + return ( + + {/*@ts-ignore*/} + {param}: {row.interpreterSettings.params[param].toString()} + + ) + }) + } +
+ ) + } + return ( + + {/*@ts-ignore*/} + {setting}: {row.interpreterSettings[setting].toString()} + + ) + }) + } +
+ + { !row || !row.serializableOutput || !row.binaryOutput + || (Object.keys(row.serializableOutput).length === 0 && Object.keys(row.binaryOutput).length === 0) + ? The output is empty. : null } + + {row.serializableOutput && + Object.keys(row.serializableOutput).length !== 0 && +
+ + + Serializable output + { Object.keys(row.serializableOutput).map((key) => { + return ( +
+ + {key}: + Download + + +
+                    {row.serializableOutput[key] ? JSON.stringify(row.serializableOutput[key], null, 2)
+                    : 'The output is empty.'}
+                  
+
+
+ ) + })} +
+ } + {row.binaryOutput + && Object.keys(row.binaryOutput).length !== 0 && +
+ + + Binary output + { Object.keys(row.binaryOutput).map((key) => { + try { + const binaryBuffer = JSON.parse(row.binaryOutput[key].data); + const b64 = Buffer.from(binaryBuffer.data).toString('base64'); + return ( + + + {key}: + Download + + {key} + + ) + } catch (e) { + console.log(e) + return + {key}: The image failed to render + + } + })} +
+ } +
+ + + ); +} From 0bcaaa565bf51b954eff9cea639ce1afd24b9d17 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Mon, 24 Jun 2024 22:38:44 +0530 Subject: [PATCH 16/27] feat: run settings --- src/components/molecules/RunSettings.tsx | 114 +++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 src/components/molecules/RunSettings.tsx diff --git a/src/components/molecules/RunSettings.tsx b/src/components/molecules/RunSettings.tsx new file mode 100644 index 00000000..45a35c24 --- /dev/null +++ b/src/components/molecules/RunSettings.tsx @@ -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({ + maxConcurrency: 1, + maxRepeats: 1, + debug: true, + }); + + return ( + +
+ { isTask + ? + ( + + Recording parameters: + { params?.map((item, index) => { + return setSettings( + { + ...settings, + params: settings.params + ? { + ...settings.params, + [item]: e.target.value, + } + : { + [item]: e.target.value, + }, + })} + /> + }) } + ) + : null + } + Interpreter settings: + setSettings( + { + ...settings, + maxConcurrency: parseInt(e.target.value), + })} + defaultValue={settings.maxConcurrency} + /> + setSettings( + { + ...settings, + maxRepeats: parseInt(e.target.value), + })} + defaultValue={settings.maxRepeats} + /> + setSettings( + { + ...settings, + debug: e.target.value === "true", + })} + > + true + false + + +
+
+ ); +} From b76d4634d77b432a0fd95efe89a6d2fccf1df502 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Mon, 24 Jun 2024 22:38:57 +0530 Subject: [PATCH 17/27] feat: runs table --- src/components/molecules/RunsTable.tsx | 153 +++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 src/components/molecules/RunsTable.tsx diff --git a/src/components/molecules/RunsTable.tsx b/src/components/molecules/RunsTable.tsx new file mode 100644 index 00000000..d6489cff --- /dev/null +++ b/src/components/molecules/RunsTable.tsx @@ -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([]); + + const { notify, rerenderRuns, setRerenderRuns } = useGlobalInfoStore(); + + const handleChangePage = (event: unknown, newPage: number) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + 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 ( + + + + + + + {columns.map((column) => ( + + {column.label} + + ))} + + + + {rows.length !== 0 ? rows + .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + .map((row, index) => + + ) + : null } + +
+
+ +
+ ); +} From e61d061070ff3411bf1cb803b28d2cae63556327 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Mon, 24 Jun 2024 22:39:17 +0530 Subject: [PATCH 18/27] feat: left panel --- .../molecules/LeftSidePanelContent.tsx | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 src/components/molecules/LeftSidePanelContent.tsx diff --git a/src/components/molecules/LeftSidePanelContent.tsx b/src/components/molecules/LeftSidePanelContent.tsx new file mode 100644 index 00000000..5c19996e --- /dev/null +++ b/src/components/molecules/LeftSidePanelContent.tsx @@ -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(0); + const [breakpoints, setBreakpoints] = React.useState([]); + 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 ( +
+ +
+ +
+
+ setShowEditModal(false)} + > + + +
+ { + workflow.workflow.map((pair, i, workflow, ) => + handleBreakpointClick(i)} + isActive={ activeId === i + 1} + key={workflow.length - i} + index={workflow.length - i} + pair={pair} + updateWorkflow={updateWorkflow} + numberOfPairs={workflow.length} + handleSelectPairForEdit={handleSelectPairForEdit} + />) + } +
+
+ ); +}; From c54529dc1b5cde100c9a85e4d54432cb0fe9ae06 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Mon, 24 Jun 2024 22:39:37 +0530 Subject: [PATCH 19/27] feat: left panel settings --- .../molecules/LeftSidePanelSettings.tsx | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/components/molecules/LeftSidePanelSettings.tsx diff --git a/src/components/molecules/LeftSidePanelSettings.tsx b/src/components/molecules/LeftSidePanelSettings.tsx new file mode 100644 index 00000000..a25ff2d1 --- /dev/null +++ b/src/components/molecules/LeftSidePanelSettings.tsx @@ -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 ( +
+ { params.length !== 0 && ( + + Parameters: + { params?.map((item: string, index: number) => { + return setSettings( + { + ...settings, + params: settings.params + ? { + ...settings.params, + [item]: e.target.value, + } + : { + [item]: e.target.value, + }, + })} + /> + }) } + + )} + Interpreter: + setSettings( + { + ...settings, + maxConcurrency: parseInt(e.target.value), + })} + defaultValue={settings.maxConcurrency} + /> + setSettings( + { + ...settings, + maxRepeats: parseInt(e.target.value), + })} + defaultValue={settings.maxRepeats} + /> + setSettings( + { + ...settings, + debug: e.target.value === "true", + })} + > + true + false + + +
+ ); +} From afdd331578179b34f3fc889a8455f9924ee5414d Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Mon, 24 Jun 2024 22:39:54 +0530 Subject: [PATCH 20/27] feat: side panel header --- src/components/molecules/SidePanelHeader.tsx | 38 ++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/components/molecules/SidePanelHeader.tsx diff --git a/src/components/molecules/SidePanelHeader.tsx b/src/components/molecules/SidePanelHeader.tsx new file mode 100644 index 00000000..7743230e --- /dev/null +++ b/src/components/molecules/SidePanelHeader.tsx @@ -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 ( +
+ setSteppingIsDisabled(!isPaused)}/> + +
+
+ ); +}; From eec2b7bc8b1de6f4e8162bd236c2b2d66132c0af Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Mon, 24 Jun 2024 22:41:33 +0530 Subject: [PATCH 21/27] feat: runs --- src/components/organisms/Runs.tsx | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/components/organisms/Runs.tsx diff --git a/src/components/organisms/Runs.tsx b/src/components/organisms/Runs.tsx new file mode 100644 index 00000000..e3927fd4 --- /dev/null +++ b/src/components/organisms/Runs.tsx @@ -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 ( + + + + + + ); +} From 02ec06505dabd7f215327c098da3e5624b4f2782 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Mon, 24 Jun 2024 22:41:54 +0530 Subject: [PATCH 22/27] feat: recordings --- src/components/organisms/Recordings.tsx | 52 +++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/components/organisms/Recordings.tsx diff --git a/src/components/organisms/Recordings.tsx b/src/components/organisms/Recordings.tsx new file mode 100644 index 00000000..ae993956 --- /dev/null +++ b/src/components/organisms/Recordings.tsx @@ -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([]); + + 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 ( + + handleRunRecording(settings) } + isTask={params.length !== 0} + params={params} + /> + + + + + + + ); +} From 3b281865d7881a63e16296b84d1e4f4aece9a86e Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Mon, 24 Jun 2024 22:42:07 +0530 Subject: [PATCH 23/27] feat: left panel --- src/components/organisms/LeftSidePanel.tsx | 135 +++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 src/components/organisms/LeftSidePanel.tsx diff --git a/src/components/organisms/LeftSidePanel.tsx b/src/components/organisms/LeftSidePanel.tsx new file mode 100644 index 00000000..3319e536 --- /dev/null +++ b/src/components/organisms/LeftSidePanel.tsx @@ -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(emptyWorkflow); + const [hasScrollbar, setHasScrollbar] = useState(alreadyHasScrollbar); + const [tab, setTab] = useState('recording'); + const [params, setParams] = useState([]); + const [settings, setSettings] = React.useState({ + 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 ( + + + + setTab(newTab)}> + + { + getParamsOfActiveWorkflow(id).then((response) => { + if (response) { + setParams(response); + } + }) + }}/> + + + + + + + + + + ); + +}; From 517172175e24d451cecbb84116c8473efbc71e8b Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Mon, 24 Jun 2024 22:42:22 +0530 Subject: [PATCH 24/27] feat: right panel --- src/components/organisms/RightSidePanel.tsx | 106 ++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 src/components/organisms/RightSidePanel.tsx diff --git a/src/components/organisms/RightSidePanel.tsx b/src/components/organisms/RightSidePanel.tsx new file mode 100644 index 00000000..3ad8dff5 --- /dev/null +++ b/src/components/organisms/RightSidePanel.tsx @@ -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('action'); + const [action, setAction] = React.useState(''); + const [isSettingsDisplayed, setIsSettingsDisplayed] = React.useState(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 ( + + + + + Last action: + {` ${lastAction}`} + + + + + + + + + {content === 'action' ? ( + + Type of action: + + + click on coordinates + enqueueLinks + scrape + scrapeSchema + screenshot + script + scroll + + + + {isSettingsDisplayed && + + } + + ) + : + } + + ); +}; + +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; +`; From 1a126da38cad236e7dbe6f5b4ad20fa10870c5c1 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Mon, 24 Jun 2024 22:42:37 +0530 Subject: [PATCH 25/27] feat: main menu --- src/components/organisms/MainMenu.tsx | 51 +++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/components/organisms/MainMenu.tsx diff --git a/src/components/organisms/MainMenu.tsx b/src/components/organisms/MainMenu.tsx new file mode 100644 index 00000000..55dcd27c --- /dev/null +++ b/src/components/organisms/MainMenu.tsx @@ -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 ( + + + + + + + + + ); +} From a3ca99bc80241ec76910d0e6593bede12d9c7bd4 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Mon, 24 Jun 2024 22:43:07 +0530 Subject: [PATCH 26/27] feat: main page --- src/pages/MainPage.tsx | 124 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 src/pages/MainPage.tsx diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx new file mode 100644 index 00000000..d358b1d3 --- /dev/null +++ b/src/pages/MainPage.tsx @@ -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([]); + const [runningRecordingName, setRunningRecordingName] = React.useState(''); + const [currentInterpretationLog, setCurrentInterpretationLog] = React.useState(''); + const [ids, setIds] = React.useState({ + 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 ; + case 'runs': + return abortRunHandler(ids.runId)} + runId={ids.runId} + runningRecordingName={runningRecordingName} + />; + default: + return null; + } + } + + return ( + + + { DisplayContent() } + + ); +}; From 0b924be209a837ae615e07b29945db94c3eb9c22 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Mon, 24 Jun 2024 22:43:51 +0530 Subject: [PATCH 27/27] feat: page wrapper --- src/pages/PageWrappper.tsx | 75 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/pages/PageWrappper.tsx diff --git a/src/pages/PageWrappper.tsx b/src/pages/PageWrappper.tsx new file mode 100644 index 00000000..00e83a39 --- /dev/null +++ b/src/pages/PageWrappper.tsx @@ -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 ( +
+ + + + {browserId + ? ( + + + + + + + ) + : + } + + + { isNotification() ? + + : null + } +
+ ); +}