Merge commit 'refs/pull/363/head' of https://github.com/getmaxun/maxun into develop

This commit is contained in:
amhsirak
2025-01-22 18:23:33 +05:30
13 changed files with 273 additions and 16 deletions

View File

@@ -5,6 +5,10 @@ import { ScheduleSettings } from "../components/robot/ScheduleSettings";
import { CreateRunResponse, ScheduleRunResponse } from "../pages/MainPage";
import { apiUrl } from "../apiConfig";
interface Credentials {
[key: string]: string;
}
export const getStoredRecordings = async (): Promise<string[] | null> => {
try {
const response = await axios.get(`${apiUrl}/storage/recordings`);
@@ -19,7 +23,7 @@ export const getStoredRecordings = async (): Promise<string[] | null> => {
}
};
export const updateRecording = async (id: string, data: { name?: string; limit?: number }): Promise<boolean> => {
export const updateRecording = async (id: string, data: { name?: string; limit?: number, credentials?: Credentials }): Promise<boolean> => {
try {
const response = await axios.put(`${apiUrl}/storage/recordings/${id}`, data);
if (response.status === 200) {

View File

@@ -22,7 +22,7 @@ export const SaveRecording = ({ fileName }: SaveRecordingProps) => {
const [recordingName, setRecordingName] = useState<string>(fileName);
const [waitingForSave, setWaitingForSave] = useState<boolean>(false);
const { browserId, setBrowserId, notify, recordings } = useGlobalInfoStore();
const { browserId, setBrowserId, notify, recordings, isLogin } = useGlobalInfoStore();
const { socket } = useSocketStore();
const { state, dispatch } = useContext(AuthContext);
const { user } = state;
@@ -59,7 +59,7 @@ export const SaveRecording = ({ fileName }: SaveRecordingProps) => {
// releases resources and changes the view for main page by clearing the global browserId
const saveRecording = async () => {
if (user) {
const payload = { fileName: recordingName, userId: user.id };
const payload = { fileName: recordingName, userId: user.id, isLogin: isLogin };
socket?.emit('save', payload);
setWaitingForSave(true);
console.log(`Saving the recording as ${recordingName} for userId ${user.id}`);

View File

@@ -11,7 +11,7 @@ import TableRow from '@mui/material/TableRow';
import { useEffect } from "react";
import { WorkflowFile } from "maxun-core";
import SearchIcon from '@mui/icons-material/Search';
import { IconButton, Button, Box, Typography, TextField, MenuItem, Menu, ListItemIcon, ListItemText, CircularProgress } from "@mui/material";
import { IconButton, Button, Box, Typography, TextField, MenuItem, Menu, ListItemIcon, ListItemText, CircularProgress, RadioGroup, FormControlLabel, Radio } from "@mui/material";
import { Schedule, DeleteForever, Edit, PlayCircle, Settings, Power, ContentCopy, MoreHoriz } from "@mui/icons-material";
import { useGlobalInfoStore } from "../../context/globalInfo";
import { checkRunsForRecording, deleteRecordingFromStorage, getStoredRecordings } from "../../api/storage";
@@ -85,7 +85,7 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
},
];
const { notify, setRecordings, browserId, setBrowserId, setInitialUrl, recordingUrl, setRecordingUrl, recordingName, setRecordingName, recordingId, setRecordingId } = useGlobalInfoStore();
const { notify, setRecordings, browserId, setBrowserId, setInitialUrl, recordingUrl, setRecordingUrl, isLogin, setIsLogin, recordingName, setRecordingName, recordingId, setRecordingId } = useGlobalInfoStore();
const navigate = useNavigate();
const handleChangePage = (event: unknown, newPage: number) => {
@@ -321,6 +321,19 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
onChange={setBrowserRecordingUrl}
style={{ marginBottom: '20px', marginTop: '20px' }}
/>
<Typography variant="h6" gutterBottom>{t('recordingtable.modal.login_title')}</Typography>
<RadioGroup
aria-labelledby="login-requirement-radio-group"
name="login-requirement"
value={isLogin ? 'yes' : 'no'}
onChange={(e) => setIsLogin(e.target.value === 'yes')}
style={{ marginBottom: '20px' }}
>
<FormControlLabel value="yes" control={<Radio />} label="Yes" />
<FormControlLabel value="no" control={<Radio />} label="No" />
</RadioGroup>
<Button
variant="contained"
color="primary"

View File

@@ -1,7 +1,8 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { GenericModal } from "../ui/GenericModal";
import { TextField, Typography, Box, Button } from "@mui/material";
import { TextField, Typography, Box, Button, IconButton, InputAdornment } from "@mui/material";
import { Visibility, VisibilityOff } from '@mui/icons-material';
import { modalStyle } from "../recorder/AddWhereCondModal";
import { useGlobalInfoStore } from '../../context/globalInfo';
import { getStoredRecording, updateRecording } from '../../api/storage';
@@ -25,6 +26,14 @@ interface RobotEditOptions {
limit?: number;
}
interface Credentials {
[key: string]: string;
}
interface CredentialVisibility {
[key: string]: boolean;
}
interface ScheduleConfig {
runEvery: number;
runEveryUnit: 'MINUTES' | 'HOURS' | 'DAYS' | 'WEEKS' | 'MONTHS';
@@ -48,6 +57,7 @@ export interface RobotSettings {
google_access_token?: string | null;
google_refresh_token?: string | null;
schedule?: ScheduleConfig | null;
isLogin?: boolean;
}
interface RobotSettingsProps {
@@ -60,7 +70,17 @@ interface RobotSettingsProps {
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 [credentialSelectors, setCredentialSelectors] = useState<string[]>([]);
const [showPasswords, setShowPasswords] = useState<CredentialVisibility>({});
const handleClickShowPassword = (selector: string) => {
setShowPasswords(prev => ({
...prev,
[selector]: !prev[selector]
}));
};
useEffect(() => {
if (isOpen) {
@@ -68,6 +88,68 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin
}
}, [isOpen]);
useEffect(() => {
if (robot?.recording?.workflow) {
const selectors = findCredentialSelectors(robot.recording.workflow);
setCredentialSelectors(selectors);
const initialCredentials = extractInitialCredentials(robot.recording.workflow);
setCredentials(initialCredentials);
}
}, [robot]);
const findCredentialSelectors = (workflow: WhereWhatPair[]): string[] => {
const selectors = new Set<string>();
workflow?.forEach(step => {
step.what?.forEach(action => {
if (
(action.action === 'type') &&
action.args &&
action.args[0] &&
typeof action.args[0] === 'string'
) {
selectors.add(action.args[0]);
}
});
});
return Array.from(selectors);
};
const extractInitialCredentials = (workflow: any[]): Record<string, string> => {
const credentials: Record<string, string> = {};
const isPrintableCharacter = (char: string): boolean => {
return char.length === 1 && !!char.match(/^[\x20-\x7E]$/);
};
workflow.forEach(step => {
if (!step.what) return;
step.what.forEach((action: any) => {
if (
(action.action === 'type' || action.action === 'press') &&
action.args?.length >= 2 &&
typeof action.args[1] === 'string'
) {
let currentSelector: string = action.args[0];
let character: string = action.args[1];
if (!credentials.hasOwnProperty(currentSelector)) {
credentials[currentSelector] = '';
}
if (isPrintableCharacter(character)) {
credentials[currentSelector] += character;
}
}
});
});
return credentials;
};
const getRobot = async () => {
if (recordingId) {
const robot = await getStoredRecording(recordingId);
@@ -83,6 +165,13 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin
);
};
const handleCredentialChange = (selector: string, value: string) => {
setCredentials(prev => ({
...prev,
[selector]: value
}));
};
const handleLimitChange = (newLimit: number) => {
setRobot((prev) => {
if (!prev) return prev;
@@ -111,6 +200,7 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin
const payload = {
name: robot.recording_meta.name,
limit: robot.recording.workflow[0]?.what[0]?.args?.[0]?.limit,
credentials: credentials,
};
const success = await updateRecording(robot.recording_meta.id, payload);
@@ -170,6 +260,38 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin
/>
)}
{(robot.isLogin || credentialSelectors.length > 0) && (
<>
<Typography variant="h6" style={{ marginBottom: '20px' }}>
{t('Login Credentials')}
</Typography>
{credentialSelectors.map((selector) => (
<TextField
key={selector}
type={showPasswords[selector] ? 'text' : 'password'}
label={`Credential for ${selector}`}
value={credentials[selector] || ''}
onChange={(e) => handleCredentialChange(selector, e.target.value)}
style={{ marginBottom: '20px' }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={() => handleClickShowPassword(selector)}
edge="end"
>
{showPasswords[selector] ? <Visibility /> : <VisibilityOff />}
</IconButton>
</InputAdornment>
),
}}
/>
))}
</>
)}
<Box mt={2} display="flex" justifyContent="flex-end">
<Button variant="contained" color="primary" onClick={handleSave}>
{t('robot_edit.save')}

View File

@@ -10,6 +10,8 @@ interface GlobalInfo {
notification: AlertSnackbarProps;
notify: (severity: 'error' | 'warning' | 'info' | 'success', message: string) => void;
closeNotify: () => void;
isLogin: boolean;
setIsLogin: (isLogin: boolean) => void;
recordings: string[];
setRecordings: (recordings: string[]) => void;
rerenderRuns: boolean;
@@ -53,6 +55,7 @@ class GlobalInfoStore implements Partial<GlobalInfo> {
recordingName = '';
initialUrl = 'https://';
recordingUrl = 'https://';
isLogin = false;
currentWorkflowActionsState = {
hasScrapeListAction: false,
hasScreenshotAction: false,
@@ -75,6 +78,7 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => {
const [recordingLength, setRecordingLength] = useState<number>(globalInfoStore.recordingLength);
const [recordingId, setRecordingId] = useState<string | null>(globalInfoStore.recordingId);
const [recordingName, setRecordingName] = useState<string>(globalInfoStore.recordingName);
const [isLogin, setIsLogin] = useState<boolean>(globalInfoStore.isLogin);
const [initialUrl, setInitialUrl] = useState<string>(globalInfoStore.initialUrl);
const [recordingUrl, setRecordingUrl] = useState<string>(globalInfoStore.recordingUrl);
const [currentWorkflowActionsState, setCurrentWorkflowActionsState] = useState(globalInfoStore.currentWorkflowActionsState);
@@ -127,6 +131,8 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => {
setInitialUrl,
recordingUrl,
setRecordingUrl,
isLogin,
setIsLogin,
currentWorkflowActionsState,
setCurrentWorkflowActionsState,
shouldResetInterpretationLog,