Merge branch 'develop' into integration_airtable
This commit is contained in:
@@ -14,7 +14,7 @@ import {
|
||||
Tooltip,
|
||||
Paper,
|
||||
} from '@mui/material';
|
||||
import { ContentCopy, Visibility, Delete } from '@mui/icons-material';
|
||||
import { ContentCopy, Visibility, VisibilityOff, Delete } from '@mui/icons-material';
|
||||
import styled from 'styled-components';
|
||||
import axios from 'axios';
|
||||
import { useGlobalInfoStore } from '../../context/globalInfo';
|
||||
@@ -137,7 +137,7 @@ const ApiKeyManager = () => {
|
||||
</Tooltip>
|
||||
<Tooltip title={showKey ? t('apikey.actions.hide') : t('apikey.actions.show')}>
|
||||
<IconButton onClick={() => setShowKey(!showKey)}>
|
||||
<Visibility />
|
||||
{showKey ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('apikey.actions.delete')}>
|
||||
|
||||
@@ -263,7 +263,12 @@ export const BrowserWindow = () => {
|
||||
}
|
||||
|
||||
if (getList === true && !listSelector) {
|
||||
setListSelector(highlighterData.selector);
|
||||
let cleanedSelector = highlighterData.selector;
|
||||
if (cleanedSelector.includes('nth-child')) {
|
||||
cleanedSelector = cleanedSelector.replace(/:nth-child\(\d+\)/g, '');
|
||||
}
|
||||
|
||||
setListSelector(cleanedSelector);
|
||||
notify(`info`, t('browser_window.attribute_modal.notifications.list_select_success'));
|
||||
setCurrentListId(Date.now());
|
||||
setFields({});
|
||||
@@ -275,13 +280,25 @@ export const BrowserWindow = () => {
|
||||
// Add fields to the list
|
||||
if (options.length === 1) {
|
||||
const attribute = options[0].value;
|
||||
let currentSelector = highlighterData.selector;
|
||||
|
||||
if (currentSelector.includes('>')) {
|
||||
const [firstPart, ...restParts] = currentSelector.split('>').map(p => p.trim());
|
||||
const listSelectorRightPart = listSelector.split('>').pop()?.trim().replace(/:nth-child\(\d+\)/g, '');
|
||||
|
||||
if (firstPart.includes('nth-child') &&
|
||||
firstPart.replace(/:nth-child\(\d+\)/g, '') === listSelectorRightPart) {
|
||||
currentSelector = `${firstPart.replace(/:nth-child\(\d+\)/g, '')} > ${restParts.join(' > ')}`;
|
||||
}
|
||||
}
|
||||
|
||||
const newField: TextStep = {
|
||||
id: Date.now(),
|
||||
type: 'text',
|
||||
label: `Label ${Object.keys(fields).length + 1}`,
|
||||
data: data,
|
||||
selectorObj: {
|
||||
selector: highlighterData.selector,
|
||||
selector: currentSelector,
|
||||
tag: highlighterData.elementInfo?.tagName,
|
||||
shadow: highlighterData.elementInfo?.isShadowRoot,
|
||||
attribute
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
Clear,
|
||||
YouTube,
|
||||
X,
|
||||
GitHub,
|
||||
Update,
|
||||
Close,
|
||||
Language,
|
||||
@@ -107,11 +108,39 @@ export const NavBar: React.FC<NavBarProps> = ({
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
dispatch({ type: "LOGOUT" });
|
||||
window.localStorage.removeItem("user");
|
||||
const { data } = await axios.get(`${apiUrl}/auth/logout`);
|
||||
notify("success", data.message);
|
||||
navigate("/login");
|
||||
try {
|
||||
const { data } = await axios.get(`${apiUrl}/auth/logout`);
|
||||
if (data.ok) {
|
||||
dispatch({ type: "LOGOUT" });
|
||||
window.localStorage.removeItem("user");
|
||||
notify('success', t('navbar.notifications.success.logout'));
|
||||
navigate("/login");
|
||||
}
|
||||
} catch (error: any) {
|
||||
const status = error.response?.status;
|
||||
let errorKey = 'unknown';
|
||||
|
||||
switch (status) {
|
||||
case 401:
|
||||
errorKey = 'unauthorized';
|
||||
break;
|
||||
case 500:
|
||||
errorKey = 'server';
|
||||
break;
|
||||
default:
|
||||
if (error.message?.includes('Network Error')) {
|
||||
errorKey = 'network';
|
||||
}
|
||||
}
|
||||
|
||||
notify(
|
||||
'error',
|
||||
t(`navbar.notifications.errors.logout.${errorKey}`, {
|
||||
error: error.response?.data?.message || error.message
|
||||
})
|
||||
);
|
||||
navigate("/login");
|
||||
}
|
||||
};
|
||||
|
||||
const goToMainMenu = async () => {
|
||||
@@ -204,7 +233,9 @@ export const NavBar: React.FC<NavBarProps> = ({
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
}}>
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => navigate('/')}>
|
||||
<img src={MaxunLogo} width={45} height={40} style={{ borderRadius: '5px', margin: '5px 0px 5px 15px' }} />
|
||||
<div style={{ padding: '11px' }}><ProjectName mode={darkMode ? 'dark' : 'light'}>{t('navbar.project_name')}</ProjectName></div>
|
||||
<Chip
|
||||
@@ -219,14 +250,16 @@ export const NavBar: React.FC<NavBarProps> = ({
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
|
||||
{!isRecording ? (
|
||||
<>
|
||||
<Button variant="outlined" onClick={handleUpdateOpen} sx={{
|
||||
marginRight: '40px',
|
||||
color: "#00000099",
|
||||
border: "#00000099 1px solid",
|
||||
'&:hover': { color: '#ff00c3', border: '#ff00c3 1px solid' }
|
||||
<IconButton onClick={handleUpdateOpen} sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderRadius: '5px',
|
||||
padding: '8px',
|
||||
marginRight: '20px',
|
||||
}}>
|
||||
<Update sx={{ marginRight: '5px' }} /> {t('navbar.upgrade.button')}
|
||||
</Button>
|
||||
<Update sx={{ marginRight: '5px' }} />
|
||||
<Typography variant="body1">{t('navbar.upgrade.button')}</Typography>
|
||||
</IconButton>
|
||||
<Modal open={open} onClose={handleUpdateClose}>
|
||||
<Box
|
||||
sx={{
|
||||
@@ -318,7 +351,14 @@ export const NavBar: React.FC<NavBarProps> = ({
|
||||
)}
|
||||
</Box>
|
||||
</Modal>
|
||||
<iframe src="https://ghbtns.com/github-btn.html?user=getmaxun&repo=maxun&type=star&count=true&size=large" frameBorder="0" scrolling="0" width="170" height="30" title="GitHub"></iframe>
|
||||
{/* <iframe
|
||||
src="https://ghbtns.com/github-btn.html?user=getmaxun&repo=maxun&type=star&count=true&size=large"
|
||||
// frameBorder="0"
|
||||
// scrolling="0"
|
||||
// width="170"
|
||||
// height="30"
|
||||
// title="GitHub">
|
||||
// </iframe>*/}
|
||||
<IconButton onClick={handleMenuOpen} sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
@@ -335,22 +375,31 @@ export const NavBar: React.FC<NavBarProps> = ({
|
||||
onClose={handleMenuClose}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
PaperProps={{ sx: { width: '180px' } }}
|
||||
>
|
||||
<MenuItem onClick={() => { handleMenuClose(); logout(); }}>
|
||||
<Logout sx={{ marginRight: '5px' }} /> {t('navbar.menu_items.logout')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleLangMenuOpen}>
|
||||
<Language sx={{ marginRight: '5px' }} /> {t('navbar.menu_items.language')}
|
||||
</MenuItem>
|
||||
<hr />
|
||||
<MenuItem onClick={() => {
|
||||
window.open('https://docs.maxun.dev', '_blank');
|
||||
}}>
|
||||
<Description sx={{ marginRight: '5px' }} /> Docs
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => {
|
||||
window.open('https://github.com/getmaxun/maxun', '_blank');
|
||||
}}>
|
||||
<GitHub sx={{ marginRight: '5px' }} /> GitHub
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => {
|
||||
window.open('https://discord.gg/5GbPjBUkws', '_blank');
|
||||
}}>
|
||||
@@ -366,20 +415,17 @@ export const NavBar: React.FC<NavBarProps> = ({
|
||||
}}>
|
||||
<X sx={{ marginRight: '5px' }} /> Twitter (X)
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleLangMenuOpen}>
|
||||
<Language sx={{ marginRight: '5px' }} /> {t('navbar.menu_items.language')}
|
||||
</MenuItem>
|
||||
<Menu
|
||||
anchorEl={langAnchorEl}
|
||||
open={Boolean(langAnchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "right",
|
||||
horizontal: "center",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
horizontal: "center",
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
@@ -471,11 +517,11 @@ export const NavBar: React.FC<NavBarProps> = ({
|
||||
onClose={handleMenuClose}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "right",
|
||||
horizontal: "center",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
horizontal: "center",
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
|
||||
@@ -148,7 +148,6 @@ export const RecordingsTable = ({
|
||||
const [rows, setRows] = React.useState<Data[]>([]);
|
||||
const [isModalOpen, setModalOpen] = React.useState(false);
|
||||
const [searchTerm, setSearchTerm] = React.useState('');
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
|
||||
const columns = useMemo(() => [
|
||||
{ id: 'interpret', label: t('recordingtable.run'), minWidth: 80 },
|
||||
@@ -169,6 +168,8 @@ export const RecordingsTable = ({
|
||||
setRecordingUrl,
|
||||
isLogin,
|
||||
setIsLogin,
|
||||
rerenderRobots,
|
||||
setRerenderRobots,
|
||||
recordingName,
|
||||
setRecordingName,
|
||||
recordingId,
|
||||
@@ -189,32 +190,45 @@ export const RecordingsTable = ({
|
||||
setPage(0);
|
||||
}, []);
|
||||
|
||||
const parseDateString = (dateStr: string): Date => {
|
||||
try {
|
||||
if (dateStr.includes('PM') || dateStr.includes('AM')) {
|
||||
return new Date(dateStr);
|
||||
}
|
||||
|
||||
return new Date(dateStr.replace(/(\d+)\/(\d+)\//, '$2/$1/'))
|
||||
} catch {
|
||||
return new Date(0);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchRecordings = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const recordings = await getStoredRecordings();
|
||||
if (recordings) {
|
||||
const parsedRows = recordings
|
||||
.map((recording: any, index: number) => {
|
||||
if (recording?.recording_meta) {
|
||||
return {
|
||||
id: index,
|
||||
...recording.recording_meta,
|
||||
content: recording.recording
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
.map((recording: any, index: number) => {
|
||||
if (recording?.recording_meta) {
|
||||
const parsedDate = parseDateString(recording.recording_meta.createdAt);
|
||||
|
||||
return {
|
||||
id: index,
|
||||
...recording.recording_meta,
|
||||
content: recording.recording,
|
||||
parsedDate
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => b.parsedDate.getTime() - a.parsedDate.getTime());
|
||||
|
||||
setRecordings(parsedRows.map((recording) => recording.name));
|
||||
setRows(parsedRows);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching recordings:', error);
|
||||
notify('error', t('recordingtable.notifications.fetch_error'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [setRecordings, notify, t]);
|
||||
|
||||
@@ -249,6 +263,14 @@ export const RecordingsTable = ({
|
||||
}
|
||||
}, [fetchRecordings]);
|
||||
|
||||
useEffect(() => {
|
||||
if (rerenderRobots) {
|
||||
fetchRecordings().then(() => {
|
||||
setRerenderRobots(false);
|
||||
});
|
||||
}
|
||||
}, [rerenderRobots, fetchRecordings, setRerenderRobots]);
|
||||
|
||||
function useDebounce<T>(value: T, delay: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = React.useState<T>(value);
|
||||
|
||||
@@ -343,39 +365,32 @@ export const RecordingsTable = ({
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
{isLoading ? (
|
||||
<Box display="flex" justifyContent="center" alignItems="center" height="50%">
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : (
|
||||
<TableContainer component={Paper} sx={{ width: '100%', overflow: 'hidden', marginTop: '15px' }}>
|
||||
<Table stickyHeader aria-label="sticky table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columns.map((column) => (
|
||||
<MemoizedTableCell
|
||||
key={column.id}
|
||||
// align={column.align}
|
||||
style={{ minWidth: column.minWidth }}
|
||||
>
|
||||
{column.label}
|
||||
</MemoizedTableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{visibleRows.map((row) => (
|
||||
<TableRowMemoized
|
||||
key={row.id}
|
||||
row={row}
|
||||
columns={columns}
|
||||
handlers={handlers}
|
||||
/>
|
||||
<TableContainer component={Paper} sx={{ width: '100%', overflow: 'hidden', marginTop: '15px' }}>
|
||||
<Table stickyHeader aria-label="sticky table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columns.map((column) => (
|
||||
<MemoizedTableCell
|
||||
key={column.id}
|
||||
style={{ minWidth: column.minWidth }}
|
||||
>
|
||||
{column.label}
|
||||
</MemoizedTableCell>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{visibleRows.map((row) => (
|
||||
<TableRowMemoized
|
||||
key={row.id}
|
||||
row={row}
|
||||
columns={columns}
|
||||
handlers={handlers}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[10, 25, 50, 100]}
|
||||
|
||||
@@ -55,9 +55,9 @@ interface RobotSettingsProps {
|
||||
|
||||
export const RobotDuplicationModal = ({ isOpen, handleStart, handleClose, initialSettings }: RobotSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [robot, setRobot] = useState<RobotSettings | null>(null);
|
||||
const [targetUrl, setTargetUrl] = useState<string | undefined>('');
|
||||
const { recordingId, notify } = useGlobalInfoStore();
|
||||
const [robot, setRobot] = useState<RobotSettings | null>(null);
|
||||
const { recordingId, notify, setRerenderRobots } = useGlobalInfoStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
@@ -96,13 +96,11 @@ export const RobotDuplicationModal = ({ isOpen, handleStart, handleClose, initia
|
||||
const success = await duplicateRecording(robot.recording_meta.id, targetUrl);
|
||||
|
||||
if (success) {
|
||||
setRerenderRobots(true);
|
||||
|
||||
notify('success', t('robot_duplication.notifications.duplicate_success'));
|
||||
handleStart(robot);
|
||||
handleClose();
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
notify('error', t('robot_duplication.notifications.duplicate_error'));
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { modalStyle } from "../recorder/AddWhereCondModal";
|
||||
import { useGlobalInfoStore } from '../../context/globalInfo';
|
||||
import { getStoredRecording, updateRecording } from '../../api/storage';
|
||||
import { WhereWhatPair } from 'maxun-core';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
interface RobotMeta {
|
||||
name: string;
|
||||
@@ -75,9 +76,9 @@ interface GroupedCredentials {
|
||||
|
||||
export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettings }: RobotSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [robot, setRobot] = useState<RobotSettings | null>(null);
|
||||
const [credentials, setCredentials] = useState<Credentials>({});
|
||||
const { recordingId, notify } = useGlobalInfoStore();
|
||||
const { recordingId, notify, setRerenderRobots } = useGlobalInfoStore();
|
||||
const [robot, setRobot] = useState<RobotSettings | null>(null);
|
||||
const [credentialGroups, setCredentialGroups] = useState<GroupedCredentials>({
|
||||
passwords: [],
|
||||
emails: [],
|
||||
@@ -123,81 +124,113 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin
|
||||
}
|
||||
}, [robot]);
|
||||
|
||||
const extractInitialCredentials = (workflow: any[]): Credentials => {
|
||||
function extractInitialCredentials(workflow: any[]): Credentials {
|
||||
const credentials: Credentials = {};
|
||||
|
||||
// Helper function to check if a character is printable
|
||||
|
||||
const isPrintableCharacter = (char: string): boolean => {
|
||||
return char.length === 1 && !!char.match(/^[\x20-\x7E]$/);
|
||||
};
|
||||
|
||||
// Process each step in the workflow
|
||||
|
||||
workflow.forEach(step => {
|
||||
if (!step.what) return;
|
||||
|
||||
// Keep track of the current input field being processed
|
||||
|
||||
let currentSelector = '';
|
||||
let currentValue = '';
|
||||
let currentType = '';
|
||||
|
||||
// Process actions in sequence to maintain correct text state
|
||||
step.what.forEach((action: any) => {
|
||||
if (
|
||||
(action.action === 'type' || action.action === 'press') &&
|
||||
action.args?.length >= 2 &&
|
||||
typeof action.args[1] === 'string'
|
||||
) {
|
||||
const selector: string = action.args[0];
|
||||
const character: string = action.args[1];
|
||||
const inputType: string = action.args[2] || '';
|
||||
|
||||
// Detect `input[type="password"]`
|
||||
if (!currentType && inputType.toLowerCase() === 'password') {
|
||||
currentType = 'password';
|
||||
let i = 0;
|
||||
|
||||
while (i < step.what.length) {
|
||||
const action = step.what[i];
|
||||
|
||||
if (!action.action || !action.args?.[0]) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const selector = action.args[0];
|
||||
|
||||
// Handle full word type actions first
|
||||
if (action.action === 'type' &&
|
||||
action.args?.length >= 2 &&
|
||||
typeof action.args[1] === 'string' &&
|
||||
action.args[1].length > 1) {
|
||||
|
||||
if (!credentials[selector]) {
|
||||
credentials[selector] = {
|
||||
value: action.args[1],
|
||||
type: action.args[2] || 'text'
|
||||
};
|
||||
}
|
||||
|
||||
// If we're dealing with a new selector, store the previous one
|
||||
if (currentSelector && selector !== currentSelector) {
|
||||
if (!credentials[currentSelector]) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle character-by-character sequences (both type and press)
|
||||
if ((action.action === 'type' || action.action === 'press') &&
|
||||
action.args?.length >= 2 &&
|
||||
typeof action.args[1] === 'string') {
|
||||
|
||||
if (selector !== currentSelector) {
|
||||
if (currentSelector && currentValue) {
|
||||
credentials[currentSelector] = {
|
||||
value: currentValue,
|
||||
type: currentType
|
||||
type: currentType || 'text'
|
||||
};
|
||||
} else {
|
||||
credentials[currentSelector].value = currentValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Update current tracking variables
|
||||
if (selector !== currentSelector) {
|
||||
currentSelector = selector;
|
||||
currentValue = credentials[selector]?.value || '';
|
||||
currentType = inputType || credentials[selector]?.type || '';
|
||||
currentType = action.args[2] || credentials[selector]?.type || 'text';
|
||||
}
|
||||
|
||||
// Handle different types of key actions
|
||||
if (character === 'Backspace') {
|
||||
// Remove the last character when backspace is pressed
|
||||
currentValue = currentValue.slice(0, -1);
|
||||
} else if (isPrintableCharacter(character)) {
|
||||
// Add the character to the current value
|
||||
|
||||
const character = action.args[1];
|
||||
|
||||
if (isPrintableCharacter(character)) {
|
||||
currentValue += character;
|
||||
} else if (character === 'Backspace') {
|
||||
currentValue = currentValue.slice(0, -1);
|
||||
}
|
||||
// Note: We ignore other special keys like 'Shift', 'Enter', etc.
|
||||
|
||||
if (!currentType && action.args[2]?.toLowerCase() === 'password') {
|
||||
currentType = 'password';
|
||||
}
|
||||
|
||||
let j = i + 1;
|
||||
while (j < step.what.length) {
|
||||
const nextAction = step.what[j];
|
||||
if (!nextAction.action || !nextAction.args?.[0] ||
|
||||
nextAction.args[0] !== selector ||
|
||||
(nextAction.action !== 'type' && nextAction.action !== 'press')) {
|
||||
break;
|
||||
}
|
||||
if (nextAction.args[1] === 'Backspace') {
|
||||
currentValue = currentValue.slice(0, -1);
|
||||
} else if (isPrintableCharacter(nextAction.args[1])) {
|
||||
currentValue += nextAction.args[1];
|
||||
}
|
||||
j++;
|
||||
}
|
||||
|
||||
credentials[currentSelector] = {
|
||||
value: currentValue,
|
||||
type: currentType
|
||||
};
|
||||
|
||||
i = j;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
});
|
||||
|
||||
// Store the final state of the last processed selector
|
||||
if (currentSelector) {
|
||||
}
|
||||
|
||||
if (currentSelector && currentValue) {
|
||||
credentials[currentSelector] = {
|
||||
value: currentValue,
|
||||
type: currentType
|
||||
type: currentType || 'text'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return credentials;
|
||||
};
|
||||
}
|
||||
|
||||
const groupCredentialsByType = (credentials: Credentials): GroupedCredentials => {
|
||||
return Object.entries(credentials).reduce((acc: GroupedCredentials, [selector, info]) => {
|
||||
@@ -366,13 +399,11 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin
|
||||
const success = await updateRecording(robot.recording_meta.id, payload);
|
||||
|
||||
if (success) {
|
||||
setRerenderRobots(true);
|
||||
|
||||
notify('success', t('robot_edit.notifications.update_success'));
|
||||
handleStart(robot);
|
||||
handleClose();
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
notify('error', t('robot_edit.notifications.update_failed'));
|
||||
}
|
||||
|
||||
@@ -54,8 +54,8 @@ interface RobotSettingsProps {
|
||||
|
||||
export const RobotSettingsModal = ({ isOpen, handleStart, handleClose, initialSettings }: RobotSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [robot, setRobot] = useState<RobotSettings | null>(null);
|
||||
const [userEmail, setUserEmail] = useState<string | null>(null);
|
||||
const [robot, setRobot] = useState<RobotSettings | null>(null);
|
||||
const { recordingId, notify } = useGlobalInfoStore();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -35,8 +35,9 @@ interface CollapsibleRowProps {
|
||||
currentLog: string;
|
||||
abortRunHandler: () => void;
|
||||
runningRecordingName: string;
|
||||
urlRunId: string | null;
|
||||
}
|
||||
export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRunHandler, runningRecordingName }: CollapsibleRowProps) => {
|
||||
export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRunHandler, runningRecordingName, urlRunId }: CollapsibleRowProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [open, setOpen] = useState(isOpen);
|
||||
@@ -62,14 +63,18 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRun
|
||||
abortRunHandler();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(urlRunId === row.runId || isOpen);
|
||||
}, [urlRunId, row.runId, isOpen]);
|
||||
|
||||
const handleRowExpand = () => {
|
||||
const newOpen = !open;
|
||||
setOpen(newOpen);
|
||||
if (newOpen) {
|
||||
navigate(`/runs/${row.robotMetaId}/run/${row.runId}`);
|
||||
} else {
|
||||
navigate(`/runs/${row.robotMetaId}`);
|
||||
}
|
||||
navigate(
|
||||
newOpen
|
||||
? `/runs/${row.robotMetaId}/run/${row.runId}`
|
||||
: `/runs/${row.robotMetaId}`
|
||||
);
|
||||
//scrollToLogBottom();
|
||||
};
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import TableRow from '@mui/material/TableRow';
|
||||
import { Accordion, AccordionSummary, AccordionDetails, Typography, Box, TextField, CircularProgress, Tooltip } from '@mui/material';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||
import { getStoredRuns } from "../../api/storage";
|
||||
import { RunSettings } from "./RunSettings";
|
||||
@@ -70,6 +70,13 @@ interface RunsTableProps {
|
||||
runningRecordingName: string;
|
||||
}
|
||||
|
||||
interface PaginationState {
|
||||
[robotMetaId: string]: {
|
||||
page: number;
|
||||
rowsPerPage: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const RunsTable: React.FC<RunsTableProps> = ({
|
||||
currentInterpretationLog,
|
||||
abortRunHandler,
|
||||
@@ -78,7 +85,24 @@ export const RunsTable: React.FC<RunsTableProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const getUrlParams = () => {
|
||||
const match = location.pathname.match(/\/runs\/([^\/]+)(?:\/run\/([^\/]+))?/);
|
||||
return {
|
||||
robotMetaId: match?.[1] || null,
|
||||
urlRunId: match?.[2] || null
|
||||
};
|
||||
};
|
||||
|
||||
const { robotMetaId: urlRobotMetaId, urlRunId } = getUrlParams();
|
||||
|
||||
const isAccordionExpanded = useCallback((currentRobotMetaId: string) => {
|
||||
return currentRobotMetaId === urlRobotMetaId;
|
||||
}, [urlRobotMetaId]);
|
||||
|
||||
const [accordionPage, setAccordionPage] = useState(0);
|
||||
const [accordionsPerPage, setAccordionsPerPage] = useState(10);
|
||||
const [accordionSortConfigs, setAccordionSortConfigs] = useState<AccordionSortConfig>({});
|
||||
|
||||
const handleSort = useCallback((columnId: keyof Data, robotMetaId: string) => {
|
||||
@@ -107,11 +131,10 @@ export const RunsTable: React.FC<RunsTableProps> = ({
|
||||
[t]
|
||||
);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const [rows, setRows] = useState<Data[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const [paginationStates, setPaginationStates] = useState<PaginationState>({});
|
||||
|
||||
const { notify, rerenderRuns, setRerenderRuns } = useGlobalInfoStore();
|
||||
|
||||
@@ -119,15 +142,50 @@ export const RunsTable: React.FC<RunsTableProps> = ({
|
||||
navigate(isExpanded ? `/runs/${robotMetaId}` : '/runs');
|
||||
}, [navigate]);
|
||||
|
||||
const handleChangePage = useCallback((event: unknown, newPage: number) => {
|
||||
setPage(newPage);
|
||||
const handleAccordionPageChange = useCallback((event: unknown, newPage: number) => {
|
||||
setAccordionPage(newPage);
|
||||
}, []);
|
||||
|
||||
const handleAccordionsPerPageChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setAccordionsPerPage(+event.target.value);
|
||||
setAccordionPage(0);
|
||||
}, []);
|
||||
|
||||
const handleChangeRowsPerPage = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(+event.target.value);
|
||||
setPage(0);
|
||||
const handleChangePage = useCallback((robotMetaId: string, newPage: number) => {
|
||||
setPaginationStates(prev => ({
|
||||
...prev,
|
||||
[robotMetaId]: {
|
||||
...prev[robotMetaId],
|
||||
page: newPage
|
||||
}
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handleChangeRowsPerPage = useCallback((robotMetaId: string, newRowsPerPage: number) => {
|
||||
setPaginationStates(prev => ({
|
||||
...prev,
|
||||
[robotMetaId]: {
|
||||
page: 0, // Reset to first page when changing rows per page
|
||||
rowsPerPage: newRowsPerPage
|
||||
}
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const getPaginationState = useCallback((robotMetaId: string) => {
|
||||
const defaultState = { page: 0, rowsPerPage: 10 };
|
||||
|
||||
if (!paginationStates[robotMetaId]) {
|
||||
setTimeout(() => {
|
||||
setPaginationStates(prev => ({
|
||||
...prev,
|
||||
[robotMetaId]: defaultState
|
||||
}));
|
||||
}, 0);
|
||||
return defaultState;
|
||||
}
|
||||
return paginationStates[robotMetaId];
|
||||
}, [paginationStates]);
|
||||
|
||||
const debouncedSearch = useCallback((fn: Function, delay: number) => {
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
return (...args: any[]) => {
|
||||
@@ -139,14 +197,20 @@ export const RunsTable: React.FC<RunsTableProps> = ({
|
||||
const handleSearchChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const debouncedSetSearch = debouncedSearch((value: string) => {
|
||||
setSearchTerm(value);
|
||||
setPage(0);
|
||||
setAccordionPage(0);
|
||||
setPaginationStates(prev => {
|
||||
const reset = Object.keys(prev).reduce((acc, robotId) => ({
|
||||
...acc,
|
||||
[robotId]: { ...prev[robotId], page: 0 }
|
||||
}), {});
|
||||
return reset;
|
||||
});
|
||||
}, 300);
|
||||
debouncedSetSearch(event.target.value);
|
||||
}, [debouncedSearch]);
|
||||
|
||||
const fetchRuns = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const runs = await getStoredRuns();
|
||||
if (runs) {
|
||||
const parsedRows: Data[] = runs.map((run: any, index: number) => ({
|
||||
@@ -159,8 +223,6 @@ export const RunsTable: React.FC<RunsTableProps> = ({
|
||||
}
|
||||
} catch (error) {
|
||||
notify('error', t('runstable.notifications.fetch_error'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [notify, t]);
|
||||
|
||||
@@ -194,18 +256,6 @@ export const RunsTable: React.FC<RunsTableProps> = ({
|
||||
return result;
|
||||
}, [rows, searchTerm]);
|
||||
|
||||
// Group filtered rows by robot meta id
|
||||
const groupedRows = useMemo(() =>
|
||||
filteredRows.reduce((acc, row) => {
|
||||
if (!acc[row.robotMetaId]) {
|
||||
acc[row.robotMetaId] = [];
|
||||
}
|
||||
acc[row.robotMetaId].push(row);
|
||||
return acc;
|
||||
}, {} as Record<string, Data[]>),
|
||||
[filteredRows]
|
||||
);
|
||||
|
||||
const parseDateString = (dateStr: string): Date => {
|
||||
try {
|
||||
if (dateStr.includes('PM') || dateStr.includes('AM')) {
|
||||
@@ -218,7 +268,37 @@ export const RunsTable: React.FC<RunsTableProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const groupedRows = useMemo(() => {
|
||||
const groupedData = filteredRows.reduce((acc, row) => {
|
||||
if (!acc[row.robotMetaId]) {
|
||||
acc[row.robotMetaId] = [];
|
||||
}
|
||||
acc[row.robotMetaId].push(row);
|
||||
return acc;
|
||||
}, {} as Record<string, Data[]>);
|
||||
|
||||
Object.keys(groupedData).forEach(robotId => {
|
||||
groupedData[robotId].sort((a, b) =>
|
||||
parseDateString(b.startedAt).getTime() - parseDateString(a.startedAt).getTime()
|
||||
);
|
||||
});
|
||||
|
||||
const robotEntries = Object.entries(groupedData).map(([robotId, runs]) => ({
|
||||
robotId,
|
||||
runs,
|
||||
latestRunDate: parseDateString(runs[0].startedAt).getTime()
|
||||
}));
|
||||
|
||||
robotEntries.sort((a, b) => b.latestRunDate - a.latestRunDate);
|
||||
|
||||
return robotEntries.reduce((acc, { robotId, runs }) => {
|
||||
acc[robotId] = runs;
|
||||
return acc;
|
||||
}, {} as Record<string, Data[]>);
|
||||
}, [filteredRows]);
|
||||
|
||||
const renderTableRows = useCallback((data: Data[], robotMetaId: string) => {
|
||||
const { page, rowsPerPage } = getPaginationState(robotMetaId);
|
||||
const start = page * rowsPerPage;
|
||||
const end = start + rowsPerPage;
|
||||
|
||||
@@ -245,13 +325,14 @@ export const RunsTable: React.FC<RunsTableProps> = ({
|
||||
key={`row-${row.id}`}
|
||||
row={row}
|
||||
handleDelete={handleDelete}
|
||||
isOpen={runId === row.runId && runningRecordingName === row.name}
|
||||
isOpen={urlRunId === row.runId || (runId === row.runId && runningRecordingName === row.name)}
|
||||
currentLog={currentInterpretationLog}
|
||||
abortRunHandler={abortRunHandler}
|
||||
runningRecordingName={runningRecordingName}
|
||||
urlRunId={urlRunId}
|
||||
/>
|
||||
));
|
||||
}, [page, rowsPerPage, runId, runningRecordingName, currentInterpretationLog, abortRunHandler, handleDelete, accordionSortConfigs]);
|
||||
}, [paginationStates, runId, runningRecordingName, currentInterpretationLog, abortRunHandler, handleDelete, accordionSortConfigs]);
|
||||
|
||||
const renderSortIcon = useCallback((column: Column, robotMetaId: string) => {
|
||||
const sortConfig = accordionSortConfigs[robotMetaId];
|
||||
@@ -279,14 +360,6 @@ export const RunsTable: React.FC<RunsTableProps> = ({
|
||||
: <UnfoldMore fontSize="small" />;
|
||||
}, [accordionSortConfigs]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Box display="flex" justifyContent="center" alignItems="center" height="50vh">
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
|
||||
@@ -305,82 +378,99 @@ export const RunsTable: React.FC<RunsTableProps> = ({
|
||||
</Box>
|
||||
|
||||
<TableContainer component={Paper} sx={{ width: '100%', overflow: 'hidden' }}>
|
||||
{Object.entries(groupedRows).map(([robotMetaId, data]) => (
|
||||
<Accordion
|
||||
key={robotMetaId}
|
||||
onChange={(event, isExpanded) => handleAccordionChange(robotMetaId, isExpanded)}
|
||||
TransitionProps={{ unmountOnExit: true }} // Optimize accordion rendering
|
||||
>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography variant="h6">{data[data.length - 1].name}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Table stickyHeader aria-label="sticky table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell />
|
||||
{translatedColumns.map((column) => (
|
||||
<TableCell
|
||||
key={column.id}
|
||||
align={column.align}
|
||||
style={{
|
||||
minWidth: column.minWidth,
|
||||
cursor: column.id === 'startedAt' || column.id === 'finishedAt' ? 'pointer' : 'default'
|
||||
}}
|
||||
onClick={() => {
|
||||
if (column.id === 'startedAt' || column.id === 'finishedAt') {
|
||||
handleSort(column.id, robotMetaId);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
title={
|
||||
(column.id === 'startedAt' || column.id === 'finishedAt')
|
||||
? t('runstable.sort_tooltip')
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
'&:hover': {
|
||||
'& .sort-icon': {
|
||||
opacity: 1
|
||||
}
|
||||
{Object.entries(groupedRows)
|
||||
.slice(
|
||||
accordionPage * accordionsPerPage,
|
||||
accordionPage * accordionsPerPage + accordionsPerPage
|
||||
)
|
||||
.map(([robotMetaId, data]) => (
|
||||
<Accordion
|
||||
key={robotMetaId}
|
||||
onChange={(event, isExpanded) => handleAccordionChange(robotMetaId, isExpanded)}
|
||||
TransitionProps={{ unmountOnExit: true }} // Optimize accordion rendering
|
||||
>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography variant="h6">{data[data.length - 1].name}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Table stickyHeader aria-label="sticky table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell />
|
||||
{translatedColumns.map((column) => (
|
||||
<TableCell
|
||||
key={column.id}
|
||||
align={column.align}
|
||||
style={{
|
||||
minWidth: column.minWidth,
|
||||
cursor: column.id === 'startedAt' || column.id === 'finishedAt' ? 'pointer' : 'default'
|
||||
}}
|
||||
onClick={() => {
|
||||
if (column.id === 'startedAt' || column.id === 'finishedAt') {
|
||||
handleSort(column.id, robotMetaId);
|
||||
}
|
||||
}}>
|
||||
{column.label}
|
||||
<Box className="sort-icon" sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
opacity: accordionSortConfigs[robotMetaId]?.field === column.id ? 1 : 0.3,
|
||||
transition: 'opacity 0.2s'
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
title={
|
||||
(column.id === 'startedAt' || column.id === 'finishedAt')
|
||||
? t('runstable.sort_tooltip')
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
'&:hover': {
|
||||
'& .sort-icon': {
|
||||
opacity: 1
|
||||
}
|
||||
}
|
||||
}}>
|
||||
{renderSortIcon(column, robotMetaId)}
|
||||
{column.label}
|
||||
<Box className="sort-icon" sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
opacity: accordionSortConfigs[robotMetaId]?.field === column.id ? 1 : 0.3,
|
||||
transition: 'opacity 0.2s'
|
||||
}}>
|
||||
{renderSortIcon(column, robotMetaId)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{renderTableRows(data, robotMetaId)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
))}
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{renderTableRows(data, robotMetaId)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<TablePagination
|
||||
component="div"
|
||||
count={data.length}
|
||||
rowsPerPage={getPaginationState(robotMetaId).rowsPerPage}
|
||||
page={getPaginationState(robotMetaId).page}
|
||||
onPageChange={(_, newPage) => handleChangePage(robotMetaId, newPage)}
|
||||
onRowsPerPageChange={(event) =>
|
||||
handleChangeRowsPerPage(robotMetaId, +event.target.value)
|
||||
}
|
||||
rowsPerPageOptions={[10, 25, 50, 100]}
|
||||
/>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
))}
|
||||
</TableContainer>
|
||||
|
||||
<TablePagination
|
||||
component="div"
|
||||
count={filteredRows.length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
count={Object.keys(groupedRows).length}
|
||||
page={accordionPage}
|
||||
rowsPerPage={accordionsPerPage}
|
||||
onPageChange={handleAccordionPageChange}
|
||||
onRowsPerPageChange={handleAccordionsPerPageChange}
|
||||
rowsPerPageOptions={[10, 25, 50, 100]}
|
||||
/>
|
||||
</React.Fragment>
|
||||
|
||||
Reference in New Issue
Block a user