Merge branch 'develop' into bulk-scraping

This commit is contained in:
Rohit
2025-01-13 12:19:23 +05:30
94 changed files with 2443 additions and 1491 deletions

View File

@@ -15,6 +15,7 @@ Maxun lets you train a robot in 2 minutes and scrape the web on auto-pilot. Web
<p align="center"> <p align="center">
<a href="https://docs.maxun.dev/?ref=ghread"><b>Documentation</b></a> |
<a href="https://www.maxun.dev/?ref=ghread"><b>Website</b></a> | <a href="https://www.maxun.dev/?ref=ghread"><b>Website</b></a> |
<a href="https://discord.gg/5GbPjBUkws"><b>Discord</b></a> | <a href="https://discord.gg/5GbPjBUkws"><b>Discord</b></a> |
<a href="https://x.com/maxun_io?ref=ghread"><b>Twitter</b></a> | <a href="https://x.com/maxun_io?ref=ghread"><b>Twitter</b></a> |

View File

@@ -43,7 +43,7 @@ services:
#build: #build:
#context: . #context: .
#dockerfile: server/Dockerfile #dockerfile: server/Dockerfile
image: getmaxun/maxun-backend:v0.0.9 image: getmaxun/maxun-backend:v0.0.10
ports: ports:
- "${BACKEND_PORT:-8080}:${BACKEND_PORT:-8080}" - "${BACKEND_PORT:-8080}:${BACKEND_PORT:-8080}"
env_file: .env env_file: .env
@@ -70,7 +70,7 @@ services:
#build: #build:
#context: . #context: .
#dockerfile: Dockerfile #dockerfile: Dockerfile
image: getmaxun/maxun-frontend:v0.0.5 image: getmaxun/maxun-frontend:v0.0.7
ports: ports:
- "${FRONTEND_PORT:-5173}:${FRONTEND_PORT:-5173}" - "${FRONTEND_PORT:-5173}:${FRONTEND_PORT:-5173}"
env_file: .env env_file: .env

View File

@@ -1,6 +1,6 @@
{ {
"name": "maxun-core", "name": "maxun-core",
"version": "0.0.7", "version": "0.0.8",
"description": "Core package for Maxun, responsible for data extraction", "description": "Core package for Maxun, responsible for data extraction",
"main": "build/index.js", "main": "build/index.js",
"typings": "build/index.d.ts", "typings": "build/index.d.ts",

View File

@@ -1,6 +1,6 @@
{ {
"name": "maxun", "name": "maxun",
"version": "0.0.5", "version": "0.0.6",
"author": "Maxun", "author": "Maxun",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
@@ -44,9 +44,10 @@
"joi": "^17.6.0", "joi": "^17.6.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"lodash": "^4.17.21",
"loglevel": "^1.8.0", "loglevel": "^1.8.0",
"loglevel-plugin-remote": "^0.6.8", "loglevel-plugin-remote": "^0.6.8",
"maxun-core": "^0.0.7", "maxun-core": "^0.0.8",
"minio": "^8.0.1", "minio": "^8.0.1",
"moment-timezone": "^0.5.45", "moment-timezone": "^0.5.45",
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
@@ -66,6 +67,7 @@
"react-transition-group": "^4.4.2", "react-transition-group": "^4.4.2",
"sequelize": "^6.37.3", "sequelize": "^6.37.3",
"sequelize-typescript": "^2.1.6", "sequelize-typescript": "^2.1.6",
"sharp": "^0.33.5",
"socket.io": "^4.4.1", "socket.io": "^4.4.1",
"socket.io-client": "^4.4.1", "socket.io-client": "^4.4.1",
"styled-components": "^5.3.3", "styled-components": "^5.3.3",
@@ -97,6 +99,7 @@
"@types/cookie-parser": "^1.4.7", "@types/cookie-parser": "^1.4.7",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/lodash": "^4.17.14",
"@types/loglevel": "^1.6.3", "@types/loglevel": "^1.6.3",
"@types/node": "22.7.9", "@types/node": "22.7.9",
"@types/node-cron": "^3.0.11", "@types/node-cron": "^3.0.11",

181
perf/performance.ts Normal file
View File

@@ -0,0 +1,181 @@
// Frontend Performance Monitoring
export class FrontendPerformanceMonitor {
private metrics: {
fps: number[];
memoryUsage: MemoryInfo[];
renderTime: number[];
eventLatency: number[];
};
private lastFrameTime: number;
private frameCount: number;
constructor() {
this.metrics = {
fps: [],
memoryUsage: [],
renderTime: [],
eventLatency: [],
};
this.lastFrameTime = performance.now();
this.frameCount = 0;
// Start monitoring
this.startMonitoring();
}
private startMonitoring(): void {
// Monitor FPS
const measureFPS = () => {
const currentTime = performance.now();
const elapsed = currentTime - this.lastFrameTime;
this.frameCount++;
if (elapsed >= 1000) { // Calculate FPS every second
const fps = Math.round((this.frameCount * 1000) / elapsed);
this.metrics.fps.push(fps);
this.frameCount = 0;
this.lastFrameTime = currentTime;
}
requestAnimationFrame(measureFPS);
};
requestAnimationFrame(measureFPS);
// Monitor Memory Usage
if (window.performance && (performance as any).memory) {
setInterval(() => {
const memory = (performance as any).memory;
this.metrics.memoryUsage.push({
usedJSHeapSize: memory.usedJSHeapSize,
totalJSHeapSize: memory.totalJSHeapSize,
timestamp: Date.now()
});
}, 1000);
}
}
// Monitor Canvas Render Time
public measureRenderTime(renderFunction: () => void): void {
const startTime = performance.now();
renderFunction();
const endTime = performance.now();
this.metrics.renderTime.push(endTime - startTime);
}
// Monitor Event Latency
public measureEventLatency(event: MouseEvent | KeyboardEvent): void {
const latency = performance.now() - event.timeStamp;
this.metrics.eventLatency.push(latency);
}
// Get Performance Report
public getPerformanceReport(): PerformanceReport {
return {
averageFPS: this.calculateAverage(this.metrics.fps),
averageRenderTime: this.calculateAverage(this.metrics.renderTime),
averageEventLatency: this.calculateAverage(this.metrics.eventLatency),
memoryTrend: this.getMemoryTrend(),
lastMemoryUsage: this.metrics.memoryUsage[this.metrics.memoryUsage.length - 1]
};
}
private calculateAverage(array: number[]): number {
return array.length ? array.reduce((a, b) => a + b) / array.length : 0;
}
private getMemoryTrend(): MemoryTrend {
if (this.metrics.memoryUsage.length < 2) return 'stable';
const latest = this.metrics.memoryUsage[this.metrics.memoryUsage.length - 1];
const previous = this.metrics.memoryUsage[this.metrics.memoryUsage.length - 2];
const change = latest.usedJSHeapSize - previous.usedJSHeapSize;
if (change > 1000000) return 'increasing'; // 1MB threshold
if (change < -1000000) return 'decreasing';
return 'stable';
}
}
// Backend Performance Monitoring
export class BackendPerformanceMonitor {
private metrics: {
screenshotTimes: number[];
emitTimes: number[];
memoryUsage: NodeJS.MemoryUsage[];
};
constructor() {
this.metrics = {
screenshotTimes: [],
emitTimes: [],
memoryUsage: []
};
this.startMonitoring();
}
private startMonitoring(): void {
// Monitor Memory Usage
setInterval(() => {
this.metrics.memoryUsage.push(process.memoryUsage());
}, 1000);
}
public async measureScreenshotPerformance(
makeScreenshot: () => Promise<void>
): Promise<void> {
const startTime = process.hrtime();
await makeScreenshot();
const [seconds, nanoseconds] = process.hrtime(startTime);
this.metrics.screenshotTimes.push(seconds * 1000 + nanoseconds / 1000000);
}
public measureEmitPerformance(emitFunction: () => void): void {
const startTime = process.hrtime();
emitFunction();
const [seconds, nanoseconds] = process.hrtime(startTime);
this.metrics.emitTimes.push(seconds * 1000 + nanoseconds / 1000000);
}
public getPerformanceReport(): BackendPerformanceReport {
return {
averageScreenshotTime: this.calculateAverage(this.metrics.screenshotTimes),
averageEmitTime: this.calculateAverage(this.metrics.emitTimes),
currentMemoryUsage: this.metrics.memoryUsage[this.metrics.memoryUsage.length - 1],
memoryTrend: this.getMemoryTrend()
};
}
private calculateAverage(array: number[]): number {
return array.length ? array.reduce((a, b) => a + b) / array.length : 0;
}
private getMemoryTrend(): MemoryTrend {
if (this.metrics.memoryUsage.length < 2) return 'stable';
const latest = this.metrics.memoryUsage[this.metrics.memoryUsage.length - 1];
const previous = this.metrics.memoryUsage[this.metrics.memoryUsage.length - 2];
const change = latest.heapUsed - previous.heapUsed;
if (change > 1000000) return 'increasing';
if (change < -1000000) return 'decreasing';
return 'stable';
}
}
interface MemoryInfo {
usedJSHeapSize: number;
totalJSHeapSize: number;
timestamp: number;
}
type MemoryTrend = 'increasing' | 'decreasing' | 'stable';
interface PerformanceReport {
averageFPS: number;
averageRenderTime: number;
averageEventLatency: number;
memoryTrend: MemoryTrend;
lastMemoryUsage: MemoryInfo;
}
interface BackendPerformanceReport {
averageScreenshotTime: number;
averageEmitTime: number;
currentMemoryUsage: NodeJS.MemoryUsage;
memoryTrend: MemoryTrend;
}

View File

@@ -9,6 +9,8 @@ import { chromium } from 'playwright-extra';
import stealthPlugin from 'puppeteer-extra-plugin-stealth'; import stealthPlugin from 'puppeteer-extra-plugin-stealth';
import { PlaywrightBlocker } from '@cliqz/adblocker-playwright'; import { PlaywrightBlocker } from '@cliqz/adblocker-playwright';
import fetch from 'cross-fetch'; import fetch from 'cross-fetch';
import { throttle } from 'lodash';
import sharp from 'sharp';
import logger from '../../logger'; import logger from '../../logger';
import { InterpreterSettings, RemoteBrowserOptions } from "../../types"; import { InterpreterSettings, RemoteBrowserOptions } from "../../types";
@@ -16,8 +18,30 @@ import { WorkflowGenerator } from "../../workflow-management/classes/Generator";
import { WorkflowInterpreter } from "../../workflow-management/classes/Interpreter"; import { WorkflowInterpreter } from "../../workflow-management/classes/Interpreter";
import { getDecryptedProxyConfig } from '../../routes/proxy'; import { getDecryptedProxyConfig } from '../../routes/proxy';
import { getInjectableScript } from 'idcac-playwright'; import { getInjectableScript } from 'idcac-playwright';
chromium.use(stealthPlugin()); chromium.use(stealthPlugin());
const MEMORY_CONFIG = {
gcInterval: 60000, // 1 minute
maxHeapSize: 2048 * 1024 * 1024, // 2GB
heapUsageThreshold: 0.85 // 85%
};
const SCREENCAST_CONFIG: {
format: "jpeg" | "png";
maxWidth: number;
maxHeight: number;
targetFPS: number;
compressionQuality: number;
maxQueueSize: number;
} = {
format: 'jpeg',
maxWidth: 900,
maxHeight: 400,
targetFPS: 30,
compressionQuality: 0.8,
maxQueueSize: 2
};
/** /**
* This class represents a remote browser instance. * This class represents a remote browser instance.
@@ -78,6 +102,11 @@ export class RemoteBrowser {
*/ */
public interpreter: WorkflowInterpreter; public interpreter: WorkflowInterpreter;
private screenshotQueue: Buffer[] = [];
private isProcessingScreenshot = false;
private screencastInterval: NodeJS.Timeout | null = null
/** /**
* Initializes a new instances of the {@link Generator} and {@link WorkflowInterpreter} classes and * Initializes a new instances of the {@link Generator} and {@link WorkflowInterpreter} classes and
* assigns the socket instance everywhere. * assigns the socket instance everywhere.
@@ -90,6 +119,46 @@ export class RemoteBrowser {
this.generator = new WorkflowGenerator(socket); this.generator = new WorkflowGenerator(socket);
} }
private initializeMemoryManagement(): void {
setInterval(() => {
const memoryUsage = process.memoryUsage();
const heapUsageRatio = memoryUsage.heapUsed / MEMORY_CONFIG.maxHeapSize;
if (heapUsageRatio > MEMORY_CONFIG.heapUsageThreshold) {
logger.warn('High memory usage detected, triggering cleanup');
this.performMemoryCleanup();
}
// Clear screenshot queue if it's too large
if (this.screenshotQueue.length > SCREENCAST_CONFIG.maxQueueSize) {
this.screenshotQueue = this.screenshotQueue.slice(-SCREENCAST_CONFIG.maxQueueSize);
}
}, MEMORY_CONFIG.gcInterval);
}
private async performMemoryCleanup(): Promise<void> {
this.screenshotQueue = [];
this.isProcessingScreenshot = false;
if (global.gc) {
global.gc();
}
// Reset CDP session if needed
if (this.client) {
try {
await this.stopScreencast();
this.client = null;
if (this.currentPage) {
this.client = await this.currentPage.context().newCDPSession(this.currentPage);
await this.startScreencast();
}
} catch (error) {
logger.error('Error resetting CDP session:', error);
}
}
}
/** /**
* Normalizes URLs to prevent navigation loops while maintaining consistent format * Normalizes URLs to prevent navigation loops while maintaining consistent format
*/ */
@@ -157,7 +226,7 @@ export class RemoteBrowser {
'Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.62 Safari/537.36', 'Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.62 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:118.0) Gecko/20100101 Firefox/118.0', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:118.0) Gecko/20100101 Firefox/118.0',
]; ];
return userAgents[Math.floor(Math.random() * userAgents.length)]; return userAgents[Math.floor(Math.random() * userAgents.length)];
} }
@@ -178,7 +247,7 @@ export class RemoteBrowser {
"--disable-extensions", "--disable-extensions",
"--no-sandbox", "--no-sandbox",
"--disable-dev-shm-usage", "--disable-dev-shm-usage",
], ],
})); }));
const proxyConfig = await getDecryptedProxyConfig(userId); const proxyConfig = await getDecryptedProxyConfig(userId);
let proxyOptions: { server: string, username?: string, password?: string } = { server: '' }; let proxyOptions: { server: string, username?: string, password?: string } = { server: '' };
@@ -251,11 +320,11 @@ export class RemoteBrowser {
this.client = await this.currentPage.context().newCDPSession(this.currentPage); this.client = await this.currentPage.context().newCDPSession(this.currentPage);
await blocker.disableBlockingInPage(this.currentPage); await blocker.disableBlockingInPage(this.currentPage);
console.log('Adblocker initialized'); console.log('Adblocker initialized');
} catch (error: any) { } catch (error: any) {
console.warn('Failed to initialize adblocker, continuing without it:', error.message); console.warn('Failed to initialize adblocker, continuing without it:', error.message);
// Still need to set up the CDP session even if blocker fails // Still need to set up the CDP session even if blocker fails
this.client = await this.currentPage.context().newCDPSession(this.currentPage); this.client = await this.currentPage.context().newCDPSession(this.currentPage);
} }
}; };
/** /**
@@ -319,7 +388,7 @@ export class RemoteBrowser {
return; return;
} }
this.client.on('Page.screencastFrame', ({ data: base64, sessionId }) => { this.client.on('Page.screencastFrame', ({ data: base64, sessionId }) => {
this.emitScreenshot(base64) this.emitScreenshot(Buffer.from(base64, 'base64'))
setTimeout(async () => { setTimeout(async () => {
try { try {
if (!this.client) { if (!this.client) {
@@ -339,16 +408,49 @@ export class RemoteBrowser {
* If an interpretation was running it will be stopped. * If an interpretation was running it will be stopped.
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
public switchOff = async (): Promise<void> => { public async switchOff(): Promise<void> {
await this.interpreter.stopInterpretation(); try {
if (this.browser) { await this.interpreter.stopInterpretation();
await this.stopScreencast();
await this.browser.close(); if (this.screencastInterval) {
} else { clearInterval(this.screencastInterval);
logger.log('error', 'Browser wasn\'t initialized'); }
logger.log('error', 'Switching off the browser failed');
if (this.client) {
await this.stopScreencast();
}
if (this.browser) {
await this.browser.close();
}
this.screenshotQueue = [];
//this.performanceMonitor.reset();
} catch (error) {
logger.error('Error during browser shutdown:', error);
} }
}; }
private async optimizeScreenshot(screenshot: Buffer): Promise<Buffer> {
try {
return await sharp(screenshot)
.jpeg({
quality: Math.round(SCREENCAST_CONFIG.compressionQuality * 100),
progressive: true
})
.resize({
width: SCREENCAST_CONFIG.maxWidth,
height: SCREENCAST_CONFIG.maxHeight,
fit: 'inside',
withoutEnlargement: true
})
.toBuffer();
} catch (error) {
logger.error('Screenshot optimization failed:', error);
return screenshot;
}
}
/** /**
* Makes and emits a single screenshot to the client side. * Makes and emits a single screenshot to the client side.
@@ -358,7 +460,7 @@ export class RemoteBrowser {
try { try {
const screenshot = await this.currentPage?.screenshot(); const screenshot = await this.currentPage?.screenshot();
if (screenshot) { if (screenshot) {
this.emitScreenshot(screenshot.toString('base64')); this.emitScreenshot(screenshot);
} }
} catch (e) { } catch (e) {
const { message } = e as Error; const { message } = e as Error;
@@ -490,37 +592,85 @@ export class RemoteBrowser {
* Should be called only once after the browser is fully initialized. * Should be called only once after the browser is fully initialized.
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private startScreencast = async (): Promise<void> => { private async startScreencast(): Promise<void> {
if (!this.client) { if (!this.client) {
logger.log('warn', 'client is not initialized'); logger.warn('Client is not initialized');
return; return;
} }
await this.client.send('Page.startScreencast', { format: 'jpeg', quality: 75 });
logger.log('info', `Browser started with screencasting a page.`);
};
/** try {
* Unsubscribes the current page from the screencast session. await this.client.send('Page.startScreencast', {
* @returns {Promise<void>} format: SCREENCAST_CONFIG.format,
*/ });
private stopScreencast = async (): Promise<void> => {
if (!this.client) { // Set up screencast frame handler
logger.log('error', 'client is not initialized'); this.client.on('Page.screencastFrame', async ({ data, sessionId }) => {
logger.log('error', 'Screencast stop failed'); try {
} else { const buffer = Buffer.from(data, 'base64');
await this.client.send('Page.stopScreencast'); await this.emitScreenshot(buffer);
logger.log('info', `Browser stopped with screencasting.`); await this.client?.send('Page.screencastFrameAck', { sessionId });
} catch (error) {
logger.error('Screencast frame processing failed:', error);
}
});
logger.info('Screencast started successfully');
} catch (error) {
logger.error('Failed to start screencast:', error);
} }
}; }
private async stopScreencast(): Promise<void> {
if (!this.client) {
logger.error('Client is not initialized');
return;
}
try {
await this.client.send('Page.stopScreencast');
this.screenshotQueue = [];
this.isProcessingScreenshot = false;
logger.info('Screencast stopped successfully');
} catch (error) {
logger.error('Failed to stop screencast:', error);
}
}
/** /**
* Helper for emitting the screenshot of browser's active page through websocket. * Helper for emitting the screenshot of browser's active page through websocket.
* @param payload the screenshot binary data * @param payload the screenshot binary data
* @returns void * @returns void
*/ */
private emitScreenshot = (payload: any): void => { private emitScreenshot = async (payload: Buffer): Promise<void> => {
const dataWithMimeType = ('data:image/jpeg;base64,').concat(payload); if (this.isProcessingScreenshot) {
this.socket.emit('screencast', dataWithMimeType); if (this.screenshotQueue.length < SCREENCAST_CONFIG.maxQueueSize) {
logger.log('debug', `Screenshot emitted`); this.screenshotQueue.push(payload);
}
return;
}
this.isProcessingScreenshot = true;
try {
const optimizedScreenshot = await this.optimizeScreenshot(payload);
const base64Data = optimizedScreenshot.toString('base64');
const dataWithMimeType = `data:image/jpeg;base64,${base64Data}`;
this.socket.emit('screencast', dataWithMimeType);
logger.debug('Screenshot emitted');
} catch (error) {
logger.error('Screenshot emission failed:', error);
} finally {
this.isProcessingScreenshot = false;
if (this.screenshotQueue.length > 0) {
const nextScreenshot = this.screenshotQueue.shift();
if (nextScreenshot) {
setTimeout(() => this.emitScreenshot(nextScreenshot), 1000 / SCREENCAST_CONFIG.targetFPS);
}
}
}
}; };
} }

View File

@@ -384,7 +384,7 @@ router.get(
httpOnly: false, httpOnly: false,
maxAge: 60000, maxAge: 60000,
}); });
res.redirect(process.env.PUBLIC_URL as string || "http://localhost:5173"); res.redirect(`${process.env.PUBLIC_URL}/robots/${robotId}/integrate` as string || `http://localhost:5173/robots/${robotId}/integrate`);
} catch (error: any) { } catch (error: any) {
res.status(500).json({ message: `Google OAuth error: ${error.message}` }); res.status(500).json({ message: `Google OAuth error: ${error.message}` });
} }

View File

@@ -6,14 +6,26 @@ import { InterpreterSettings } from "../../types";
import { decrypt } from "../../utils/auth"; import { decrypt } from "../../utils/auth";
/** /**
* Decrypts any encrypted inputs in the workflow. * Decrypts any encrypted inputs in the workflow. If checkLimit is true, it will also handle the limit validation for scrapeList action.
* @param workflow The workflow to decrypt. * @param workflow The workflow to decrypt.
* @param checkLimit If true, it will handle the limit validation for scrapeList action.
*/ */
function decryptWorkflow(workflow: WorkflowFile): WorkflowFile { function processWorkflow(workflow: WorkflowFile, checkLimit: boolean = false): WorkflowFile {
const decryptedWorkflow = JSON.parse(JSON.stringify(workflow)) as WorkflowFile; const processedWorkflow = JSON.parse(JSON.stringify(workflow)) as WorkflowFile;
decryptedWorkflow.workflow.forEach((pair) => { processedWorkflow.workflow.forEach((pair) => {
pair.what.forEach((action) => { pair.what.forEach((action) => {
// Handle limit validation for scrapeList action
if (action.action === 'scrapeList' && checkLimit && Array.isArray(action.args) && action.args.length > 0) {
const scrapeConfig = action.args[0];
if (scrapeConfig && typeof scrapeConfig === 'object' && 'limit' in scrapeConfig) {
if (typeof scrapeConfig.limit === 'number' && scrapeConfig.limit > 5) {
scrapeConfig.limit = 5;
}
}
}
// Handle decryption for type and press actions
if ((action.action === 'type' || action.action === 'press') && Array.isArray(action.args) && action.args.length > 1) { if ((action.action === 'type' || action.action === 'press') && Array.isArray(action.args) && action.args.length > 1) {
try { try {
const encryptedValue = action.args[1]; const encryptedValue = action.args[1];
@@ -33,7 +45,7 @@ function decryptWorkflow(workflow: WorkflowFile): WorkflowFile {
}); });
}); });
return decryptedWorkflow; return processedWorkflow;
} }
/** /**
@@ -156,7 +168,7 @@ export class WorkflowInterpreter {
const params = settings.params ? settings.params : null; const params = settings.params ? settings.params : null;
delete settings.params; delete settings.params;
const decryptedWorkflow = decryptWorkflow(workflow); const processedWorkflow = processWorkflow(workflow, true);
const options = { const options = {
...settings, ...settings,
@@ -178,7 +190,7 @@ export class WorkflowInterpreter {
} }
} }
const interpreter = new Interpreter(decryptedWorkflow, options); const interpreter = new Interpreter(processedWorkflow, options);
this.interpreter = interpreter; this.interpreter = interpreter;
interpreter.on('flag', async (page, resume) => { interpreter.on('flag', async (page, resume) => {
@@ -253,7 +265,7 @@ export class WorkflowInterpreter {
const params = settings.params ? settings.params : null; const params = settings.params ? settings.params : null;
delete settings.params; delete settings.params;
const decryptedWorkflow = decryptWorkflow(workflow); const processedWorkflow = processWorkflow(workflow);
const options = { const options = {
...settings, ...settings,
@@ -277,7 +289,7 @@ export class WorkflowInterpreter {
} }
} }
const interpreter = new Interpreter(decryptedWorkflow, options); const interpreter = new Interpreter(processedWorkflow, options);
this.interpreter = interpreter; this.interpreter = interpreter;
interpreter.on('flag', async (page, resume) => { interpreter.on('flag', async (page, resume) => {

View File

@@ -175,7 +175,17 @@ export const getElementInformation = async (
info.innerText = targetElement.textContent ?? ''; info.innerText = targetElement.textContent ?? '';
} else if (targetElement.tagName === 'IMG') { } else if (targetElement.tagName === 'IMG') {
info.imageUrl = (targetElement as HTMLImageElement).src; info.imageUrl = (targetElement as HTMLImageElement).src;
} else { } else if (targetElement?.tagName === 'SELECT') {
const selectElement = targetElement as HTMLSelectElement;
info.innerText = selectElement.options[selectElement.selectedIndex]?.text ?? '';
info.attributes = {
...info.attributes,
selectedValue: selectElement.value,
};
} else if (targetElement?.tagName === 'INPUT' && (targetElement as HTMLInputElement).type === 'time' || (targetElement as HTMLInputElement).type === 'date') {
info.innerText = (targetElement as HTMLInputElement).value;
}
else {
info.hasOnlyText = targetElement.children.length === 0 && info.hasOnlyText = targetElement.children.length === 0 &&
(targetElement.textContent !== null && (targetElement.textContent !== null &&
targetElement.textContent.trim().length > 0); targetElement.textContent.trim().length > 0);

View File

@@ -4,6 +4,7 @@ import { ThemeProvider, createTheme } from "@mui/material/styles";
import { GlobalInfoProvider } from "./context/globalInfo"; import { GlobalInfoProvider } from "./context/globalInfo";
import { PageWrapper } from "./pages/PageWrappper"; import { PageWrapper } from "./pages/PageWrappper";
import i18n from "./i18n"; import i18n from "./i18n";
import ThemeModeProvider from './context/theme-provider';
const theme = createTheme({ const theme = createTheme({
@@ -85,15 +86,23 @@ const theme = createTheme({
function App() { function App() {
return ( return (
<ThemeProvider theme={theme}> <ThemeModeProvider>
<GlobalInfoProvider>
<Routes>
<Route path="/*" element={<PageWrapper />} />
</Routes>
</GlobalInfoProvider>
</ThemeModeProvider>
// <ThemeProvider theme={theme}>
<GlobalInfoProvider> // <GlobalInfoProvider>
<Routes> // <Routes>
<Route path="/*" element={<PageWrapper />} /> // <Route path="/*" element={<PageWrapper />} />
</Routes> // </Routes>
</GlobalInfoProvider> // </GlobalInfoProvider>
</ThemeProvider> // </ThemeProvider>
); );
} }

View File

@@ -1,7 +1,7 @@
import { default as axios } from "axios"; import { default as axios } from "axios";
import { WorkflowFile } from "maxun-core"; import { WorkflowFile } from "maxun-core";
import { RunSettings } from "../components/molecules/RunSettings"; import { RunSettings } from "../components/run/RunSettings";
import { ScheduleSettings } from "../components/molecules/ScheduleSettings"; import { ScheduleSettings } from "../components/robot/ScheduleSettings";
import { CreateRunResponse, ScheduleRunResponse } from "../pages/MainPage"; import { CreateRunResponse, ScheduleRunResponse } from "../pages/MainPage";
import { apiUrl } from "../apiConfig"; import { apiUrl } from "../apiConfig";

View File

@@ -5,19 +5,24 @@ import { useActionContext } from '../../context/browserActions';
import MaxunLogo from "../../assets/maxunlogo.png"; import MaxunLogo from "../../assets/maxunlogo.png";
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const CustomBoxContainer = styled.div` interface CustomBoxContainerProps {
isDarkMode: boolean;
}
const CustomBoxContainer = styled.div<CustomBoxContainerProps>`
position: relative; position: relative;
min-width: 250px; min-width: 250px;
width: auto; width: auto;
min-height: 100px; min-height: 100px;
height: auto; height: auto;
// border: 2px solid #ff00c3;
border-radius: 5px; border-radius: 5px;
background-color: white; background-color: ${({ isDarkMode }) => (isDarkMode ? '#313438' : 'white')};
color: ${({ isDarkMode }) => (isDarkMode ? 'white' : 'black')};
margin: 80px 13px 25px 13px; margin: 80px 13px 25px 13px;
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
`; `;
const Triangle = styled.div` const Triangle = styled.div<CustomBoxContainerProps>`
position: absolute; position: absolute;
top: -15px; top: -15px;
left: 50%; left: 50%;
@@ -26,7 +31,7 @@ const Triangle = styled.div`
height: 0; height: 0;
border-left: 20px solid transparent; border-left: 20px solid transparent;
border-right: 20px solid transparent; border-right: 20px solid transparent;
border-bottom: 20px solid white; border-bottom: 20px solid ${({ isDarkMode }) => (isDarkMode ? '#313438' : 'white')};
`; `;
const Logo = styled.img` const Logo = styled.img`
@@ -44,7 +49,8 @@ const Content = styled.div`
text-align: left; text-align: left;
`; `;
const ActionDescriptionBox = () => {
const ActionDescriptionBox = ({ isDarkMode }: { isDarkMode: boolean }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { getText, getScreenshot, getList, captureStage } = useActionContext() as { const { getText, getScreenshot, getList, captureStage } = useActionContext() as {
getText: boolean; getText: boolean;
@@ -93,9 +99,19 @@ const ActionDescriptionBox = () => {
<Checkbox <Checkbox
checked={index < currentStageIndex} checked={index < currentStageIndex}
disabled disabled
sx={{
color: isDarkMode ? 'white' : 'default',
'&.Mui-checked': {
color: isDarkMode ? '#90caf9' : '#1976d2',
},
}}
/> />
} }
label={<Typography variant="body2" gutterBottom>{text}</Typography>} label={
<Typography variant="body2" gutterBottom color={isDarkMode ? 'white' : 'textPrimary'}>
{text}
</Typography>
}
/> />
))} ))}
</Box> </Box>
@@ -112,9 +128,9 @@ const ActionDescriptionBox = () => {
}; };
return ( return (
<CustomBoxContainer> <CustomBoxContainer isDarkMode={isDarkMode}>
<Logo src={MaxunLogo} alt='maxun_logo' /> <Logo src={MaxunLogo} alt="Maxun Logo" />
<Triangle /> <Triangle isDarkMode={isDarkMode} />
<Content> <Content>
{renderActionDescription()} {renderActionDescription()}
</Content> </Content>

View File

@@ -1,16 +1,15 @@
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import styled from "styled-components"; import styled from "styled-components";
import { Button } from "@mui/material"; import { Button } from "@mui/material";
//import { ActionDescription } from "../organisms/RightSidePanel";
import * as Settings from "./action-settings"; import * as Settings from "./action-settings";
import { useSocketStore } from "../../context/socket"; import { useSocketStore } from "../../context/socket";
interface ActionSettingsProps { interface ActionSettingsProps {
action: string; action: string;
darkMode?: boolean;
} }
export const ActionSettings = ({ action }: ActionSettingsProps) => { export const ActionSettings = ({ action, darkMode = false }: ActionSettingsProps) => {
const settingsRef = useRef<{ getSettings: () => object }>(null); const settingsRef = useRef<{ getSettings: () => object }>(null);
const { socket } = useSocketStore(); const { socket } = useSocketStore();
@@ -20,30 +19,27 @@ export const ActionSettings = ({ action }: ActionSettingsProps) => {
return <Settings.ScreenshotSettings ref={settingsRef} />; return <Settings.ScreenshotSettings ref={settingsRef} />;
case 'scroll': case 'scroll':
return <Settings.ScrollSettings ref={settingsRef} />; return <Settings.ScrollSettings ref={settingsRef} />;
case 'scrape': case 'scrape':
return <Settings.ScrapeSettings ref={settingsRef} />; return <Settings.ScrapeSettings ref={settingsRef} />;
case 'scrapeSchema': case 'scrapeSchema':
return <Settings.ScrapeSchemaSettings ref={settingsRef} />; return <Settings.ScrapeSchemaSettings ref={settingsRef} />;
default: default:
return null; return null;
} }
} };
const handleSubmit = (event: React.SyntheticEvent) => { const handleSubmit = (event: React.SyntheticEvent) => {
event.preventDefault(); event.preventDefault();
//get the data from settings
const settings = settingsRef.current?.getSettings(); const settings = settingsRef.current?.getSettings();
//Send notification to the server and generate the pair
socket?.emit(`action`, { socket?.emit(`action`, {
action, action,
settings settings
}); });
} };
return ( return (
<div> <div>
{/* <ActionDescription>Action settings:</ActionDescription> */} <ActionSettingsWrapper action={action} darkMode={darkMode}>
<ActionSettingsWrapper action={action}>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<DisplaySettings /> <DisplaySettings />
<Button <Button
@@ -64,10 +60,13 @@ export const ActionSettings = ({ action }: ActionSettingsProps) => {
); );
}; };
const ActionSettingsWrapper = styled.div<{ action: string }>` // Ensure that the Wrapper accepts the darkMode prop for styling adjustments.
const ActionSettingsWrapper = styled.div<{ action: string; darkMode: boolean }>`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: ${({ action }) => action === 'script' ? 'stretch' : 'center'};; align-items: ${({ action }) => (action === 'script' ? 'stretch' : 'center')};
justify-content: center; justify-content: center;
margin-top: 20px; margin-top: 20px;
background-color: ${({ darkMode }) => (darkMode ? '#1E1E1E' : 'white')};
color: ${({ darkMode }) => (darkMode ? 'white' : 'black')};
`; `;

View File

@@ -1,6 +1,6 @@
import React, { forwardRef, useImperativeHandle } from 'react'; import React, { forwardRef, useImperativeHandle } from 'react';
import { Stack, TextField } from "@mui/material"; import { Stack, TextField } from "@mui/material";
import { WarningText } from '../../atoms/texts'; import { WarningText } from '../../ui/texts';
import InfoIcon from "@mui/icons-material/Info"; import InfoIcon from "@mui/icons-material/Info";
export const ScrapeSettings = forwardRef((props, ref) => { export const ScrapeSettings = forwardRef((props, ref) => {

View File

@@ -1,7 +1,7 @@
import React, { forwardRef, useCallback, useImperativeHandle, useRef } from 'react'; import React, { forwardRef, useImperativeHandle, useRef } from 'react';
import { WarningText } from "../../atoms/texts"; import { WarningText } from "../../ui/texts";
import InfoIcon from "@mui/icons-material/Info"; import InfoIcon from "@mui/icons-material/Info";
import { KeyValueForm } from "../KeyValueForm"; import { KeyValueForm } from "../../recorder/KeyValueForm";
export const ScrapeSchemaSettings = forwardRef((props, ref) => { export const ScrapeSchemaSettings = forwardRef((props, ref) => {
const keyValueFormRef = useRef<{ getObject: () => object }>(null); const keyValueFormRef = useRef<{ getObject: () => object }>(null);

View File

@@ -1,9 +1,9 @@
import React, { forwardRef, useImperativeHandle } from 'react'; import React, { forwardRef, useImperativeHandle } from 'react';
import { InputLabel, MenuItem, TextField, Select, FormControl } from "@mui/material"; import { MenuItem, TextField } from "@mui/material";
import { ScreenshotSettings as Settings } from "../../../shared/types"; import { ScreenshotSettings as Settings } from "../../../shared/types";
import styled from "styled-components"; import styled from "styled-components";
import { SelectChangeEvent } from "@mui/material/Select/Select"; import { SelectChangeEvent } from "@mui/material/Select/Select";
import { Dropdown } from "../../atoms/DropdownMui"; import { Dropdown } from "../../ui/DropdownMui";
export const ScreenshotSettings = forwardRef((props, ref) => { export const ScreenshotSettings = forwardRef((props, ref) => {
const [settings, setSettings] = React.useState<Settings>({}); const [settings, setSettings] = React.useState<Settings>({});

View File

@@ -1,17 +1,13 @@
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import styled from "styled-components"; import styled from "styled-components";
import BrowserNavBar from "../molecules/BrowserNavBar"; import BrowserNavBar from "./BrowserNavBar";
import { BrowserWindow } from "./BrowserWindow"; import { BrowserWindow } from "./BrowserWindow";
import { useBrowserDimensionsStore } from "../../context/browserDimensions"; import { useBrowserDimensionsStore } from "../../context/browserDimensions";
import { BrowserTabs } from "../molecules/BrowserTabs"; import { BrowserTabs } from "./BrowserTabs";
import { useSocketStore } from "../../context/socket"; import { useSocketStore } from "../../context/socket";
import { import {
getCurrentTabs, getCurrentTabs,
getCurrentUrl,
interpretCurrentRecording,
} from "../../api/recording"; } from "../../api/recording";
import { Box } from "@mui/material";
import { InterpretationLog } from "../molecules/InterpretationLog";
// TODO: Tab !show currentUrl after recordingUrl global state // TODO: Tab !show currentUrl after recordingUrl global state
export const BrowserContent = () => { export const BrowserContent = () => {
@@ -152,6 +148,7 @@ export const BrowserContent = () => {
// todo: use width from browser dimension once fixed // todo: use width from browser dimension once fixed
browserWidth={900} browserWidth={900}
handleUrlChanged={handleUrlChanged} handleUrlChanged={handleUrlChanged}
/> />
<BrowserWindow /> <BrowserWindow />
</div> </div>

View File

@@ -1,27 +1,34 @@
import type { import type { FC } from 'react';
FC,
} from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import ReplayIcon from '@mui/icons-material/Replay'; import ReplayIcon from '@mui/icons-material/Replay';
import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import { NavBarButton } from '../ui/buttons/buttons';
import { NavBarButton } from '../atoms/buttons/buttons';
import { UrlForm } from './UrlForm'; import { UrlForm } from './UrlForm';
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useSocketStore } from "../../context/socket"; import { useSocketStore } from "../../context/socket";
import { getCurrentUrl } from "../../api/recording"; import { getCurrentUrl } from "../../api/recording";
import { useGlobalInfoStore } from '../../context/globalInfo'; import { useGlobalInfoStore } from '../../context/globalInfo';
import { useThemeMode } from '../../context/theme-provider';
const StyledNavBar = styled.div<{ browserWidth: number }>` const StyledNavBar = styled.div<{ browserWidth: number; isDarkMode: boolean }>`
display: flex; display: flex;
padding: 12px 0px; padding: 12px 0px;
background-color: #f6f6f6; background-color: ${({ isDarkMode }) => (isDarkMode ? '#2C2F33' : '#f6f6f6')};
width: ${({ browserWidth }) => browserWidth}px; width: ${({ browserWidth }) => browserWidth}px;
border-radius: 0px 5px 0px 0px; border-radius: 0px 5px 0px 0px;
`; `;
const IconButton = styled(NavBarButton) <{ mode: string }>`
background-color: ${({ mode }) => (mode === 'dark' ? '#2C2F33' : '#f6f6f6')};
transition: background-color 0.3s ease, transform 0.1s ease;
color: ${({ mode }) => (mode === 'dark' ? '#FFFFFF' : '#333')};
cursor: pointer;
&:hover {
background-color: ${({ mode }) => (mode === 'dark' ? '#586069' : '#D0D0D0')};
}
`;
interface NavBarProps { interface NavBarProps {
browserWidth: number; browserWidth: number;
handleUrlChanged: (url: string) => void; handleUrlChanged: (url: string) => void;
@@ -31,6 +38,7 @@ const BrowserNavBar: FC<NavBarProps> = ({
browserWidth, browserWidth,
handleUrlChanged, handleUrlChanged,
}) => { }) => {
const isDarkMode = useThemeMode().darkMode;
const { socket } = useSocketStore(); const { socket } = useSocketStore();
const { recordingUrl, setRecordingUrl } = useGlobalInfoStore(); const { recordingUrl, setRecordingUrl } = useGlobalInfoStore();
@@ -67,7 +75,7 @@ const BrowserNavBar: FC<NavBarProps> = ({
socket.off('urlChanged', handleCurrentUrlChange); socket.off('urlChanged', handleCurrentUrlChange);
} }
} }
}, [socket, handleCurrentUrlChange]) }, [socket, handleCurrentUrlChange]);
const addAddress = (address: string) => { const addAddress = (address: string) => {
if (socket) { if (socket) {
@@ -78,38 +86,41 @@ const BrowserNavBar: FC<NavBarProps> = ({
}; };
return ( return (
<StyledNavBar browserWidth={900}> <StyledNavBar browserWidth={browserWidth} isDarkMode={isDarkMode}>
<NavBarButton <IconButton
type="button" type="button"
onClick={() => { onClick={() => {
socket?.emit('input:back'); socket?.emit('input:back');
}} }}
disabled={false} disabled={false}
mode={isDarkMode ? 'dark' : 'light'}
> >
<ArrowBackIcon /> <ArrowBackIcon />
</NavBarButton> </IconButton>
<NavBarButton <IconButton
type="button" type="button"
onClick={() => { onClick={() => {
socket?.emit('input:forward'); socket?.emit('input:forward');
}} }}
disabled={false} disabled={false}
mode={isDarkMode ? 'dark' : 'light'}
> >
<ArrowForwardIcon /> <ArrowForwardIcon />
</NavBarButton> </IconButton>
<NavBarButton <IconButton
type="button" type="button"
onClick={() => { onClick={() => {
if (socket) { if (socket) {
handleRefresh() handleRefresh();
} }
}} }}
disabled={false} disabled={false}
mode={isDarkMode ? 'dark' : 'light'}
> >
<ReplayIcon /> <ReplayIcon />
</NavBarButton> </IconButton>
<UrlForm <UrlForm
currentAddress={recordingUrl} currentAddress={recordingUrl}

View File

@@ -1,10 +1,10 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import { Grid, Button, Box, Typography } from '@mui/material'; import { Grid, Button, Box, Typography } from '@mui/material';
import { SaveRecording } from "./SaveRecording"; import { SaveRecording } from "../recorder/SaveRecording";
import { useGlobalInfoStore } from '../../context/globalInfo'; import { useGlobalInfoStore } from '../../context/globalInfo';
import { stopRecording } from "../../api/recording"; import { stopRecording } from "../../api/recording";
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { GenericModal } from "../atoms/GenericModal"; import { GenericModal } from "../ui/GenericModal";
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const BrowserRecordingSave = () => { const BrowserRecordingSave = () => {
@@ -31,14 +31,26 @@ const BrowserRecordingSave = () => {
position: 'absolute', position: 'absolute',
background: '#ff00c3', background: '#ff00c3',
border: 'none', border: 'none',
borderRadius: '5px', borderRadius: '0px 0px 8px 8px',
padding: '7.5px', padding: '7.5px',
width: 'calc(100% - 20px)', width: 'calc(100% - 20px)',
overflow: 'hidden', overflow: 'hidden',
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
height: "48px"
}}> }}>
<Button onClick={() => setOpenModal(true)} variant="outlined" style={{ marginLeft: "25px" }} size="small" color="error"> <Button
onClick={() => setOpenModal(true)}
variant="outlined"
color="error"
sx={{
marginLeft: '25px',
color: 'red !important',
borderColor: 'red !important',
backgroundColor: 'whitesmoke !important',
}}
size="small"
>
{t('right_panel.buttons.discard')} {t('right_panel.buttons.discard')}
</Button> </Button>
<GenericModal isOpen={openModal} onClose={() => setOpenModal(false)} modalStyle={modalStyle}> <GenericModal isOpen={openModal} onClose={() => setOpenModal(false)} modalStyle={modalStyle}>
@@ -48,7 +60,14 @@ const BrowserRecordingSave = () => {
<Button onClick={goToMainMenu} variant="contained" color="error"> <Button onClick={goToMainMenu} variant="contained" color="error">
{t('right_panel.buttons.discard')} {t('right_panel.buttons.discard')}
</Button> </Button>
<Button onClick={() => setOpenModal(false)} variant="outlined"> <Button
onClick={() => setOpenModal(false)}
variant="outlined"
sx={{
color: '#ff00c3 !important',
borderColor: '#ff00c3 !important',
backgroundColor: 'whitesmoke !important',
}} >
{t('right_panel.buttons.cancel')} {t('right_panel.buttons.cancel')}
</Button> </Button>
</Box> </Box>

View File

@@ -1,8 +1,8 @@
import * as React from 'react'; import * as React from 'react';
import { Box, IconButton, Tab, Tabs } from "@mui/material"; import { Box, IconButton, Tab, Tabs } from "@mui/material";
import { AddButton } from "../atoms/buttons/AddButton";
import { useBrowserDimensionsStore } from "../../context/browserDimensions"; import { useBrowserDimensionsStore } from "../../context/browserDimensions";
import { Close } from "@mui/icons-material"; import { Close } from "@mui/icons-material";
import { useThemeMode } from '../../context/theme-provider';
interface BrowserTabsProp { interface BrowserTabsProp {
tabs: string[], tabs: string[],
@@ -28,15 +28,16 @@ export const BrowserTabs = (
handleChangeIndex(newValue); handleChangeIndex(newValue);
} }
}; };
const isDarkMode = useThemeMode().darkMode;
return ( return (
<Box sx={{ <Box sx={{
width: 800, width: 800, // Fixed width
display: 'flex', display: 'flex',
overflow: 'auto', overflow: 'auto',
alignItems: 'center', alignItems: 'center',
}}> }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}> <Box sx={{ borderBottom: 1, borderColor: 'divider' }}> {/* Synced border color */}
<Tabs <Tabs
value={tabIndex} value={tabIndex}
onChange={handleChange} onChange={handleChange}
@@ -48,7 +49,11 @@ export const BrowserTabs = (
id={`tab-${index}`} id={`tab-${index}`}
sx={{ sx={{
background: 'white', background: 'white',
borderRadius: '5px 5px 0px 0px' borderRadius: '5px 5px 0px 0px',
'&.Mui-selected': {
backgroundColor: ` ${isDarkMode ? "#2a2a2a" : "#f5f5f5"}`, // Synced selected tab color
color: '#ff00c3', // Slightly lighter text when selected
},
}} }}
icon={<CloseButton closeTab={() => { icon={<CloseButton closeTab={() => {
tabWasClosed = true; tabWasClosed = true;
@@ -60,8 +65,7 @@ export const BrowserTabs = (
if (!tabWasClosed) { if (!tabWasClosed) {
handleTabChange(index) handleTabChange(index)
} }
} }}
}
label={tab} label={tab}
/> />
); );

View File

@@ -1,9 +1,9 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { useSocketStore } from '../../context/socket'; import { useSocketStore } from '../../context/socket';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import Canvas from "../atoms/canvas"; import Canvas from "../recorder/canvas";
import { Highlighter } from "../atoms/Highlighter"; import { Highlighter } from "../recorder/Highlighter";
import { GenericModal } from '../atoms/GenericModal'; import { GenericModal } from '../ui/GenericModal';
import { useActionContext } from '../../context/browserActions'; import { useActionContext } from '../../context/browserActions';
import { useBrowserSteps, TextStep } from '../../context/browserSteps'; import { useBrowserSteps, TextStep } from '../../context/browserSteps';
import { useGlobalInfoStore } from '../../context/globalInfo'; import { useGlobalInfoStore } from '../../context/globalInfo';
@@ -141,9 +141,9 @@ export const BrowserWindow = () => {
} else if (data.elementInfo?.isIframeContent && data.childSelectors) { } else if (data.elementInfo?.isIframeContent && data.childSelectors) {
// Handle pure iframe elements - similar to previous shadow DOM logic but using iframe syntax // Handle pure iframe elements - similar to previous shadow DOM logic but using iframe syntax
// Check if the selector matches any iframe child selectors // Check if the selector matches any iframe child selectors
const isIframeChild = data.childSelectors.some(childSelector => const isIframeChild = data.childSelectors.some(childSelector =>
data.selector.includes(':>>') && // Iframe uses :>> for traversal data.selector.includes(':>>') && // Iframe uses :>> for traversal
childSelector.split(':>>').some(part => childSelector.split(':>>').some(part =>
data.selector.includes(part.trim()) data.selector.includes(part.trim())
) )
); );
@@ -152,9 +152,9 @@ export const BrowserWindow = () => {
// Handle mixed DOM cases with iframes // Handle mixed DOM cases with iframes
// Split the selector into parts and check each against child selectors // Split the selector into parts and check each against child selectors
const selectorParts = data.selector.split(':>>').map(part => part.trim()); const selectorParts = data.selector.split(':>>').map(part => part.trim());
const isValidMixedSelector = selectorParts.some(part => const isValidMixedSelector = selectorParts.some(part =>
// We know data.childSelectors is defined due to hasValidChildSelectors check // We know data.childSelectors is defined due to hasValidChildSelectors check
data.childSelectors!.some(childSelector => data.childSelectors!.some(childSelector =>
childSelector.includes(part) childSelector.includes(part)
) )
); );
@@ -162,36 +162,36 @@ export const BrowserWindow = () => {
} else if (data.elementInfo?.isShadowRoot && data.childSelectors) { } else if (data.elementInfo?.isShadowRoot && data.childSelectors) {
// New case: Handle pure Shadow DOM elements // New case: Handle pure Shadow DOM elements
// Check if the selector matches any shadow root child selectors // Check if the selector matches any shadow root child selectors
const isShadowChild = data.childSelectors.some(childSelector => const isShadowChild = data.childSelectors.some(childSelector =>
data.selector.includes('>>') && // Shadow DOM uses >> for piercing data.selector.includes('>>') && // Shadow DOM uses >> for piercing
childSelector.split('>>').some(part => childSelector.split('>>').some(part =>
data.selector.includes(part.trim()) data.selector.includes(part.trim())
) )
); );
setHighlighterData(isShadowChild ? data : null); setHighlighterData(isShadowChild ? data : null);
} else if (data.selector.includes('>>') && hasValidChildSelectors) { } else if (data.selector.includes('>>') && hasValidChildSelectors) {
// New case: Handle mixed DOM cases // New case: Handle mixed DOM cases
// Split the selector into parts and check each against child selectors // Split the selector into parts and check each against child selectors
const selectorParts = data.selector.split('>>').map(part => part.trim()); const selectorParts = data.selector.split('>>').map(part => part.trim());
const isValidMixedSelector = selectorParts.some(part => const isValidMixedSelector = selectorParts.some(part =>
// Now we know data.childSelectors is defined // Now we know data.childSelectors is defined
data.childSelectors!.some(childSelector => data.childSelectors!.some(childSelector =>
childSelector.includes(part) childSelector.includes(part)
) )
); );
setHighlighterData(isValidMixedSelector ? data : null); setHighlighterData(isValidMixedSelector ? data : null);
} else { } else {
// if !valid child in normal mode, clear the highlighter // if !valid child in normal mode, clear the highlighter
setHighlighterData(null); setHighlighterData(null);
} }
} else { } else {
// Set highlighterData for the initial listSelector selection // Set highlighterData for the initial listSelector selection
setHighlighterData(data); setHighlighterData(data);
} }
} else { } else {
// For non-list steps // For non-list steps
setHighlighterData(data); setHighlighterData(data);
} }
}, [highlighterData, getList, socket, listSelector, paginationMode, paginationType, captureStage]); }, [highlighterData, getList, socket, listSelector, paginationMode, paginationType, captureStage]);
@@ -379,7 +379,6 @@ export const BrowserWindow = () => {
} }
}, [paginationMode, resetPaginationSelector]); }, [paginationMode, resetPaginationSelector]);
return ( return (
<div onClick={handleClick} style={{ width: '900px' }} id="browser-window"> <div onClick={handleClick} style={{ width: '900px' }} id="browser-window">
{ {
@@ -405,6 +404,11 @@ export const BrowserWindow = () => {
overflow: 'hidden', overflow: 'hidden',
padding: '5px 10px', padding: '5px 10px',
}} }}
sx={{
color: '#ff00c3 !important',
borderColor: '#ff00c3 !important',
backgroundColor: 'whitesmoke !important',
}}
> >
<span style={{ <span style={{
display: 'block', display: 'block',

View File

@@ -1,8 +1,8 @@
import React, { useState, useEffect, useCallback, useRef } from 'react'; import React, { useState, useEffect, useCallback, useRef } from 'react';
import type { SyntheticEvent } from 'react'; import type { SyntheticEvent } from 'react';
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight'; import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
import { NavBarForm, NavBarInput } from "../atoms/form"; import { NavBarForm, NavBarInput } from "../ui/form";
import { UrlFormButton } from "../atoms/buttons/buttons"; import { UrlFormButton } from "../ui/buttons/buttons";
import { useSocketStore } from '../../context/socket'; import { useSocketStore } from '../../context/socket';
import { Socket } from "socket.io-client"; import { Socket } from "socket.io-client";

View File

@@ -1,41 +1,59 @@
import * as React from 'react'; import React from 'react';
import Tabs from '@mui/material/Tabs'; import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab'; import Tab from '@mui/material/Tab';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import { Paper, Button } from "@mui/material"; import { useNavigate } from 'react-router-dom';
import { AutoAwesome, FormatListBulleted, VpnKey, Usb, CloudQueue, Code } from "@mui/icons-material"; import { Paper, Button, useTheme } from "@mui/material";
import { AutoAwesome, FormatListBulleted, VpnKey, Usb, Article, CloudQueue, Code, } from "@mui/icons-material";
import { apiUrl } from "../../apiConfig"; import { apiUrl } from "../../apiConfig";
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import i18n from '../../i18n'; import i18n from '../../i18n';
interface MainMenuProps { interface MainMenuProps {
value: string; value: string;
handleChangeContent: (newValue: string) => void; handleChangeContent: (newValue: string) => void;
} }
export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenuProps) => { export const MainMenu = ({ value = 'robots', handleChangeContent }: MainMenuProps) => {
const {t} = useTranslation(); const theme = useTheme();
const { t } = useTranslation();
const navigate = useNavigate();
const handleChange = (event: React.SyntheticEvent, newValue: string) => { const handleChange = (event: React.SyntheticEvent, newValue: string) => {
navigate(`/${newValue}`);
handleChangeContent(newValue); handleChangeContent(newValue);
}; };
// Define colors based on theme mode
const defaultcolor = theme.palette.mode === 'light' ? 'black' : 'white';
const buttonStyles = {
justifyContent: 'flex-start',
textAlign: 'left',
fontSize: 'medium',
padding: '6px 16px 6px 22px',
minHeight: '48px',
minWidth: '100%',
display: 'flex',
alignItems: 'center',
textTransform: 'none',
color: theme.palette.mode === 'light' ? '#6C6C6C' : 'inherit',
};
return ( return (
<Paper <Paper
sx={{ sx={{
height: 'auto', height: 'auto',
width: '250px', width: '250px',
backgroundColor: 'white', backgroundColor: theme.palette.background.paper,
paddingTop: '0.5rem', paddingTop: '0.5rem',
color: defaultcolor,
}} }}
variant="outlined" variant="outlined"
square square
> >
<Box sx={{ <Box sx={{ width: '100%', paddingBottom: '1rem' }}>
width: '100%',
paddingBottom: '1rem',
}}>
<Tabs <Tabs
value={value} value={value}
onChange={handleChange} onChange={handleChange}
@@ -50,7 +68,7 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
textAlign: 'left', textAlign: 'left',
fontSize: 'medium', fontSize: 'medium',
}} }}
value="recordings" value="robots"
label={t('mainmenu.recordings')} label={t('mainmenu.recordings')}
icon={<AutoAwesome />} icon={<AutoAwesome />}
iconPosition="start" iconPosition="start"
@@ -101,17 +119,4 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
</Box> </Box>
</Paper> </Paper>
); );
}
const buttonStyles = {
justifyContent: 'flex-start',
textAlign: 'left',
fontSize: 'medium',
padding: '6px 16px 6px 22px',
minHeight: '48px',
minWidth: '100%',
display: 'flex',
alignItems: 'center',
textTransform: 'none',
color: '#6C6C6C !important',
}; };

View File

@@ -4,17 +4,17 @@ import axios from 'axios';
import styled from "styled-components"; import styled from "styled-components";
import { stopRecording } from "../../api/recording"; import { stopRecording } from "../../api/recording";
import { useGlobalInfoStore } from "../../context/globalInfo"; import { useGlobalInfoStore } from "../../context/globalInfo";
import { IconButton, Menu, MenuItem, Typography, Chip, Button, Modal, Tabs, Tab, Box, Snackbar } from "@mui/material"; import { IconButton, Menu, MenuItem, Typography, Chip, Button, Modal, Tabs, Tab, Box, Snackbar, Tooltip } from "@mui/material";
import { AccountCircle, Logout, Clear, YouTube, X, Update, Close, Language } from "@mui/icons-material"; import { AccountCircle, Logout, Clear, YouTube, X, Update, Close, Language, Brightness7, Brightness4, Description } from "@mui/icons-material";
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { AuthContext } from '../../context/auth'; import { AuthContext } from '../../context/auth';
import { SaveRecording } from '../molecules/SaveRecording'; import { SaveRecording } from '../recorder/SaveRecording';
import DiscordIcon from '../atoms/DiscordIcon'; import DiscordIcon from '../icons/DiscordIcon';
import { apiUrl } from '../../apiConfig'; import { apiUrl } from '../../apiConfig';
import MaxunLogo from "../../assets/maxunlogo.png"; import MaxunLogo from "../../assets/maxunlogo.png";
import { useThemeMode } from '../../context/theme-provider';
import packageJson from "../../../package.json" import packageJson from "../../../package.json"
interface NavBarProps { interface NavBarProps {
recordingName: string; recordingName: string;
isRecording: boolean; isRecording: boolean;
@@ -28,6 +28,7 @@ export const NavBar: React.FC<NavBarProps> = ({
const { state, dispatch } = useContext(AuthContext); const { state, dispatch } = useContext(AuthContext);
const { user } = state; const { user } = state;
const navigate = useNavigate(); const navigate = useNavigate();
const { darkMode, toggleTheme } = useThemeMode();
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
@@ -102,6 +103,22 @@ export const NavBar: React.FC<NavBarProps> = ({
localStorage.setItem("language", lang); localStorage.setItem("language", lang);
}; };
const renderThemeToggle = () => (
<Tooltip title="Toggle Mode">
<IconButton
onClick={toggleTheme}
sx={{
color: darkMode ? '#ffffff' : '#0000008A',
'&:hover': {
color: '#ff00c3'
}
}}
>
{darkMode ? <Brightness7 /> : <Brightness4 />}
</IconButton>
</Tooltip>
);
useEffect(() => { useEffect(() => {
const checkForUpdates = async () => { const checkForUpdates = async () => {
const latestVersion = await fetchLatestVersion(); const latestVersion = await fetchLatestVersion();
@@ -158,13 +175,13 @@ export const NavBar: React.FC<NavBarProps> = ({
}} }}
/> />
)} )}
<NavBarWrapper> <NavBarWrapper mode={darkMode ? 'dark' : 'light'}>
<div style={{ <div style={{
display: 'flex', display: 'flex',
justifyContent: 'flex-start', justifyContent: 'flex-start',
}}> }}>
<img src={MaxunLogo} width={45} height={40} style={{ borderRadius: '5px', margin: '5px 0px 5px 15px' }} /> <img src={MaxunLogo} width={45} height={40} style={{ borderRadius: '5px', margin: '5px 0px 5px 15px' }} />
<div style={{ padding: '11px' }}><ProjectName>{t('navbar.project_name')}</ProjectName></div> <div style={{ padding: '11px' }}><ProjectName mode={darkMode ? 'dark' : 'light'}>{t('navbar.project_name')}</ProjectName></div>
<Chip <Chip
label={`${currentVersion}`} label={`${currentVersion}`}
color="primary" color="primary"
@@ -261,6 +278,11 @@ export const NavBar: React.FC<NavBarProps> = ({
docker-compose down docker-compose down
<br /> <br />
<br /> <br />
# replace existing docker-compose file with new one by copy pasting the code from
<br />
<a href="https://github.com/getmaxun/maxun/blob/develop/docker-compose.yml">Latest Docker Compose</a>
<br />
<br />
# pull latest docker images # pull latest docker images
<br /> <br />
docker-compose pull docker-compose pull
@@ -283,7 +305,6 @@ export const NavBar: React.FC<NavBarProps> = ({
borderRadius: '5px', borderRadius: '5px',
padding: '8px', padding: '8px',
marginRight: '10px', marginRight: '10px',
'&:hover': { backgroundColor: 'white', color: '#ff00c3' }
}}> }}>
<AccountCircle sx={{ marginRight: '5px' }} /> <AccountCircle sx={{ marginRight: '5px' }} />
<Typography variant="body1">{user.email}</Typography> <Typography variant="body1">{user.email}</Typography>
@@ -305,6 +326,11 @@ export const NavBar: React.FC<NavBarProps> = ({
<MenuItem onClick={() => { handleMenuClose(); logout(); }}> <MenuItem onClick={() => { handleMenuClose(); logout(); }}>
<Logout sx={{ marginRight: '5px' }} /> {t('navbar.menu_items.logout')} <Logout sx={{ marginRight: '5px' }} /> {t('navbar.menu_items.logout')}
</MenuItem> </MenuItem>
<MenuItem onClick={() => {
window.open('https://docs.maxun.dev', '_blank');
}}>
<Description sx={{ marginRight: '5px' }} /> Docs
</MenuItem>
<MenuItem onClick={() => { <MenuItem onClick={() => {
window.open('https://discord.gg/5GbPjBUkws', '_blank'); window.open('https://discord.gg/5GbPjBUkws', '_blank');
}}> }}>
@@ -376,8 +402,17 @@ export const NavBar: React.FC<NavBarProps> = ({
> >
Deutsch Deutsch
</MenuItem> </MenuItem>
<MenuItem
onClick={() => {
window.open('https://docs.maxun.dev/development/i18n', '_blank');
handleMenuClose();
}}
>
Add Language
</MenuItem>
</Menu> </Menu>
</Menu> </Menu>
{renderThemeToggle()}
</> </>
) : ( ) : (
<> <>
@@ -397,18 +432,19 @@ export const NavBar: React.FC<NavBarProps> = ({
)} )}
</div> </div>
) : ( ) : (
<><IconButton <NavBarRight>
onClick={handleLangMenuOpen} <IconButton
sx={{ onClick={handleLangMenuOpen}
display: "flex", sx={{
alignItems: "center", display: "flex",
borderRadius: "5px", alignItems: "center",
padding: "8px", borderRadius: "5px",
marginRight: "10px", padding: "8px",
}} marginRight: "8px",
> }}
<Language sx={{ marginRight: '5px' }} /><Typography variant="body1">{t("Language")}</Typography> >
</IconButton> <Language sx={{ marginRight: '5px' }} /><Typography variant="body1">{t("Language")}</Typography>
</IconButton>
<Menu <Menu
anchorEl={langAnchorEl} anchorEl={langAnchorEl}
open={Boolean(langAnchorEl)} open={Boolean(langAnchorEl)}
@@ -462,23 +498,40 @@ export const NavBar: React.FC<NavBarProps> = ({
> >
Deutsch Deutsch
</MenuItem> </MenuItem>
</Menu></> <MenuItem
onClick={() => {
window.open('https://docs.maxun.dev/development/i18n', '_blank');
handleMenuClose();
}}
>
Add Language
</MenuItem>
</Menu>
{renderThemeToggle()}
</NavBarRight>
)} )}
</NavBarWrapper> </NavBarWrapper>
</> </>
); );
}; };
const NavBarWrapper = styled.div` const NavBarWrapper = styled.div<{ mode: 'light' | 'dark' }>`
grid-area: navbar; grid-area: navbar;
background-color: white; background-color: ${({ mode }) => (mode === 'dark' ? '#1e2124' : '#ffffff')};
padding: 5px; padding: 5px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
border-bottom: 1px solid #e0e0e0; border-bottom: 1px solid ${({ mode }) => (mode === 'dark' ? '#333' : '#e0e0e0')};
`; `;
const ProjectName = styled.b` const ProjectName = styled.b<{ mode: 'light' | 'dark' }>`
color: #3f4853; color: ${({ mode }) => (mode === 'dark' ? '#ffffff' : '#3f4853')};
font-size: 1.3em; font-size: 1.3em;
`; `;
const NavBarRight = styled.div`
display: flex;
align-items: center;
justify-content: flex-end;
margin-left: auto;
`;

View File

@@ -14,7 +14,7 @@ export const RecordingIcon = () => {
textIndent: 0, textIndent: 0,
textTransform: 'none', textTransform: 'none',
}} }}
fill="white" fill="black"
d="m82.048,962.36c-0.18271-0.003-0.35147-0.001-0.53125,0.0312-0.69633,0.12662-1.3353,0.54943-1.7812,1.1562l-0.03125-0.0312-0.03125,0.0625-18.031,22.125h-44.438c-2.809,0-5.0938,2.2847-5.0938,5.0938v35.531c0,2.8091,2.2847,5.125,5.0938,5.125h20.562l-1.3125,4.5938h-0.71875c-1.1137,0-2.0312,0.9175-2.0312,2.0312v2.2188c0,1.1137,0.91751,2.0625,2.0312,2.0625h19.938c1.1137,0,2.0312-0.9488,2.0312-2.0625v-2.2188c0-1.1137-0.91751-2.0312-2.0312-2.0312h-0.71875l-1.3438-4.5938h20.438c2.809,0,5.0938-2.3159,5.0938-5.125v-35.531c0-1.7706-0.90663-3.3369-2.2812-4.25l10.531-17.625,0.03125-0.0625c0.84234-1.2783,0.51486-3.0308-0.75-3.9062l-3.0312-2.0938c-0.48208-0.33338-1.0456-0.49073-1.5938-0.5zm-0.21875,1.6875c0.28723-0.0523,0.57635,0.0338,0.84375,0.21875l3.0312,2.0938c0.53421,0.36973,0.65504,1.0569,0.28125,1.5938a0.85008,0.85008,0,0,0,-0.03125,0.0312l-17.906,30.062-9.0938-6.3125,22.094-27.125a0.85008,0.85008,0,0,0,0.03125,-0.0625c0.18694-0.26873,0.46277-0.4477,0.75-0.5zm-64.625,23.344,43.062,0-2.3438,2.9062-40.688,0c-0.0312-0.002-0.06255-0.002-0.09375,0-0.0312-0.002-0.06255-0.002-0.09375,0-0.38644,0.0753-0.69183,0.45007-0.6875,0.84375v34.844c0.003,0.4514,0.42377,0.857,0.875,0.8437h56.781c0.44088,0,0.84048-0.4028,0.84375-0.8437v-34.844c-0.008-0.25538-0.13841-0.50419-0.34375-0.65625l1.5-2.5c0.87419,0.61342,1.4375,1.6512,1.4375,2.8125v35.531c0,1.8967-1.5096,3.4063-3.4062,3.4063h-56.844c-1.8966,0-3.4062-1.5096-3.4062-3.4063v-35.531c0-1.8966,1.5096-3.4062,3.4062-3.4062zm0.875,4.5938,38.469,0-1.0312,1.25,0,0.0312c-0.48971,0.60518-0.64056,1.3922-0.5,2.0312,0.14234,0.64722,0.49536,1.1659,0.84375,1.6562a0.85008,0.85008,0,0,0,0.1875,0.21875l1.2812,0.875c-1.0387,0.79518-2.0706,1.1661-3.2188,1.6562-1.4337,0.61212-3.0045,1.4512-4.3438,3.375-1.1451,1.6448-1.0525,3.5446-0.78125,5.3437,0.27121,1.7991,0.70152,3.5802,0.5625,5.2188a0.85008,0.85008,0,0,0,1.2188,0.8437c1.4928-0.7039,3.3085-0.9361,5.0938-1.3125s3.6049-0.9489,4.75-2.5937c1.34-1.9249,1.5559-3.6628,1.625-5.2188,0.05552-1.2502,0.05447-2.363,0.4375-3.625l1.2812,0.875c1.2744,0.8814,3.0499,0.4785,3.8438-0.8437l0.03125-0.031,1.125-1.9063a0.85008,0.85008,0,0,0,0.03125,-0.0312l0.03125-0.0312a0.85008,0.85008,0,0,0,0.09375,-0.21875l4.0625-6.8125v32.406h-55.094v-33.156zm39.812,1.0625,9.3125,6.4375-0.84375,1.4062a0.85008,0.85008,0,0,0,-0.03125,0c-0.33037,0.5726-0.86691,0.7168-1.4062,0.3438l-2.1875-1.5-0.1875-0.15625-0.65625-0.4375-1.8438-1.2812-0.84375-0.59375-0.0625-0.0312-1.9688-1.3438c-0.25075-0.36937-0.4494-0.7387-0.5-0.96875-0.0558-0.25371-0.0497-0.34572,0.15625-0.59375l1.0625-1.2812zm0.84375,5.9688,0.34375,0.25,1.8438,1.25,0.375,0.25c-0.60662,1.6994-0.69236,3.2017-0.75,4.5-0.0657,1.481-0.18871,2.7295-1.3125,4.3438-0.76502,1.0988-2.0465,1.5537-3.7188,1.9062-1.3283,0.2801-2.854,0.5618-4.3438,1.0625-0.0521-1.5631-0.29881-3.0716-0.5-4.4062-0.25388-1.6841-0.29624-3.0262,0.46875-4.125,1.1246-1.6154,2.2602-2.1673,3.625-2.75,1.1932-0.5094,2.5901-1.1274,3.9688-2.2813zm-30.5,2.5313c-1.6815,0-3.0625,1.4119-3.0625,3.0937s1.381,3.0313,3.0625,3.0313,3.0625-1.3495,3.0625-3.0313-1.381-3.0937-3.0625-3.0937zm0,1.7187c0.76283,0,1.375,0.612,1.375,1.375s-0.61217,1.3438-1.375,1.3438-1.3438-0.5808-1.3438-1.3438,0.58092-1.375,1.3438-1.375zm8,5.6563c-3.3379,0.1812-7.1915,2.4749-10.344,4.6875-3.1522,2.2126-5.5625,4.4062-5.5625,4.4062-0.3273,0.3027-0.36527,0.8915-0.0625,1.2188,0.30273,0.3272,0.89151,0.334,1.2188,0.031,0,0,2.3185-2.1046,5.375-4.25s6.8989-4.2667,9.4688-4.4063c1.6177-0.088,4.3314,1.0381,6.5312,2.25,2.1999,1.212,3.9375,2.4375,3.9375,2.4375,0.35264,0.3353,1.001,0.2728,1.2812-0.125,0.28024-0.3977,0.12188-1.0307-0.3125-1.25,0,0-1.7602-1.2941-4.0625-2.5625-2.3024-1.2684-5.0831-2.567-7.4688-2.4375zm3.2812,22.562,12.344,0,1.3438,4.5312-15,0,1.3125-4.5312zm-3.7812,6.25,19.938,0c0.20135,0,0.3125,0.1424,0.3125,0.3437v2.2188c0,0.2013-0.11115,0.3437-0.3125,0.3437h-19.938c-0.20135,0-0.34375-0.1424-0.34375-0.3437v-2.2188c0-0.2013,0.1424-0.3437,0.34375-0.3437z" /> d="m82.048,962.36c-0.18271-0.003-0.35147-0.001-0.53125,0.0312-0.69633,0.12662-1.3353,0.54943-1.7812,1.1562l-0.03125-0.0312-0.03125,0.0625-18.031,22.125h-44.438c-2.809,0-5.0938,2.2847-5.0938,5.0938v35.531c0,2.8091,2.2847,5.125,5.0938,5.125h20.562l-1.3125,4.5938h-0.71875c-1.1137,0-2.0312,0.9175-2.0312,2.0312v2.2188c0,1.1137,0.91751,2.0625,2.0312,2.0625h19.938c1.1137,0,2.0312-0.9488,2.0312-2.0625v-2.2188c0-1.1137-0.91751-2.0312-2.0312-2.0312h-0.71875l-1.3438-4.5938h20.438c2.809,0,5.0938-2.3159,5.0938-5.125v-35.531c0-1.7706-0.90663-3.3369-2.2812-4.25l10.531-17.625,0.03125-0.0625c0.84234-1.2783,0.51486-3.0308-0.75-3.9062l-3.0312-2.0938c-0.48208-0.33338-1.0456-0.49073-1.5938-0.5zm-0.21875,1.6875c0.28723-0.0523,0.57635,0.0338,0.84375,0.21875l3.0312,2.0938c0.53421,0.36973,0.65504,1.0569,0.28125,1.5938a0.85008,0.85008,0,0,0,-0.03125,0.0312l-17.906,30.062-9.0938-6.3125,22.094-27.125a0.85008,0.85008,0,0,0,0.03125,-0.0625c0.18694-0.26873,0.46277-0.4477,0.75-0.5zm-64.625,23.344,43.062,0-2.3438,2.9062-40.688,0c-0.0312-0.002-0.06255-0.002-0.09375,0-0.0312-0.002-0.06255-0.002-0.09375,0-0.38644,0.0753-0.69183,0.45007-0.6875,0.84375v34.844c0.003,0.4514,0.42377,0.857,0.875,0.8437h56.781c0.44088,0,0.84048-0.4028,0.84375-0.8437v-34.844c-0.008-0.25538-0.13841-0.50419-0.34375-0.65625l1.5-2.5c0.87419,0.61342,1.4375,1.6512,1.4375,2.8125v35.531c0,1.8967-1.5096,3.4063-3.4062,3.4063h-56.844c-1.8966,0-3.4062-1.5096-3.4062-3.4063v-35.531c0-1.8966,1.5096-3.4062,3.4062-3.4062zm0.875,4.5938,38.469,0-1.0312,1.25,0,0.0312c-0.48971,0.60518-0.64056,1.3922-0.5,2.0312,0.14234,0.64722,0.49536,1.1659,0.84375,1.6562a0.85008,0.85008,0,0,0,0.1875,0.21875l1.2812,0.875c-1.0387,0.79518-2.0706,1.1661-3.2188,1.6562-1.4337,0.61212-3.0045,1.4512-4.3438,3.375-1.1451,1.6448-1.0525,3.5446-0.78125,5.3437,0.27121,1.7991,0.70152,3.5802,0.5625,5.2188a0.85008,0.85008,0,0,0,1.2188,0.8437c1.4928-0.7039,3.3085-0.9361,5.0938-1.3125s3.6049-0.9489,4.75-2.5937c1.34-1.9249,1.5559-3.6628,1.625-5.2188,0.05552-1.2502,0.05447-2.363,0.4375-3.625l1.2812,0.875c1.2744,0.8814,3.0499,0.4785,3.8438-0.8437l0.03125-0.031,1.125-1.9063a0.85008,0.85008,0,0,0,0.03125,-0.0312l0.03125-0.0312a0.85008,0.85008,0,0,0,0.09375,-0.21875l4.0625-6.8125v32.406h-55.094v-33.156zm39.812,1.0625,9.3125,6.4375-0.84375,1.4062a0.85008,0.85008,0,0,0,-0.03125,0c-0.33037,0.5726-0.86691,0.7168-1.4062,0.3438l-2.1875-1.5-0.1875-0.15625-0.65625-0.4375-1.8438-1.2812-0.84375-0.59375-0.0625-0.0312-1.9688-1.3438c-0.25075-0.36937-0.4494-0.7387-0.5-0.96875-0.0558-0.25371-0.0497-0.34572,0.15625-0.59375l1.0625-1.2812zm0.84375,5.9688,0.34375,0.25,1.8438,1.25,0.375,0.25c-0.60662,1.6994-0.69236,3.2017-0.75,4.5-0.0657,1.481-0.18871,2.7295-1.3125,4.3438-0.76502,1.0988-2.0465,1.5537-3.7188,1.9062-1.3283,0.2801-2.854,0.5618-4.3438,1.0625-0.0521-1.5631-0.29881-3.0716-0.5-4.4062-0.25388-1.6841-0.29624-3.0262,0.46875-4.125,1.1246-1.6154,2.2602-2.1673,3.625-2.75,1.1932-0.5094,2.5901-1.1274,3.9688-2.2813zm-30.5,2.5313c-1.6815,0-3.0625,1.4119-3.0625,3.0937s1.381,3.0313,3.0625,3.0313,3.0625-1.3495,3.0625-3.0313-1.381-3.0937-3.0625-3.0937zm0,1.7187c0.76283,0,1.375,0.612,1.375,1.375s-0.61217,1.3438-1.375,1.3438-1.3438-0.5808-1.3438-1.3438,0.58092-1.375,1.3438-1.375zm8,5.6563c-3.3379,0.1812-7.1915,2.4749-10.344,4.6875-3.1522,2.2126-5.5625,4.4062-5.5625,4.4062-0.3273,0.3027-0.36527,0.8915-0.0625,1.2188,0.30273,0.3272,0.89151,0.334,1.2188,0.031,0,0,2.3185-2.1046,5.375-4.25s6.8989-4.2667,9.4688-4.4063c1.6177-0.088,4.3314,1.0381,6.5312,2.25,2.1999,1.212,3.9375,2.4375,3.9375,2.4375,0.35264,0.3353,1.001,0.2728,1.2812-0.125,0.28024-0.3977,0.12188-1.0307-0.3125-1.25,0,0-1.7602-1.2941-4.0625-2.5625-2.3024-1.2684-5.0831-2.567-7.4688-2.4375zm3.2812,22.562,12.344,0,1.3438,4.5312-15,0,1.3125-4.5312zm-3.7812,6.25,19.938,0c0.20135,0,0.3125,0.1424,0.3125,0.3437v2.2188c0,0.2013-0.11115,0.3437-0.3125,0.3437h-19.938c-0.20135,0-0.34375-0.1424-0.34375-0.3437v-2.2188c0-0.2013,0.1424-0.3437,0.34375-0.3437z" />
</g> </g>
</g> </g>

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { GenericModal } from "../atoms/GenericModal"; import { GenericModal } from "../ui/GenericModal";
import { import {
MenuItem, MenuItem,
Typography, Typography,
@@ -17,6 +17,7 @@ import { apiUrl } from "../../apiConfig.js";
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
interface IntegrationProps { interface IntegrationProps {
isOpen: boolean; isOpen: boolean;
handleStart: (data: IntegrationSettings) => void; handleStart: (data: IntegrationSettings) => void;
@@ -29,6 +30,20 @@ export interface IntegrationSettings {
data: string; data: string;
} }
// Helper functions to replace js-cookie functionality
const getCookie = (name: string): string | null => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) {
return parts.pop()?.split(';').shift() || null;
}
return null;
};
const removeCookie = (name: string): void => {
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
};
export const IntegrationSettingsModal = ({ export const IntegrationSettingsModal = ({
isOpen, isOpen,
handleStart, handleStart,
@@ -141,14 +156,14 @@ export const IntegrationSettingsModal = ({
useEffect(() => { useEffect(() => {
// Check if there is a success message in cookies // Check if there is a success message in cookies
const status = Cookies.get("robot_auth_status"); const status = getCookie("robot_auth_status");
const message = Cookies.get("robot_auth_message"); const message = getCookie("robot_auth_message");
if (status === "success" && message) { if (status === "success" && message) {
notify("success", message); notify("success", message);
// Clear the cookies after reading // Clear the cookies after reading
Cookies.remove("robot_auth_status"); removeCookie("robot_auth_status");
Cookies.remove("robot_auth_message"); removeCookie("robot_auth_message");
} }
// Check if we're on the callback URL // Check if we're on the callback URL
@@ -172,11 +187,11 @@ export const IntegrationSettingsModal = ({
return ( return (
<GenericModal isOpen={isOpen} onClose={handleClose} modalStyle={modalStyle}> <GenericModal isOpen={isOpen} onClose={handleClose} modalStyle={modalStyle}>
<div style={{ <div style={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
alignItems: "flex-start", alignItems: "flex-start",
marginLeft: "65px", marginLeft: "65px",
}}> }}>
<Typography variant="h6"> <Typography variant="h6">
{t('integration_settings.title')} {t('integration_settings.title')}
</Typography> </Typography>
@@ -220,8 +235,8 @@ export const IntegrationSettingsModal = ({
<> <>
{recording.google_sheet_email && ( {recording.google_sheet_email && (
<Typography sx={{ margin: "20px 0px 30px 0px" }}> <Typography sx={{ margin: "20px 0px 30px 0px" }}>
{t('integration_settings.descriptions.authenticated_as', { {t('integration_settings.descriptions.authenticated_as', {
email: recording.google_sheet_email email: recording.google_sheet_email
})} })}
</Typography> </Typography>
)} )}
@@ -309,4 +324,4 @@ export const modalStyle = {
height: "fit-content", height: "fit-content",
display: "block", display: "block",
padding: "20px", padding: "20px",
}; };

View File

@@ -1,133 +0,0 @@
import { WhereWhatPair } from "maxun-core";
import { GenericModal } from "../atoms/GenericModal";
import { modalStyle } from "./AddWhereCondModal";
import { Button, MenuItem, TextField, Typography } from "@mui/material";
import React, { useRef } from "react";
import { Dropdown as MuiDropdown } from "../atoms/DropdownMui";
import { KeyValueForm } from "./KeyValueForm";
import { ClearButton } from "../atoms/buttons/ClearButton";
import { useSocketStore } from "../../context/socket";
interface AddWhatCondModalProps {
isOpen: boolean;
onClose: () => void;
pair: WhereWhatPair;
index: number;
}
export const AddWhatCondModal = ({isOpen, onClose, pair, index}: AddWhatCondModalProps) => {
const [action, setAction] = React.useState<string>('');
const [objectIndex, setObjectIndex] = React.useState<number>(0);
const [args, setArgs] = React.useState<({type: string, value: (string|number|object|unknown)})[]>([]);
const objectRefs = useRef<({getObject: () => object}|unknown)[]>([]);
const {socket} = useSocketStore();
const handleSubmit = () => {
const argsArray: (string|number|object|unknown)[] = [];
args.map((arg, index) => {
switch (arg.type) {
case 'string':
case 'number':
argsArray[index] = arg.value;
break;
case 'object':
// @ts-ignore
argsArray[index] = objectRefs.current[arg.value].getObject();
}
})
setArgs([]);
onClose();
pair.what.push({
// @ts-ignore
action,
args: argsArray,
})
socket?.emit('updatePair', {index: index-1, pair: pair});
}
return (
<GenericModal isOpen={isOpen} onClose={() => {
setArgs([]);
onClose();
}} modalStyle={modalStyle}>
<div>
<Typography sx={{margin: '20px 0px'}}>Add what condition:</Typography>
<div style={{margin:'8px'}}>
<Typography>Action:</Typography>
<TextField
size='small'
type="string"
onChange={(e) => setAction(e.target.value)}
value={action}
label='action'
/>
<div>
<Typography>Add new argument of type:</Typography>
<Button onClick={() => setArgs([...args,{type: 'string', value: null}]) }>string</Button>
<Button onClick={() => setArgs([...args,{type: 'number', value: null}]) }>number</Button>
<Button onClick={() => {
setArgs([...args,{type: 'object', value: objectIndex}])
setObjectIndex(objectIndex+1);
} }>object</Button>
</div>
<Typography>args:</Typography>
{args.map((arg, index) => {
// @ts-ignore
return (
<div style={{border:'solid 1px gray', padding: '10px', display:'flex', flexDirection:'row', alignItems:'center' }}
key={`wrapper-for-${arg.type}-${index}`}>
<ClearButton handleClick={() => {
args.splice(index,1);
setArgs([...args]);
}}/>
<Typography sx={{margin: '5px'}} key={`number-argument-${arg.type}-${index}`}>{index}: </Typography>
{arg.type === 'string' ?
<TextField
size='small'
type="string"
onChange={(e) => setArgs([
...args.slice(0, index),
{type: arg.type, value: e.target.value},
...args.slice(index + 1)
])}
value={args[index].value || ''}
label="string"
key={`arg-${arg.type}-${index}`}
/> : arg.type === 'number' ?
<TextField
key={`arg-${arg.type}-${index}`}
size='small'
type="number"
onChange={(e) => setArgs([
...args.slice(0, index),
{type: arg.type, value: Number(e.target.value)},
...args.slice(index + 1)
])}
value={args[index].value || ''}
label="number"
/> :
<KeyValueForm ref={el =>
//@ts-ignore
objectRefs.current[arg.value] = el} key={`arg-${arg.type}-${index}`}/>
}
</div>
)})}
<Button
onClick={handleSubmit}
variant="outlined"
sx={{
display: "table-cell",
float: "right",
marginRight: "15px",
marginTop: "20px",
}}
>
{"Add Condition"}
</Button>
</div>
</div>
</GenericModal>
)
}

View File

@@ -1,151 +0,0 @@
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 "maxun-core";
import { SelectChangeEvent } from "@mui/material/Select/Select";
import { DisplayConditionSettings } from "./DisplayWhereConditionSettings";
import { useSocketStore } from "../../context/socket";
interface AddWhereCondModalProps {
isOpen: boolean;
onClose: () => void;
pair: WhereWhatPair;
index: number;
}
export const AddWhereCondModal = ({isOpen, onClose, pair, index}: AddWhereCondModalProps) => {
const [whereProp, setWhereProp] = React.useState<string>('');
const [additionalSettings, setAdditionalSettings] = React.useState<string>('');
const [newValue, setNewValue] = React.useState<any>('');
const [checked, setChecked] = React.useState<boolean[]>(new Array(Object.keys(pair.where).length).fill(false));
const keyValueFormRef = useRef<{getObject: () => object}>(null);
const {socket} = useSocketStore();
const handlePropSelect = (event: SelectChangeEvent<string>) => {
setWhereProp(event.target.value);
switch (event.target.value) {
case 'url': setNewValue(''); break;
case 'selectors': setNewValue(['']); break;
case 'default': return;
}
}
const handleSubmit = () => {
switch (whereProp) {
case 'url':
if (additionalSettings === 'string'){
pair.where.url = newValue;
} else {
pair.where.url = { $regex: newValue };
}
break;
case 'selectors':
pair.where.selectors = newValue;
break;
case 'cookies':
pair.where.cookies = keyValueFormRef.current?.getObject() as Record<string,string>
break;
case 'before':
pair.where.$before = newValue;
break;
case 'after':
pair.where.$after = newValue;
break;
case 'boolean':
const booleanArr = [];
const deleteKeys: string[] = [];
for (let i = 0; i < checked.length; i++) {
if (checked[i]) {
if (Object.keys(pair.where)[i]) {
//@ts-ignore
if (pair.where[Object.keys(pair.where)[i]]) {
booleanArr.push({
//@ts-ignore
[Object.keys(pair.where)[i]]: pair.where[Object.keys(pair.where)[i]]});
}
deleteKeys.push(Object.keys(pair.where)[i]);
}
}
}
// @ts-ignore
deleteKeys.forEach((key: string) => delete pair.where[key]);
//@ts-ignore
pair.where[`$${additionalSettings}`] = booleanArr;
break;
default:
return;
}
onClose();
setWhereProp('');
setAdditionalSettings('');
setNewValue('');
socket?.emit('updatePair', {index: index-1, pair: pair});
}
return (
<GenericModal isOpen={isOpen} onClose={() => {
setWhereProp('');
setAdditionalSettings('');
setNewValue('');
onClose();
}} modalStyle={modalStyle}>
<div>
<Typography sx={{margin: '20px 0px'}}>Add where condition:</Typography>
<div style={{margin:'8px'}}>
<MuiDropdown
id="whereProp"
label="Condition"
value={whereProp}
handleSelect={handlePropSelect}>
<MenuItem value="url">url</MenuItem>
<MenuItem value="selectors">selectors</MenuItem>
<MenuItem value="cookies">cookies</MenuItem>
<MenuItem value="before">before</MenuItem>
<MenuItem value="after">after</MenuItem>
<MenuItem value="boolean">boolean logic</MenuItem>
</MuiDropdown>
</div>
{whereProp ?
<div style={{margin: '8px'}}>
<DisplayConditionSettings
whereProp={whereProp} additionalSettings={additionalSettings} setAdditionalSettings={setAdditionalSettings}
newValue={newValue} setNewValue={setNewValue} checked={checked} setChecked={setChecked}
keyValueFormRef={keyValueFormRef} whereKeys={Object.keys(pair.where)}
/>
<Button
onClick={handleSubmit}
variant="outlined"
sx={{
display: "table-cell",
float: "right",
marginRight: "15px",
marginTop: "20px",
}}
>
{"Add Condition"}
</Button>
</div>
: null}
</div>
</GenericModal>
)
}
export const modalStyle = {
top: '40%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '30%',
backgroundColor: 'background.paper',
p: 4,
height:'fit-content',
display:'block',
padding: '20px',
};

View File

@@ -1,86 +0,0 @@
import React from "react";
import { Button, MenuItem, TextField, Typography } from "@mui/material";
import { Dropdown } from "../atoms/DropdownMui";
import { RunSettings } from "./RunSettings";
import { useSocketStore } from "../../context/socket";
interface LeftSidePanelSettingsProps {
params: any[]
settings: RunSettings,
setSettings: (setting: RunSettings) => void
}
export const LeftSidePanelSettings = ({params, settings, setSettings}: LeftSidePanelSettingsProps) => {
const { socket } = useSocketStore();
return (
<div style={{ display: 'flex', flexDirection:'column', alignItems: 'flex-start'}}>
{ params.length !== 0 && (
<React.Fragment>
<Typography>Parameters:</Typography>
{ params?.map((item: string, index: number) => {
return <TextField
sx={{margin: '15px 0px'}}
value={settings.params ? settings.params[item] : ''}
key={`param-${index}`}
type="string"
label={item}
required
onChange={(e) => setSettings(
{
...settings,
params: settings.params
? {
...settings.params,
[item]: e.target.value,
}
: {
[item]: e.target.value,
},
})}
/>
}) }
</React.Fragment>
)}
<Typography sx={{margin: '15px 0px'}}>Interpreter:</Typography>
<TextField
type="number"
label="maxConcurrency"
required
onChange={(e) => setSettings(
{
...settings,
maxConcurrency: parseInt(e.target.value),
})}
defaultValue={settings.maxConcurrency}
/>
<TextField
sx={{margin: '15px 0px'}}
type="number"
label="maxRepeats"
required
onChange={(e) => setSettings(
{
...settings,
maxRepeats: parseInt(e.target.value),
})}
defaultValue={settings.maxRepeats}
/>
<Dropdown
id="debug"
label="debug"
value={settings.debug?.toString()}
handleSelect={(e) => setSettings(
{
...settings,
debug: e.target.value === "true",
})}
>
<MenuItem value="true">true</MenuItem>
<MenuItem value="false">false</MenuItem>
</Dropdown>
<Button sx={{margin: '15px 0px'}} variant='contained'
onClick={() => socket?.emit('settings', settings)}>change</Button>
</div>
);
}

View File

@@ -1,310 +0,0 @@
import React, { useLayoutEffect, useRef, useState } from 'react';
import { WhereWhatPair } from "maxun-core";
import { Box, Button, IconButton, MenuItem, Stack, TextField, Tooltip, Typography } from "@mui/material";
import { Close, KeyboardArrowDown, KeyboardArrowUp } from "@mui/icons-material";
import TreeView from '@mui/lab/TreeView';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import TreeItem from '@mui/lab/TreeItem';
import { AddButton } from "../atoms/buttons/AddButton";
import { WarningText } from "../atoms/texts";
import NotificationImportantIcon from '@mui/icons-material/NotificationImportant';
import { RemoveButton } from "../atoms/buttons/RemoveButton";
import { AddWhereCondModal } from "./AddWhereCondModal";
import { UpdatePair } from "../../api/workflow";
import { useSocketStore } from "../../context/socket";
import { AddWhatCondModal } from "./AddWhatCondModal";
interface PairDetailProps {
pair: WhereWhatPair | null;
index: number;
}
export const PairDetail = ({ pair, index }: PairDetailProps) => {
const [pairIsSelected, setPairIsSelected] = useState(false);
const [collapseWhere, setCollapseWhere] = useState(true);
const [collapseWhat, setCollapseWhat] = useState(true);
const [rerender, setRerender] = useState(false);
const [expanded, setExpanded] = React.useState<string[]>(
pair ? Object.keys(pair.where).map((key, index) => `${key}-${index}`) : []
);
const [addWhereCondOpen, setAddWhereCondOpen] = useState(false);
const [addWhatCondOpen, setAddWhatCondOpen] = useState(false);
const { socket } = useSocketStore();
const handleCollapseWhere = () => {
setCollapseWhere(!collapseWhere);
}
const handleCollapseWhat = () => {
setCollapseWhat(!collapseWhat);
}
const handleToggle = (event: React.SyntheticEvent, nodeIds: string[]) => {
setExpanded(nodeIds);
};
useLayoutEffect(() => {
if (pair) {
setPairIsSelected(true);
}
}, [pair])
const handleChangeValue = (value: any, where: boolean, keys: (string|number)[]) => {
// a moving reference to internal objects within pair.where or pair.what
let schema: any = where ? pair?.where : pair?.what;
const length = keys.length;
for(let i = 0; i < length-1; i++) {
const elem = keys[i];
if( !schema[elem] ) schema[elem] = {}
schema = schema[elem];
}
schema[keys[length-1]] = value;
if (pair && socket) {
socket.emit('updatePair', {index: index-1, pair: pair});
}
setRerender(!rerender);
}
const DisplayValueContent = (value: any, keys: (string|number)[], where: boolean = true) => {
switch (typeof(value)) {
case 'string':
return <TextField
size='small'
type="string"
onChange={(e) => {
try {
const obj = JSON.parse(e.target.value);
handleChangeValue(obj, where, keys);
} catch (error) {
const num = Number(e.target.value);
if (!isNaN(num)) {
handleChangeValue(num, where, keys);
}
handleChangeValue(e.target.value, where, keys)
}
}}
defaultValue={value}
key={`text-field-${keys.join('-')}-${where}`}
/>
case 'number':
return <TextField
size='small'
type="number"
onChange={(e) => handleChangeValue(Number(e.target.value), where, keys)}
defaultValue={value}
key={`text-field-${keys.join('-')}-${where}`}
/>
case 'object':
if (value) {
if (Array.isArray(value)) {
return (
<React.Fragment>
{
value.map((element, index) => {
return DisplayValueContent(element, [...keys, index], where);
})
}
<AddButton handleClick={()=> {
let prevValue:any = where ? pair?.where : pair?.what;
for (const key of keys) {
prevValue = prevValue[key];
}
handleChangeValue([...prevValue, ''], where, keys);
setRerender(!rerender);
}} hoverEffect={false}/>
<RemoveButton handleClick={()=> {
let prevValue:any = where ? pair?.where : pair?.what;
for (const key of keys) {
prevValue = prevValue[key];
}
prevValue.splice(-1);
handleChangeValue(prevValue, where, keys);
setRerender(!rerender);
}}/>
</React.Fragment>
)
} else {
return (
<TreeView
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
sx={{ flexGrow: 1, overflowY: 'auto' }}
key={`tree-view-nested-${keys.join('-')}-${where}`}
>
{
Object.keys(value).map((key2, index) =>
{
return (
<TreeItem nodeId={`${key2}-${index}`} label={`${key2}:`} key={`${key2}-${index}`}>
{ DisplayValueContent(value[key2], [...keys, key2], where) }
</TreeItem>
)
})
}
</TreeView>
)
}
}
break;
default:
return null;
}
}
return (
<React.Fragment>
{ pair &&
<React.Fragment>
<AddWhatCondModal isOpen={addWhatCondOpen} onClose={() => setAddWhatCondOpen(false)}
pair={pair} index={index}/>
<AddWhereCondModal isOpen={addWhereCondOpen} onClose={() => setAddWhereCondOpen(false)}
pair={pair} index={index}/>
</React.Fragment>
}
{
pairIsSelected
? (
<div style={{padding: '10px', overflow: 'hidden'}}>
<Typography>Pair number: {index}</Typography>
<TextField
size='small'
label='id'
onChange={(e) => {
if (pair && socket) {
socket.emit('updatePair', {index: index-1, pair: pair});
pair.id = e.target.value;
}
}}
value={pair ? pair.id ? pair.id : '' : ''}
/>
<Stack spacing={0} direction='row' sx={{
display: 'flex',
alignItems: 'center',
background: 'lightGray',
}}>
<CollapseButton
handleClick={handleCollapseWhere}
isCollapsed={collapseWhere}
/>
<Typography>Where</Typography>
<Tooltip title='Add where condition' placement='right'>
<div>
<AddButton handleClick={()=> {
setAddWhereCondOpen(true);
}} style={{color:'rgba(0, 0, 0, 0.54)', background:'transparent'}}/>
</div>
</Tooltip>
</Stack>
{(collapseWhere && pair && pair.where)
?
<React.Fragment>
{ Object.keys(pair.where).map((key, index) => {
return (
<TreeView
expanded={expanded}
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
sx={{ flexGrow: 1, overflowY: 'auto' }}
onNodeToggle={handleToggle}
key={`tree-view-${key}-${index}`}
>
<TreeItem nodeId={`${key}-${index}`} label={`${key}:`} key={`${key}-${index}`}>
{
// @ts-ignore
DisplayValueContent(pair.where[key], [key])
}
</TreeItem>
</TreeView>
);
})}
</React.Fragment>
: null
}
<Stack spacing={0} direction='row' sx={{
display: 'flex',
alignItems: 'center',
background: 'lightGray',
}}>
<CollapseButton
handleClick={handleCollapseWhat}
isCollapsed={collapseWhat}
/>
<Typography>What</Typography>
<Tooltip title='Add what condition' placement='right'>
<div>
<AddButton handleClick={()=> {
setAddWhatCondOpen(true);
}} style={{color:'rgba(0, 0, 0, 0.54)', background:'transparent'}}/>
</div>
</Tooltip>
</Stack>
{(collapseWhat && pair && pair.what)
?(
<React.Fragment>
{ Object.keys(pair.what).map((key, index) => {
return (
<TreeView
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
sx={{ flexGrow: 1, overflowY: 'auto' }}
key={`tree-view-2-${key}-${index}`}
>
<TreeItem nodeId={`${key}-${index}`} label={`${pair.what[index].action}`}>
{
// @ts-ignore
DisplayValueContent(pair.what[key], [key], false)
}
<Tooltip title='remove action' placement='left'>
<div style={{float:'right'}}>
<CloseButton handleClick={() => {
//@ts-ignore
pair.what.splice(key, 1);
setRerender(!rerender);
}}/>
</div>
</Tooltip>
</TreeItem>
</TreeView>
);
})}
</React.Fragment>
)
: null
}
</div>
)
: <WarningText>
<NotificationImportantIcon color="warning"/>
No pair from the left side panel was selected.
</WarningText>
}
</React.Fragment>
);
}
interface CollapseButtonProps {
handleClick: () => void;
isCollapsed?: boolean;
}
const CollapseButton = ({handleClick, isCollapsed } : CollapseButtonProps) => {
return (
<IconButton aria-label="add" size={"small"} onClick={handleClick}>
{ isCollapsed ? <KeyboardArrowDown/> : <KeyboardArrowUp/>}
</IconButton>
);
}
const CloseButton = ({handleClick } : CollapseButtonProps) => {
return (
<IconButton aria-label="add" size={"small"} onClick={handleClick}
sx={{'&:hover': { color: '#1976d2', backgroundColor: 'white' }}}>
<Close/>
</IconButton>
);
}

View File

@@ -1,195 +0,0 @@
import React, { useState } from 'react';
import { RecordingsTable } from "../molecules/RecordingsTable";
import { Grid } from "@mui/material";
import { RunSettings, RunSettingsModal } from "../molecules/RunSettings";
import { ScheduleSettings, ScheduleSettingsModal } from "../molecules/ScheduleSettings";
import { IntegrationSettings, IntegrationSettingsModal } from "../molecules/IntegrationSettings";
import { RobotSettings, RobotSettingsModal } from "../molecules/RobotSettings";
import { RobotEditModal } from '../molecules/RobotEdit';
import { RobotDuplicationModal } from '../molecules/RobotDuplicate';
interface RecordingsProps {
handleEditRecording: (id: string, fileName: string) => void;
handleRunRecording: (settings: RunSettings) => void;
handleScheduleRecording: (settings: ScheduleSettings) => void;
setRecordingInfo: (id: string, name: string) => void;
}
export const Recordings = ({ handleEditRecording, handleRunRecording, setRecordingInfo, handleScheduleRecording}: RecordingsProps) => {
const [runSettingsAreOpen, setRunSettingsAreOpen] = useState(false);
const [scheduleSettingsAreOpen, setScheduleSettingsAreOpen] = useState(false);
const [integrateSettingsAreOpen, setIntegrateSettingsAreOpen] = useState(false);
const [robotSettingsAreOpen, setRobotSettingsAreOpen] = useState(false);
const [robotEditAreOpen, setRobotEditAreOpen] = useState(false);
const [robotDuplicateAreOpen, setRobotDuplicateAreOpen] = useState(false);
const [params, setParams] = useState<string[]>([]);
const [selectedRecordingId, setSelectedRecordingId] = useState<string>('');
const handleIntegrateRecording = (id: string, settings: IntegrationSettings) => {};
const handleSettingsRecording = (id: string, settings: RobotSettings) => {};
const handleEditRobot = (id: string, settings: RobotSettings) => {};
const handleDuplicateRobot = (id: string, settings: RobotSettings) => {};
const handleSettingsAndIntegrate = (id: string, name: string, params: string[]) => {
if (params.length === 0) {
setIntegrateSettingsAreOpen(true);
setRecordingInfo(id, name);
setSelectedRecordingId(id);
} else {
setParams(params);
setIntegrateSettingsAreOpen(true);
setRecordingInfo(id, name);
setSelectedRecordingId(id);
}
}
const handleSettingsAndRun = (id: string, name: string, params: string[]) => {
if (params.length === 0) {
setRunSettingsAreOpen(true);
setRecordingInfo(id, name);
setSelectedRecordingId(id);
} else {
setParams(params);
setRunSettingsAreOpen(true);
setRecordingInfo(id, name);
setSelectedRecordingId(id);
}
}
const handleSettingsAndSchedule = (id: string, name: string, params: string[]) => {
if (params.length === 0) {
setScheduleSettingsAreOpen(true);
setRecordingInfo(id, name);
setSelectedRecordingId(id);
} else {
setParams(params);
setScheduleSettingsAreOpen(true);
setRecordingInfo(id, name);
setSelectedRecordingId(id);
}
}
const handleRobotSettings = (id: string, name: string, params: string[]) => {
if (params.length === 0) {
setRobotSettingsAreOpen(true);
setRecordingInfo(id, name);
setSelectedRecordingId(id);
} else {
setParams(params);
setRobotSettingsAreOpen(true);
setRecordingInfo(id, name);
setSelectedRecordingId(id);
}
}
const handleEditRobotOption = (id: string, name: string, params: string[]) => {
if (params.length === 0) {
setRobotEditAreOpen(true);
setRecordingInfo(id, name);
setSelectedRecordingId(id);
} else {
setParams(params);
setRobotEditAreOpen(true);
setRecordingInfo(id, name);
setSelectedRecordingId(id);
}
}
const handleDuplicateRobotOption = (id: string, name: string, params: string[]) => {
if (params.length === 0) {
setRobotDuplicateAreOpen(true);
setRecordingInfo(id, name);
setSelectedRecordingId(id);
} else {
setParams(params);
setRobotDuplicateAreOpen(true);
setRecordingInfo(id, name);
setSelectedRecordingId(id);
}
}
const handleClose = () => {
setParams([]);
setRunSettingsAreOpen(false);
setRecordingInfo('', '');
setSelectedRecordingId('');
}
const handleIntegrateClose = () => {
setParams([]);
setIntegrateSettingsAreOpen(false);
setRecordingInfo('', '');
setSelectedRecordingId('');
}
const handleScheduleClose = () => {
setParams([]);
setScheduleSettingsAreOpen(false);
setRecordingInfo('', '');
setSelectedRecordingId('');
}
const handleRobotSettingsClose = () => {
setParams([]);
setRobotSettingsAreOpen(false);
setRecordingInfo('', '');
setSelectedRecordingId('');
}
const handleRobotEditClose = () => {
setParams([]);
setRobotEditAreOpen(false);
setRecordingInfo('', '');
setSelectedRecordingId('');
}
const handleRobotDuplicateClose = () => {
setParams([]);
setRobotDuplicateAreOpen(false);
setRecordingInfo('', '');
setSelectedRecordingId('');
}
return (
<React.Fragment>
<RunSettingsModal isOpen={runSettingsAreOpen}
handleClose={handleClose}
handleStart={(settings) => handleRunRecording(settings)}
isTask={params.length !== 0}
params={params}
/>
<ScheduleSettingsModal isOpen={scheduleSettingsAreOpen}
handleClose={handleScheduleClose}
handleStart={(settings) => handleScheduleRecording(settings)}
/>
<IntegrationSettingsModal isOpen={integrateSettingsAreOpen}
handleClose={handleIntegrateClose}
handleStart={(settings) => handleIntegrateRecording(selectedRecordingId, settings)}
/>
<RobotSettingsModal isOpen={robotSettingsAreOpen}
handleClose={handleRobotSettingsClose}
handleStart={(settings) => handleSettingsRecording(selectedRecordingId, settings)}
/>
<RobotEditModal isOpen={robotEditAreOpen}
handleClose={handleRobotEditClose}
handleStart={(settings) => handleEditRobot(selectedRecordingId,settings)}
/>
<RobotDuplicationModal isOpen={robotDuplicateAreOpen}
handleClose={handleRobotDuplicateClose}
handleStart={(settings) => handleDuplicateRobot(selectedRecordingId, settings)}
/>
<Grid container direction="column" sx={{ padding: '30px' }}>
<Grid item xs>
<RecordingsTable
handleEditRecording={handleEditRecording}
handleRunRecording={handleSettingsAndRun}
handleScheduleRecording={handleSettingsAndSchedule}
handleIntegrateRecording={handleSettingsAndIntegrate}
handleSettingsRecording={handleRobotSettings}
handleEditRobot={handleEditRobotOption}
handleDuplicateRobot={handleDuplicateRobotOption}
/>
</Grid>
</Grid>
</React.Fragment>
);
}

View File

@@ -27,7 +27,7 @@ const DatePicker: React.FC<DatePickerProps> = ({ coordinates, selector, onClose
}; };
return ( return (
<div <div
style={{ style={{
position: 'absolute', position: 'absolute',
left: `${coordinates.x}px`, left: `${coordinates.x}px`,
@@ -48,20 +48,19 @@ const DatePicker: React.FC<DatePickerProps> = ({ coordinates, selector, onClose
autoFocus autoFocus
/> />
<div className="flex justify-end space-x-2"> <div className="flex justify-end space-x-2">
<button <button
onClick={onClose} onClick={onClose}
className="px-3 py-1 text-sm text-gray-600 hover:text-gray-800 border rounded" className="px-3 py-1 text-sm text-gray-600 hover:text-gray-800 border rounded"
> >
Cancel Cancel
</button> </button>
<button <button
onClick={handleConfirm} onClick={handleConfirm}
disabled={!selectedDate} disabled={!selectedDate}
className={`px-3 py-1 text-sm rounded ${ className={`px-3 py-1 text-sm rounded ${selectedDate
selectedDate ? 'bg-blue-500 text-white hover:bg-blue-600'
? 'bg-blue-500 text-white hover:bg-blue-600' : 'bg-gray-300 text-gray-500 cursor-not-allowed'
: 'bg-gray-300 text-gray-500 cursor-not-allowed' }`}
}`}
> >
Confirm Confirm
</button> </button>

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useSocketStore } from '../../context/socket'; import { useSocketStore } from '../../context/socket';
import { Coordinates } from './canvas'; import { Coordinates } from '../recorder/canvas';
interface DateTimeLocalPickerProps { interface DateTimeLocalPickerProps {
coordinates: Coordinates; coordinates: Coordinates;
@@ -27,7 +27,7 @@ const DateTimeLocalPicker: React.FC<DateTimeLocalPickerProps> = ({ coordinates,
}; };
return ( return (
<div <div
style={{ style={{
position: 'absolute', position: 'absolute',
left: `${coordinates.x}px`, left: `${coordinates.x}px`,
@@ -48,20 +48,19 @@ const DateTimeLocalPicker: React.FC<DateTimeLocalPickerProps> = ({ coordinates,
autoFocus autoFocus
/> />
<div className="flex justify-end space-x-2"> <div className="flex justify-end space-x-2">
<button <button
onClick={onClose} onClick={onClose}
className="px-3 py-1 text-sm text-gray-600 hover:text-gray-800 border rounded" className="px-3 py-1 text-sm text-gray-600 hover:text-gray-800 border rounded"
> >
Cancel Cancel
</button> </button>
<button <button
onClick={handleConfirm} onClick={handleConfirm}
disabled={!selectedDateTime} disabled={!selectedDateTime}
className={`px-3 py-1 text-sm rounded ${ className={`px-3 py-1 text-sm rounded ${selectedDateTime
selectedDateTime ? 'bg-blue-500 text-white hover:bg-blue-600'
? 'bg-blue-500 text-white hover:bg-blue-600'
: 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-gray-300 text-gray-500 cursor-not-allowed'
}`} }`}
> >
Confirm Confirm
</button> </button>

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useSocketStore } from '../../context/socket'; import { useSocketStore } from '../../context/socket';
import { Coordinates } from './canvas'; import { Coordinates } from '../recorder/canvas';
interface DropdownProps { interface DropdownProps {
coordinates: Coordinates; coordinates: Coordinates;
@@ -47,20 +47,20 @@ const Dropdown = ({ coordinates, selector, options, onClose }: DropdownProps) =>
lineHeight: '18px', lineHeight: '18px',
padding: '0 3px', padding: '0 3px',
cursor: option.disabled ? 'default' : 'default', cursor: option.disabled ? 'default' : 'default',
backgroundColor: hoveredIndex === index ? '#0078D7' : backgroundColor: hoveredIndex === index ? '#0078D7' :
option.selected ? '#0078D7' : option.selected ? '#0078D7' :
option.disabled ? '#f8f8f8' : 'white', option.disabled ? '#f8f8f8' : 'white',
color: (hoveredIndex === index || option.selected) ? 'white' : color: (hoveredIndex === index || option.selected) ? 'white' :
option.disabled ? '#a0a0a0' : 'black', option.disabled ? '#a0a0a0' : 'black',
userSelect: 'none', userSelect: 'none',
}); });
return ( return (
<div <div
className="fixed inset-0" className="fixed inset-0"
onClick={onClose} onClick={onClose}
> >
<div <div
style={containerStyle} style={containerStyle}
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
> >

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useSocketStore } from '../../context/socket'; import { useSocketStore } from '../../context/socket';
import { Coordinates } from './canvas'; import { Coordinates } from '../recorder/canvas';
interface TimePickerProps { interface TimePickerProps {
coordinates: Coordinates; coordinates: Coordinates;
@@ -69,7 +69,7 @@ const TimePicker = ({ coordinates, selector, onClose }: TimePickerProps) => {
const getOptionStyle = (value: number, isHour: boolean): React.CSSProperties => { const getOptionStyle = (value: number, isHour: boolean): React.CSSProperties => {
const isHovered = isHour ? hoveredHour === value : hoveredMinute === value; const isHovered = isHour ? hoveredHour === value : hoveredMinute === value;
const isSelected = isHour ? selectedHour === value : selectedMinute === value; const isSelected = isHour ? selectedHour === value : selectedMinute === value;
return { return {
fontSize: '13.333px', fontSize: '13.333px',
lineHeight: '18px', lineHeight: '18px',
@@ -85,11 +85,11 @@ const TimePicker = ({ coordinates, selector, onClose }: TimePickerProps) => {
const minutes = Array.from({ length: 60 }, (_, i) => i); const minutes = Array.from({ length: 60 }, (_, i) => i);
return ( return (
<div <div
className="fixed inset-0" className="fixed inset-0"
onClick={onClose} onClick={onClose}
> >
<div <div
style={containerStyle} style={containerStyle}
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
> >
@@ -109,7 +109,7 @@ const TimePicker = ({ coordinates, selector, onClose }: TimePickerProps) => {
</div> </div>
{/* Minutes column */} {/* Minutes column */}
<div style={{...columnStyle, borderRight: 'none'}}> <div style={{ ...columnStyle, borderRight: 'none' }}>
{minutes.map((minute) => ( {minutes.map((minute) => (
<div <div
key={minute} key={minute}

View File

@@ -1,8 +1,27 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { styled } from '@mui/system'; import { styled } from '@mui/system';
import { Alert, AlertTitle, TextField, Button, Switch, FormControlLabel, Box, Typography, Tabs, Tab, Table, TableContainer, TableHead, TableRow, TableBody, TableCell, Paper } from '@mui/material'; import {
Alert,
AlertTitle,
TextField,
Button,
Switch,
FormControlLabel,
Box,
Typography,
Tabs,
Tab,
Table,
TableContainer,
TableHead,
TableRow,
TableBody,
TableCell,
Paper
} from '@mui/material';
import { sendProxyConfig, getProxyConfig, testProxyConfig, deleteProxyConfig } from '../../api/proxy'; import { sendProxyConfig, getProxyConfig, testProxyConfig, deleteProxyConfig } from '../../api/proxy';
import { useGlobalInfoStore } from '../../context/globalInfo'; import { useGlobalInfoStore } from '../../context/globalInfo';
import { useThemeMode } from '../../context/theme-provider';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const FormContainer = styled(Box)({ const FormContainer = styled(Box)({
@@ -134,6 +153,9 @@ const ProxyForm: React.FC = () => {
fetchProxyConfig(); fetchProxyConfig();
}, []); }, []);
const theme = useThemeMode();
const isDarkMode = theme.darkMode;
return ( return (
<> <>
<FormContainer> <FormContainer>
@@ -144,6 +166,7 @@ const ProxyForm: React.FC = () => {
<Tab label={t('proxy.tab_standard')} /> <Tab label={t('proxy.tab_standard')} />
<Tab label={t('proxy.tab_rotation')} /> <Tab label={t('proxy.tab_rotation')} />
</Tabs> </Tabs>
{tabIndex === 0 && ( {tabIndex === 0 && (
isProxyConfigured ? ( isProxyConfigured ? (
<Box sx={{ maxWidth: 600, width: '100%', marginTop: '5px' }}> <Box sx={{ maxWidth: 600, width: '100%', marginTop: '5px' }}>
@@ -236,14 +259,20 @@ const ProxyForm: React.FC = () => {
<Typography variant="body1" gutterBottom component="div"> <Typography variant="body1" gutterBottom component="div">
{t('proxy.coming_soon')} {t('proxy.coming_soon')}
</Typography> </Typography>
{/* <Button variant="contained" color="primary" sx={{ marginTop: '20px',backgroundColor: '#ff00c3' }}>
<a style={{ color: 'white', textDecoration: 'none' }} href="https://forms.gle/hXjgqDvkEhPcaBW76">Join Maxun Cloud Waitlist</a> */}
<Button variant="contained" color="primary" sx={{ marginTop: '20px' }}> <Button variant="contained" color="primary" sx={{ marginTop: '20px' }}>
<a style={{ color: 'white', textDecoration: 'none' }} href="https://forms.gle/hXjgqDvkEhPcaBW76">{t('proxy.join_waitlist')}</a> <a style={{ color: 'white', textDecoration: 'none' }} href="https://forms.gle/hXjgqDvkEhPcaBW76">{t('proxy.join_waitlist')}</a>
</Button> </Button>
</> </>
</Box> </Box>
)} )}
</FormContainer> </FormContainer>
<Alert severity="info" sx={{ marginTop: '80px', marginLeft: '50px', height: '230px', width: '450px', border: '1px solid #ff00c3' }}>
<Alert severity="info" sx={{ marginTop: '80px', marginLeft: '50px', height: '250px', width: '600px', border: '1px solid #ff00c3' }}>
<AlertTitle>{t('proxy.alert.title')}</AlertTitle> <AlertTitle>{t('proxy.alert.title')}</AlertTitle>
<br /> <br />
<b>{t('proxy.alert.right_way')}</b> <b>{t('proxy.alert.right_way')}</b>
@@ -257,6 +286,7 @@ const ProxyForm: React.FC = () => {
<br /> <br />
<b>{t('proxy.alert.wrong_way')}</b> <b>{t('proxy.alert.wrong_way')}</b>
<br /> <br />
{t('proxy.alert.proxy_url')} http://myusername:mypassword@proxy.com:1337 {t('proxy.alert.proxy_url')} http://myusername:mypassword@proxy.com:1337
</Alert> </Alert>
</> </>

View File

@@ -0,0 +1,134 @@
import { WhereWhatPair } from "maxun-core";
import { GenericModal } from "../ui/GenericModal";
import { modalStyle } from "./AddWhereCondModal";
import { Button, MenuItem, TextField, Typography } from "@mui/material";
import React, { useRef } from "react";
import { Dropdown as MuiDropdown } from "../ui/DropdownMui";
import { KeyValueForm } from "./KeyValueForm";
import { ClearButton } from "../ui/buttons/ClearButton";
import { useSocketStore } from "../../context/socket";
interface AddWhatCondModalProps {
isOpen: boolean;
onClose: () => void;
pair: WhereWhatPair;
index: number;
}
export const AddWhatCondModal = ({ isOpen, onClose, pair, index }: AddWhatCondModalProps) => {
const [action, setAction] = React.useState<string>('');
const [objectIndex, setObjectIndex] = React.useState<number>(0);
const [args, setArgs] = React.useState<({ type: string, value: (string | number | object | unknown) })[]>([]);
const objectRefs = useRef<({ getObject: () => object } | unknown)[]>([]);
const { socket } = useSocketStore();
const handleSubmit = () => {
const argsArray: (string | number | object | unknown)[] = [];
args.map((arg, index) => {
switch (arg.type) {
case 'string':
case 'number':
argsArray[index] = arg.value;
break;
case 'object':
// @ts-ignore
argsArray[index] = objectRefs.current[arg.value].getObject();
}
})
setArgs([]);
onClose();
pair.what.push({
// @ts-ignore
action,
args: argsArray,
})
socket?.emit('updatePair', { index: index - 1, pair: pair });
}
return (
<GenericModal isOpen={isOpen} onClose={() => {
setArgs([]);
onClose();
}} modalStyle={modalStyle}>
<div>
<Typography sx={{ margin: '20px 0px' }}>Add what condition:</Typography>
<div style={{ margin: '8px' }}>
<Typography>Action:</Typography>
<TextField
size='small'
type="string"
onChange={(e) => setAction(e.target.value)}
value={action}
label='action'
/>
<div>
<Typography>Add new argument of type:</Typography>
<Button onClick={() => setArgs([...args, { type: 'string', value: null }])}>string</Button>
<Button onClick={() => setArgs([...args, { type: 'number', value: null }])}>number</Button>
<Button onClick={() => {
setArgs([...args, { type: 'object', value: objectIndex }])
setObjectIndex(objectIndex + 1);
}}>object</Button>
</div>
<Typography>args:</Typography>
{args.map((arg, index) => {
// @ts-ignore
return (
<div style={{ border: 'solid 1px gray', padding: '10px', display: 'flex', flexDirection: 'row', alignItems: 'center' }}
key={`wrapper-for-${arg.type}-${index}`}>
<ClearButton handleClick={() => {
args.splice(index, 1);
setArgs([...args]);
}} />
<Typography sx={{ margin: '5px' }} key={`number-argument-${arg.type}-${index}`}>{index}: </Typography>
{arg.type === 'string' ?
<TextField
size='small'
type="string"
onChange={(e) => setArgs([
...args.slice(0, index),
{ type: arg.type, value: e.target.value },
...args.slice(index + 1)
])}
value={args[index].value || ''}
label="string"
key={`arg-${arg.type}-${index}`}
/> : arg.type === 'number' ?
<TextField
key={`arg-${arg.type}-${index}`}
size='small'
type="number"
onChange={(e) => setArgs([
...args.slice(0, index),
{ type: arg.type, value: Number(e.target.value) },
...args.slice(index + 1)
])}
value={args[index].value || ''}
label="number"
/> :
<KeyValueForm ref={el =>
//@ts-ignore
objectRefs.current[arg.value] = el} key={`arg-${arg.type}-${index}`} />
}
</div>
)
})}
<Button
onClick={handleSubmit}
variant="outlined"
sx={{
display: "table-cell",
float: "right",
marginRight: "15px",
marginTop: "20px",
}}
>
{"Add Condition"}
</Button>
</div>
</div>
</GenericModal>
)
}

View File

@@ -0,0 +1,152 @@
import { Dropdown as MuiDropdown } from "../ui/DropdownMui";
import {
Button,
MenuItem,
Typography
} from "@mui/material";
import React, { useRef } from "react";
import { GenericModal } from "../ui/GenericModal";
import { WhereWhatPair } from "maxun-core";
import { SelectChangeEvent } from "@mui/material/Select/Select";
import { DisplayConditionSettings } from "./DisplayWhereConditionSettings";
import { useSocketStore } from "../../context/socket";
interface AddWhereCondModalProps {
isOpen: boolean;
onClose: () => void;
pair: WhereWhatPair;
index: number;
}
export const AddWhereCondModal = ({ isOpen, onClose, pair, index }: AddWhereCondModalProps) => {
const [whereProp, setWhereProp] = React.useState<string>('');
const [additionalSettings, setAdditionalSettings] = React.useState<string>('');
const [newValue, setNewValue] = React.useState<any>('');
const [checked, setChecked] = React.useState<boolean[]>(new Array(Object.keys(pair.where).length).fill(false));
const keyValueFormRef = useRef<{ getObject: () => object }>(null);
const { socket } = useSocketStore();
const handlePropSelect = (event: SelectChangeEvent<string>) => {
setWhereProp(event.target.value);
switch (event.target.value) {
case 'url': setNewValue(''); break;
case 'selectors': setNewValue(['']); break;
case 'default': return;
}
}
const handleSubmit = () => {
switch (whereProp) {
case 'url':
if (additionalSettings === 'string') {
pair.where.url = newValue;
} else {
pair.where.url = { $regex: newValue };
}
break;
case 'selectors':
pair.where.selectors = newValue;
break;
case 'cookies':
pair.where.cookies = keyValueFormRef.current?.getObject() as Record<string, string>
break;
case 'before':
pair.where.$before = newValue;
break;
case 'after':
pair.where.$after = newValue;
break;
case 'boolean':
const booleanArr = [];
const deleteKeys: string[] = [];
for (let i = 0; i < checked.length; i++) {
if (checked[i]) {
if (Object.keys(pair.where)[i]) {
//@ts-ignore
if (pair.where[Object.keys(pair.where)[i]]) {
booleanArr.push({
//@ts-ignore
[Object.keys(pair.where)[i]]: pair.where[Object.keys(pair.where)[i]]
});
}
deleteKeys.push(Object.keys(pair.where)[i]);
}
}
}
// @ts-ignore
deleteKeys.forEach((key: string) => delete pair.where[key]);
//@ts-ignore
pair.where[`$${additionalSettings}`] = booleanArr;
break;
default:
return;
}
onClose();
setWhereProp('');
setAdditionalSettings('');
setNewValue('');
socket?.emit('updatePair', { index: index - 1, pair: pair });
}
return (
<GenericModal isOpen={isOpen} onClose={() => {
setWhereProp('');
setAdditionalSettings('');
setNewValue('');
onClose();
}} modalStyle={modalStyle}>
<div>
<Typography sx={{ margin: '20px 0px' }}>Add where condition:</Typography>
<div style={{ margin: '8px' }}>
<MuiDropdown
id="whereProp"
label="Condition"
value={whereProp}
handleSelect={handlePropSelect}>
<MenuItem value="url">url</MenuItem>
<MenuItem value="selectors">selectors</MenuItem>
<MenuItem value="cookies">cookies</MenuItem>
<MenuItem value="before">before</MenuItem>
<MenuItem value="after">after</MenuItem>
<MenuItem value="boolean">boolean logic</MenuItem>
</MuiDropdown>
</div>
{whereProp ?
<div style={{ margin: '8px' }}>
<DisplayConditionSettings
whereProp={whereProp} additionalSettings={additionalSettings} setAdditionalSettings={setAdditionalSettings}
newValue={newValue} setNewValue={setNewValue} checked={checked} setChecked={setChecked}
keyValueFormRef={keyValueFormRef} whereKeys={Object.keys(pair.where)}
/>
<Button
onClick={handleSubmit}
variant="outlined"
sx={{
display: "table-cell",
float: "right",
marginRight: "15px",
marginTop: "20px",
}}
>
{"Add Condition"}
</Button>
</div>
: null}
</div>
</GenericModal>
)
}
export const modalStyle = {
top: '40%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '30%',
backgroundColor: 'background.paper',
p: 4,
height: 'fit-content',
display: 'block',
padding: '20px',
};

View File

@@ -1,10 +1,10 @@
import React from "react"; import React from "react";
import { Dropdown as MuiDropdown } from "../atoms/DropdownMui"; import { Dropdown as MuiDropdown } from "../ui/DropdownMui";
import { Checkbox, FormControlLabel, FormGroup, MenuItem, Stack, TextField } from "@mui/material"; import { Checkbox, FormControlLabel, FormGroup, MenuItem, Stack, TextField } from "@mui/material";
import { AddButton } from "../atoms/buttons/AddButton"; import { AddButton } from "../ui/buttons/AddButton";
import { RemoveButton } from "../atoms/buttons/RemoveButton"; import { RemoveButton } from "../ui/buttons/RemoveButton";
import { KeyValueForm } from "./KeyValueForm"; import { KeyValueForm } from "./KeyValueForm";
import { WarningText } from "../atoms/texts"; import { WarningText } from "../ui/texts";
interface DisplayConditionSettingsProps { interface DisplayConditionSettingsProps {
whereProp: string; whereProp: string;
@@ -12,15 +12,15 @@ interface DisplayConditionSettingsProps {
setAdditionalSettings: (value: any) => void; setAdditionalSettings: (value: any) => void;
newValue: any; newValue: any;
setNewValue: (value: any) => void; setNewValue: (value: any) => void;
keyValueFormRef: React.RefObject<{getObject: () => object}>; keyValueFormRef: React.RefObject<{ getObject: () => object }>;
whereKeys: string[]; whereKeys: string[];
checked: boolean[]; checked: boolean[];
setChecked: (value: boolean[]) => void; setChecked: (value: boolean[]) => void;
} }
export const DisplayConditionSettings = ( export const DisplayConditionSettings = (
{whereProp, setAdditionalSettings, additionalSettings, { whereProp, setAdditionalSettings, additionalSettings,
setNewValue, newValue, keyValueFormRef, whereKeys, checked, setChecked} setNewValue, newValue, keyValueFormRef, whereKeys, checked, setChecked }
: DisplayConditionSettingsProps) => { : DisplayConditionSettingsProps) => {
switch (whereProp) { switch (whereProp) {
case 'url': case 'url':
@@ -34,7 +34,7 @@ export const DisplayConditionSettings = (
<MenuItem value="string">string</MenuItem> <MenuItem value="string">string</MenuItem>
<MenuItem value="regex">regex</MenuItem> <MenuItem value="regex">regex</MenuItem>
</MuiDropdown> </MuiDropdown>
{ additionalSettings ? <TextField {additionalSettings ? <TextField
size='small' size='small'
type="string" type="string"
onChange={(e) => setNewValue(e.target.value)} onChange={(e) => setNewValue(e.target.value)}
@@ -56,20 +56,20 @@ export const DisplayConditionSettings = (
...newValue.slice(0, index), ...newValue.slice(0, index),
e.target.value, e.target.value,
...newValue.slice(index + 1) ...newValue.slice(index + 1)
])}/> ])} />
}) })
} }
</Stack> </Stack>
<AddButton handleClick={() => setNewValue([...newValue, ''])}/> <AddButton handleClick={() => setNewValue([...newValue, ''])} />
<RemoveButton handleClick={()=> { <RemoveButton handleClick={() => {
const arr = newValue; const arr = newValue;
arr.splice(-1); arr.splice(-1);
setNewValue([...arr]); setNewValue([...arr]);
}}/> }} />
</React.Fragment> </React.Fragment>
) )
case 'cookies': case 'cookies':
return <KeyValueForm ref={keyValueFormRef}/> return <KeyValueForm ref={keyValueFormRef} />
case 'before': case 'before':
return <TextField return <TextField
label='pair id' label='pair id'
@@ -96,23 +96,23 @@ export const DisplayConditionSettings = (
<MenuItem value="or">or</MenuItem> <MenuItem value="or">or</MenuItem>
</MuiDropdown> </MuiDropdown>
<FormGroup> <FormGroup>
{ {
whereKeys.map((key: string, index: number) => { whereKeys.map((key: string, index: number) => {
return ( return (
<FormControlLabel control={ <FormControlLabel control={
<Checkbox <Checkbox
checked={checked[index]} checked={checked[index]}
onChange={() => setChecked([ onChange={() => setChecked([
...checked.slice(0, index), ...checked.slice(0, index),
!checked[index], !checked[index],
...checked.slice(index + 1) ...checked.slice(index + 1)
])} ])}
key={`checkbox-${key}-${index}`} key={`checkbox-${key}-${index}`}
/> />
} label={key} key={`control-label-form-${key}-${index}`}/> } label={key} key={`control-label-form-${key}-${index}`} />
) )
}) })
} }
</FormGroup> </FormGroup>
<WarningText> <WarningText>
Choose at least 2 where conditions. Nesting of boolean operators Choose at least 2 where conditions. Nesting of boolean operators

View File

@@ -1,11 +1,11 @@
import React, { forwardRef, useImperativeHandle, useRef } from 'react'; import React, { forwardRef, useImperativeHandle, useRef } from 'react';
import { KeyValuePair } from "../atoms/KeyValuePair"; import { KeyValuePair } from "./KeyValuePair";
import { AddButton } from "../atoms/buttons/AddButton"; import { AddButton } from "../ui/buttons/AddButton";
import { RemoveButton } from "../atoms/buttons/RemoveButton"; import { RemoveButton } from "../ui/buttons/RemoveButton";
export const KeyValueForm = forwardRef((props, ref) => { export const KeyValueForm = forwardRef((props, ref) => {
const [numberOfPairs, setNumberOfPairs] = React.useState<number>(1); const [numberOfPairs, setNumberOfPairs] = React.useState<number>(1);
const keyValuePairRefs = useRef<{getKeyValuePair: () => { key: string, value: string }}[]>([]); const keyValuePairRefs = useRef<{ getKeyValuePair: () => { key: string, value: string } }[]>([]);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
getObject() { getObject() {
@@ -28,12 +28,12 @@ export const KeyValueForm = forwardRef((props, ref) => {
{ {
new Array(numberOfPairs).fill(1).map((_, index) => { new Array(numberOfPairs).fill(1).map((_, index) => {
return <KeyValuePair keyLabel={`key ${index + 1}`} valueLabel={`value ${index + 1}`} key={`keyValuePair-${index}`} return <KeyValuePair keyLabel={`key ${index + 1}`} valueLabel={`value ${index + 1}`} key={`keyValuePair-${index}`}
//@ts-ignore //@ts-ignore
ref={el => keyValuePairRefs.current[index] = el}/> ref={el => keyValuePairRefs.current[index] = el} />
}) })
} }
<AddButton handleClick={() => setNumberOfPairs(numberOfPairs + 1)} hoverEffect={false}/> <AddButton handleClick={() => setNumberOfPairs(numberOfPairs + 1)} hoverEffect={false} />
<RemoveButton handleClick={() => setNumberOfPairs(numberOfPairs - 1)}/> <RemoveButton handleClick={() => setNumberOfPairs(numberOfPairs - 1)} />
</div> </div>
); );
}); });

View File

@@ -3,14 +3,14 @@ import React, { useCallback, useEffect, useState } from "react";
import { getActiveWorkflow, getParamsOfActiveWorkflow } from "../../api/workflow"; import { getActiveWorkflow, getParamsOfActiveWorkflow } from "../../api/workflow";
import { useSocketStore } from '../../context/socket'; import { useSocketStore } from '../../context/socket';
import { WhereWhatPair, WorkflowFile } from "maxun-core"; import { WhereWhatPair, WorkflowFile } from "maxun-core";
import { SidePanelHeader } from "../molecules/SidePanelHeader"; import { SidePanelHeader } from "./SidePanelHeader";
import { emptyWorkflow } from "../../shared/constants"; import { emptyWorkflow } from "../../shared/constants";
import { LeftSidePanelContent } from "../molecules/LeftSidePanelContent"; import { LeftSidePanelContent } from "./LeftSidePanelContent";
import { useBrowserDimensionsStore } from "../../context/browserDimensions"; import { useBrowserDimensionsStore } from "../../context/browserDimensions";
import { useGlobalInfoStore } from "../../context/globalInfo"; import { useGlobalInfoStore } from "../../context/globalInfo";
import { TabContext, TabPanel } from "@mui/lab"; import { TabContext, TabPanel } from "@mui/lab";
import { LeftSidePanelSettings } from "../molecules/LeftSidePanelSettings"; import { LeftSidePanelSettings } from "./LeftSidePanelSettings";
import { RunSettings } from "../molecules/RunSettings"; import { RunSettings } from "../run/RunSettings";
const fetchWorkflow = (id: string, callback: (response: WorkflowFile) => void) => { const fetchWorkflow = (id: string, callback: (response: WorkflowFile) => void) => {
getActiveWorkflow(id).then( getActiveWorkflow(id).then(

View File

@@ -5,9 +5,9 @@ import { WhereWhatPair, WorkflowFile } from "maxun-core";
import { useSocketStore } from "../../context/socket"; import { useSocketStore } from "../../context/socket";
import { Add } from "@mui/icons-material"; import { Add } from "@mui/icons-material";
import { Socket } from "socket.io-client"; import { Socket } from "socket.io-client";
import { AddButton } from "../atoms/buttons/AddButton"; import { AddButton } from "../ui/buttons/AddButton";
import { AddPair } from "../../api/workflow"; import { AddPair } from "../../api/workflow";
import { GenericModal } from "../atoms/GenericModal"; import { GenericModal } from "../ui/GenericModal";
import { PairEditForm } from "./PairEditForm"; import { PairEditForm } from "./PairEditForm";
import { Fab, Tooltip, Typography } from "@mui/material"; import { Fab, Tooltip, Typography } from "@mui/material";
@@ -18,7 +18,7 @@ interface LeftSidePanelContentProps {
handleSelectPairForEdit: (pair: WhereWhatPair, index: number) => void; handleSelectPairForEdit: (pair: WhereWhatPair, index: number) => void;
} }
export const LeftSidePanelContent = ({ workflow, updateWorkflow, recordingName, handleSelectPairForEdit}: LeftSidePanelContentProps) => { export const LeftSidePanelContent = ({ workflow, updateWorkflow, recordingName, handleSelectPairForEdit }: LeftSidePanelContentProps) => {
const [activeId, setActiveId] = React.useState<number>(0); const [activeId, setActiveId] = React.useState<number>(0);
const [breakpoints, setBreakpoints] = React.useState<boolean[]>([]); const [breakpoints, setBreakpoints] = React.useState<boolean[]>([]);
const [showEditModal, setShowEditModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false);
@@ -67,12 +67,12 @@ export const LeftSidePanelContent = ({ workflow, updateWorkflow, recordingName,
return ( return (
<div> <div>
<Tooltip title='Add pair' placement='left' arrow> <Tooltip title='Add pair' placement='left' arrow>
<div style={{ float: 'right'}}> <div style={{ float: 'right' }}>
<AddButton <AddButton
handleClick={handleAddPair} handleClick={handleAddPair}
title='' title=''
hoverEffect={false} hoverEffect={false}
style={{color: 'white', background: '#1976d2'}} style={{ color: 'white', background: '#1976d2' }}
/> />
</div> </div>
</Tooltip> </Tooltip>
@@ -86,20 +86,20 @@ export const LeftSidePanelContent = ({ workflow, updateWorkflow, recordingName,
/> />
</GenericModal> </GenericModal>
<div> <div>
{ {
workflow.workflow.map((pair, i, workflow, ) => workflow.workflow.map((pair, i, workflow,) =>
<Pair <Pair
handleBreakpoint={() => handleBreakpointClick(i)} handleBreakpoint={() => handleBreakpointClick(i)}
isActive={ activeId === i + 1} isActive={activeId === i + 1}
key={workflow.length - i} key={workflow.length - i}
index={workflow.length - i} index={workflow.length - i}
pair={pair} pair={pair}
updateWorkflow={updateWorkflow} updateWorkflow={updateWorkflow}
numberOfPairs={workflow.length} numberOfPairs={workflow.length}
handleSelectPairForEdit={handleSelectPairForEdit} handleSelectPairForEdit={handleSelectPairForEdit}
/>) />)
} }
</div> </div>
</div> </div>
); );
}; };

View File

@@ -0,0 +1,86 @@
import React from "react";
import { Button, MenuItem, TextField, Typography } from "@mui/material";
import { Dropdown } from "../ui/DropdownMui";
import { RunSettings } from "../run/RunSettings";
import { useSocketStore } from "../../context/socket";
interface LeftSidePanelSettingsProps {
params: any[]
settings: RunSettings,
setSettings: (setting: RunSettings) => void
}
export const LeftSidePanelSettings = ({ params, settings, setSettings }: LeftSidePanelSettingsProps) => {
const { socket } = useSocketStore();
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
{params.length !== 0 && (
<React.Fragment>
<Typography>Parameters:</Typography>
{params?.map((item: string, index: number) => {
return <TextField
sx={{ margin: '15px 0px' }}
value={settings.params ? settings.params[item] : ''}
key={`param-${index}`}
type="string"
label={item}
required
onChange={(e) => setSettings(
{
...settings,
params: settings.params
? {
...settings.params,
[item]: e.target.value,
}
: {
[item]: e.target.value,
},
})}
/>
})}
</React.Fragment>
)}
<Typography sx={{ margin: '15px 0px' }}>Interpreter:</Typography>
<TextField
type="number"
label="maxConcurrency"
required
onChange={(e) => setSettings(
{
...settings,
maxConcurrency: parseInt(e.target.value),
})}
defaultValue={settings.maxConcurrency}
/>
<TextField
sx={{ margin: '15px 0px' }}
type="number"
label="maxRepeats"
required
onChange={(e) => setSettings(
{
...settings,
maxRepeats: parseInt(e.target.value),
})}
defaultValue={settings.maxRepeats}
/>
<Dropdown
id="debug"
label="debug"
value={settings.debug?.toString()}
handleSelect={(e) => setSettings(
{
...settings,
debug: e.target.value === "true",
})}
>
<MenuItem value="true">true</MenuItem>
<MenuItem value="false">false</MenuItem>
</Dropdown>
<Button sx={{ margin: '15px 0px' }} variant='contained'
onClick={() => socket?.emit('settings', settings)}>change</Button>
</div>
);
}

View File

@@ -2,12 +2,12 @@ import React, { FC, useState } from 'react';
import { Stack, Button, IconButton, Tooltip, Badge } from "@mui/material"; import { Stack, Button, IconButton, Tooltip, Badge } from "@mui/material";
import { AddPair, deletePair, UpdatePair } from "../../api/workflow"; import { AddPair, deletePair, UpdatePair } from "../../api/workflow";
import { WorkflowFile } from "maxun-core"; import { WorkflowFile } from "maxun-core";
import { ClearButton } from "../atoms/buttons/ClearButton"; import { ClearButton } from "../ui/buttons/ClearButton";
import { GenericModal } from "../atoms/GenericModal"; import { GenericModal } from "../ui/GenericModal";
import { PairEditForm } from "./PairEditForm"; import { PairEditForm } from "./PairEditForm";
import { PairDisplayDiv } from "../atoms/PairDisplayDiv"; import { PairDisplayDiv } from "./PairDisplayDiv";
import { EditButton } from "../atoms/buttons/EditButton"; import { EditButton } from "../ui/buttons/EditButton";
import { BreakpointButton } from "../atoms/buttons/BreakpointButton"; import { BreakpointButton } from "../ui/buttons/BreakpointButton";
import VisibilityIcon from '@mui/icons-material/Visibility'; import VisibilityIcon from '@mui/icons-material/Visibility';
import styled from "styled-components"; import styled from "styled-components";
import { LoadingButton } from "@mui/lab"; import { LoadingButton } from "@mui/lab";
@@ -54,19 +54,19 @@ export const Pair: FC<PairProps> = (
}; };
const handleEdit = (pair: WhereWhatPair, newIndex: number) => { const handleEdit = (pair: WhereWhatPair, newIndex: number) => {
if (newIndex !== index){ if (newIndex !== index) {
AddPair((newIndex - 1), pair).then((updatedWorkflow) => { AddPair((newIndex - 1), pair).then((updatedWorkflow) => {
updateWorkflow(updatedWorkflow); updateWorkflow(updatedWorkflow);
}).catch((error) => { }).catch((error) => {
console.error(error); console.error(error);
}); });
} else { } else {
UpdatePair((index - 1), pair).then((updatedWorkflow) => { UpdatePair((index - 1), pair).then((updatedWorkflow) => {
updateWorkflow(updatedWorkflow); updateWorkflow(updatedWorkflow);
}).catch((error) => { }).catch((error) => {
console.error(error); console.error(error);
}); });
} }
handleClose(); handleClose();
}; };
@@ -78,10 +78,10 @@ export const Pair: FC<PairProps> = (
return ( return (
<PairWrapper isActive={isActive}> <PairWrapper isActive={isActive}>
<Stack direction="row"> <Stack direction="row">
<div style={{display: 'flex', maxWidth:'20px', alignItems:'center', justifyContent: 'center', }}> <div style={{ display: 'flex', maxWidth: '20px', alignItems: 'center', justifyContent: 'center', }}>
{isActive ? <LoadingButton loading variant="text"/> {isActive ? <LoadingButton loading variant="text" />
: breakpoint ? <BreakpointButton changeColor={true} handleClick={handleBreakpointClick}/> : breakpoint ? <BreakpointButton changeColor={true} handleClick={handleBreakpointClick} />
: <BreakpointButton handleClick={handleBreakpointClick}/> : <BreakpointButton handleClick={handleBreakpointClick} />
} }
</div> </div>
<Badge badgeContent={pair.what.length} color="primary"> <Badge badgeContent={pair.what.length} color="primary">
@@ -92,53 +92,53 @@ export const Pair: FC<PairProps> = (
fontSize: '1rem', fontSize: '1rem',
textTransform: 'none', textTransform: 'none',
}} variant='text' key={`pair-${index}`} }} variant='text' key={`pair-${index}`}
onClick={() => handleSelectPairForEdit(pair, index)}> onClick={() => handleSelectPairForEdit(pair, index)}>
index: {index} index: {index}
</Button> </Button>
</Badge> </Badge>
<Stack direction="row" spacing={0} <Stack direction="row" spacing={0}
sx={{ sx={{
color: 'inherit',
"&:hover": {
color: 'inherit', color: 'inherit',
} "&:hover": {
}}> color: 'inherit',
}
}}>
<Tooltip title="View" placement='right' arrow> <Tooltip title="View" placement='right' arrow>
<div> <div>
<ViewButton <ViewButton
handleClick={handleOpen} handleClick={handleOpen}
/> />
</div> </div>
</Tooltip> </Tooltip>
<Tooltip title="Raw edit" placement='right' arrow> <Tooltip title="Raw edit" placement='right' arrow>
<div> <div>
<EditButton <EditButton
handleClick={() => { handleClick={() => {
enableEdit(); enableEdit();
handleOpen(); handleOpen();
}} }}
/> />
</div> </div>
</Tooltip> </Tooltip>
<Tooltip title="Delete" placement='right' arrow> <Tooltip title="Delete" placement='right' arrow>
<div> <div>
<ClearButton handleClick={handleDelete}/> <ClearButton handleClick={handleDelete} />
</div> </div>
</Tooltip> </Tooltip>
</Stack> </Stack>
</Stack> </Stack>
<GenericModal isOpen={open} onClose={handleClose}> <GenericModal isOpen={open} onClose={handleClose}>
{ edit {edit
? ?
<PairEditForm <PairEditForm
onSubmitOfPair={handleEdit} onSubmitOfPair={handleEdit}
numberOfPairs={numberOfPairs} numberOfPairs={numberOfPairs}
index={index.toString()} index={index.toString()}
where={pair.where ? JSON.stringify(pair.where) : undefined} where={pair.where ? JSON.stringify(pair.where) : undefined}
what={pair.what ? JSON.stringify(pair.what) : undefined} what={pair.what ? JSON.stringify(pair.what) : undefined}
id={pair.id} id={pair.id}
/> />
: :
<div> <div>
<PairDisplayDiv <PairDisplayDiv
@@ -149,26 +149,26 @@ export const Pair: FC<PairProps> = (
} }
</GenericModal> </GenericModal>
</PairWrapper> </PairWrapper>
); );
}; };
interface ViewButtonProps { interface ViewButtonProps {
handleClick: () => void; handleClick: () => void;
} }
const ViewButton = ({handleClick}: ViewButtonProps) => { const ViewButton = ({ handleClick }: ViewButtonProps) => {
return ( return (
<IconButton aria-label="add" size={"small"} onClick={handleClick} <IconButton aria-label="add" size={"small"} onClick={handleClick}
sx={{color: 'inherit', '&:hover': { color: '#1976d2', backgroundColor: 'transparent' }}}> sx={{ color: 'inherit', '&:hover': { color: '#1976d2', backgroundColor: 'transparent' } }}>
<VisibilityIcon/> <VisibilityIcon />
</IconButton> </IconButton>
); );
} }
const PairWrapper = styled.div<{ isActive: boolean }>` const PairWrapper = styled.div<{ isActive: boolean }>`
background-color: ${({ isActive }) => isActive ? 'rgba(255, 0, 0, 0.1)' : 'transparent' }; background-color: ${({ isActive }) => isActive ? 'rgba(255, 0, 0, 0.1)' : 'transparent'};
border: ${({ isActive }) => isActive ? 'solid 2px red' : 'none' }; border: ${({ isActive }) => isActive ? 'solid 2px red' : 'none'};
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-grow: 1; flex-grow: 1;
@@ -176,6 +176,6 @@ const PairWrapper = styled.div<{ isActive: boolean }>`
color: gray; color: gray;
&:hover { &:hover {
color: dimgray; color: dimgray;
background: ${({ isActive }) => isActive ? 'rgba(255, 0, 0, 0.1)' : 'transparent' }; background: ${({ isActive }) => isActive ? 'rgba(255, 0, 0, 0.1)' : 'transparent'};
} }
`; `;

View File

@@ -0,0 +1,309 @@
import React, { useLayoutEffect, useRef, useState } from 'react';
import { WhereWhatPair } from "maxun-core";
import { Box, Button, IconButton, MenuItem, Stack, TextField, Tooltip, Typography } from "@mui/material";
import { Close, KeyboardArrowDown, KeyboardArrowUp } from "@mui/icons-material";
import TreeView from '@mui/lab/TreeView';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import TreeItem from '@mui/lab/TreeItem';
import { AddButton } from "../ui/buttons/AddButton";
import { WarningText } from "../ui/texts";
import NotificationImportantIcon from '@mui/icons-material/NotificationImportant';
import { RemoveButton } from "../ui/buttons/RemoveButton";
import { AddWhereCondModal } from "./AddWhereCondModal";
import { UpdatePair } from "../../api/workflow";
import { useSocketStore } from "../../context/socket";
import { AddWhatCondModal } from "./AddWhatCondModal";
interface PairDetailProps {
pair: WhereWhatPair | null;
index: number;
}
export const PairDetail = ({ pair, index }: PairDetailProps) => {
const [pairIsSelected, setPairIsSelected] = useState(false);
const [collapseWhere, setCollapseWhere] = useState(true);
const [collapseWhat, setCollapseWhat] = useState(true);
const [rerender, setRerender] = useState(false);
const [expanded, setExpanded] = React.useState<string[]>(
pair ? Object.keys(pair.where).map((key, index) => `${key}-${index}`) : []
);
const [addWhereCondOpen, setAddWhereCondOpen] = useState(false);
const [addWhatCondOpen, setAddWhatCondOpen] = useState(false);
const { socket } = useSocketStore();
const handleCollapseWhere = () => {
setCollapseWhere(!collapseWhere);
}
const handleCollapseWhat = () => {
setCollapseWhat(!collapseWhat);
}
const handleToggle = (event: React.SyntheticEvent, nodeIds: string[]) => {
setExpanded(nodeIds);
};
useLayoutEffect(() => {
if (pair) {
setPairIsSelected(true);
}
}, [pair])
const handleChangeValue = (value: any, where: boolean, keys: (string | number)[]) => {
// a moving reference to internal objects within pair.where or pair.what
let schema: any = where ? pair?.where : pair?.what;
const length = keys.length;
for (let i = 0; i < length - 1; i++) {
const elem = keys[i];
if (!schema[elem]) schema[elem] = {}
schema = schema[elem];
}
schema[keys[length - 1]] = value;
if (pair && socket) {
socket.emit('updatePair', { index: index - 1, pair: pair });
}
setRerender(!rerender);
}
const DisplayValueContent = (value: any, keys: (string | number)[], where: boolean = true) => {
switch (typeof (value)) {
case 'string':
return <TextField
size='small'
type="string"
onChange={(e) => {
try {
const obj = JSON.parse(e.target.value);
handleChangeValue(obj, where, keys);
} catch (error) {
const num = Number(e.target.value);
if (!isNaN(num)) {
handleChangeValue(num, where, keys);
}
handleChangeValue(e.target.value, where, keys)
}
}}
defaultValue={value}
key={`text-field-${keys.join('-')}-${where}`}
/>
case 'number':
return <TextField
size='small'
type="number"
onChange={(e) => handleChangeValue(Number(e.target.value), where, keys)}
defaultValue={value}
key={`text-field-${keys.join('-')}-${where}`}
/>
case 'object':
if (value) {
if (Array.isArray(value)) {
return (
<React.Fragment>
{
value.map((element, index) => {
return DisplayValueContent(element, [...keys, index], where);
})
}
<AddButton handleClick={() => {
let prevValue: any = where ? pair?.where : pair?.what;
for (const key of keys) {
prevValue = prevValue[key];
}
handleChangeValue([...prevValue, ''], where, keys);
setRerender(!rerender);
}} hoverEffect={false} />
<RemoveButton handleClick={() => {
let prevValue: any = where ? pair?.where : pair?.what;
for (const key of keys) {
prevValue = prevValue[key];
}
prevValue.splice(-1);
handleChangeValue(prevValue, where, keys);
setRerender(!rerender);
}} />
</React.Fragment>
)
} else {
return (
<TreeView
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
sx={{ flexGrow: 1, overflowY: 'auto' }}
key={`tree-view-nested-${keys.join('-')}-${where}`}
>
{
Object.keys(value).map((key2, index) => {
return (
<TreeItem nodeId={`${key2}-${index}`} label={`${key2}:`} key={`${key2}-${index}`}>
{DisplayValueContent(value[key2], [...keys, key2], where)}
</TreeItem>
)
})
}
</TreeView>
)
}
}
break;
default:
return null;
}
}
return (
<React.Fragment>
{pair &&
<React.Fragment>
<AddWhatCondModal isOpen={addWhatCondOpen} onClose={() => setAddWhatCondOpen(false)}
pair={pair} index={index} />
<AddWhereCondModal isOpen={addWhereCondOpen} onClose={() => setAddWhereCondOpen(false)}
pair={pair} index={index} />
</React.Fragment>
}
{
pairIsSelected
? (
<div style={{ padding: '10px', overflow: 'hidden' }}>
<Typography>Pair number: {index}</Typography>
<TextField
size='small'
label='id'
onChange={(e) => {
if (pair && socket) {
socket.emit('updatePair', { index: index - 1, pair: pair });
pair.id = e.target.value;
}
}}
value={pair ? pair.id ? pair.id : '' : ''}
/>
<Stack spacing={0} direction='row' sx={{
display: 'flex',
alignItems: 'center',
background: 'lightGray',
}}>
<CollapseButton
handleClick={handleCollapseWhere}
isCollapsed={collapseWhere}
/>
<Typography>Where</Typography>
<Tooltip title='Add where condition' placement='right'>
<div>
<AddButton handleClick={() => {
setAddWhereCondOpen(true);
}} style={{ color: 'rgba(0, 0, 0, 0.54)', background: 'transparent' }} />
</div>
</Tooltip>
</Stack>
{(collapseWhere && pair && pair.where)
?
<React.Fragment>
{Object.keys(pair.where).map((key, index) => {
return (
<TreeView
expanded={expanded}
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
sx={{ flexGrow: 1, overflowY: 'auto' }}
onNodeToggle={handleToggle}
key={`tree-view-${key}-${index}`}
>
<TreeItem nodeId={`${key}-${index}`} label={`${key}:`} key={`${key}-${index}`}>
{
// @ts-ignore
DisplayValueContent(pair.where[key], [key])
}
</TreeItem>
</TreeView>
);
})}
</React.Fragment>
: null
}
<Stack spacing={0} direction='row' sx={{
display: 'flex',
alignItems: 'center',
background: 'lightGray',
}}>
<CollapseButton
handleClick={handleCollapseWhat}
isCollapsed={collapseWhat}
/>
<Typography>What</Typography>
<Tooltip title='Add what condition' placement='right'>
<div>
<AddButton handleClick={() => {
setAddWhatCondOpen(true);
}} style={{ color: 'rgba(0, 0, 0, 0.54)', background: 'transparent' }} />
</div>
</Tooltip>
</Stack>
{(collapseWhat && pair && pair.what)
? (
<React.Fragment>
{Object.keys(pair.what).map((key, index) => {
return (
<TreeView
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
sx={{ flexGrow: 1, overflowY: 'auto' }}
key={`tree-view-2-${key}-${index}`}
>
<TreeItem nodeId={`${key}-${index}`} label={`${pair.what[index].action}`}>
{
// @ts-ignore
DisplayValueContent(pair.what[key], [key], false)
}
<Tooltip title='remove action' placement='left'>
<div style={{ float: 'right' }}>
<CloseButton handleClick={() => {
//@ts-ignore
pair.what.splice(key, 1);
setRerender(!rerender);
}} />
</div>
</Tooltip>
</TreeItem>
</TreeView>
);
})}
</React.Fragment>
)
: null
}
</div>
)
: <WarningText>
<NotificationImportantIcon color="warning" />
No pair from the left side panel was selected.
</WarningText>
}
</React.Fragment>
);
}
interface CollapseButtonProps {
handleClick: () => void;
isCollapsed?: boolean;
}
const CollapseButton = ({ handleClick, isCollapsed }: CollapseButtonProps) => {
return (
<IconButton aria-label="add" size={"small"} onClick={handleClick}>
{isCollapsed ? <KeyboardArrowDown /> : <KeyboardArrowUp />}
</IconButton>
);
}
const CloseButton = ({ handleClick }: CollapseButtonProps) => {
return (
<IconButton aria-label="add" size={"small"} onClick={handleClick}
sx={{ '&:hover': { color: '#1976d2', backgroundColor: 'white' } }}>
<Close />
</IconButton>
);
}

View File

@@ -56,30 +56,30 @@ export const PairEditForm: FC<PairEditFormProps> = (
event.preventDefault(); event.preventDefault();
let whereFromPair, whatFromPair; let whereFromPair, whatFromPair;
// validate where // validate where
whereFromPair = { whereFromPair = {
where: pairProps.where && pairProps.where !== '{"url":"","selectors":[""] }' where: pairProps.where && pairProps.where !== '{"url":"","selectors":[""] }'
? JSON.parse(pairProps.where) ? JSON.parse(pairProps.where)
: {}, : {},
what: [], what: [],
}; };
const validationError = Preprocessor.validateWorkflow({workflow: [whereFromPair]}); const validationError = Preprocessor.validateWorkflow({ workflow: [whereFromPair] });
setErrors({ ...errors, where: null }); setErrors({ ...errors, where: null });
if (validationError) { if (validationError) {
setErrors({ ...errors, where: validationError.message }); setErrors({ ...errors, where: validationError.message });
return; return;
} }
// validate what // validate what
whatFromPair = { whatFromPair = {
where: {}, where: {},
what: pairProps.what && pairProps.what !== '[{"action":"","args":[""] }]' what: pairProps.what && pairProps.what !== '[{"action":"","args":[""] }]'
? JSON.parse(pairProps.what): [], ? JSON.parse(pairProps.what) : [],
}; };
const validationErrorWhat = Preprocessor.validateWorkflow({workflow: [whatFromPair]}); const validationErrorWhat = Preprocessor.validateWorkflow({ workflow: [whatFromPair] });
setErrors({ ...errors, "what": null }); setErrors({ ...errors, "what": null });
if (validationErrorWhat) { if (validationErrorWhat) {
setErrors({ ...errors, what: validationErrorWhat.message }); setErrors({ ...errors, what: validationErrorWhat.message });
return; return;
} }
//validate index //validate index
const index = parseInt(pairProps?.index, 10); const index = parseInt(pairProps?.index, 10);
if (index > (numberOfPairs + 1)) { if (index > (numberOfPairs + 1)) {
@@ -99,18 +99,18 @@ export const PairEditForm: FC<PairEditFormProps> = (
} else { } else {
setErrors({ ...errors, index: '' }); setErrors({ ...errors, index: '' });
} }
// submit the pair // submit the pair
onSubmitOfPair(pairProps.id onSubmitOfPair(pairProps.id
? { ? {
id: pairProps.id, id: pairProps.id,
where: whereFromPair?.where || {}, where: whereFromPair?.where || {},
what: whatFromPair?.what || [], what: whatFromPair?.what || [],
} }
: { : {
where: whereFromPair?.where || {}, where: whereFromPair?.where || {},
what: whatFromPair?.what || [], what: whatFromPair?.what || [],
} }
, index); , index);
}; };
return ( return (
@@ -122,33 +122,33 @@ export const PairEditForm: FC<PairEditFormProps> = (
marginTop: "36px", marginTop: "36px",
}} }}
> >
<Typography sx={{marginBottom:'30px'}} variant='h5'>Raw pair edit form:</Typography> <Typography sx={{ marginBottom: '30px' }} variant='h5'>Raw pair edit form:</Typography>
<TextField sx={{ <TextField sx={{
display:"block", display: "block",
marginBottom: "20px" marginBottom: "20px"
}} id="index" label="Index" type="number" }} id="index" label="Index" type="number"
InputProps={{ inputProps: { min: 1 } }} InputProps={{ inputProps: { min: 1 } }}
InputLabelProps={{ InputLabelProps={{
shrink: true, shrink: true,
}} defaultValue={pairProps.index} }} defaultValue={pairProps.index}
onChange={handleInputChange} onChange={handleInputChange}
error={!!errors.index} helperText={errors.index} error={!!errors.index} helperText={errors.index}
required required
/> />
<TextField sx={{ <TextField sx={{
marginBottom: "20px" marginBottom: "20px"
}} id="id" label="Id" type="string" }} id="id" label="Id" type="string"
defaultValue={pairProps.id} defaultValue={pairProps.id}
onChange={handleInputChange} onChange={handleInputChange}
/> />
<TextField multiline sx={{marginBottom: "20px"}} <TextField multiline sx={{ marginBottom: "20px" }}
id="where" label="Where" variant="outlined" onChange={handleInputChange} id="where" label="Where" variant="outlined" onChange={handleInputChange}
defaultValue={ where || '{"url":"","selectors":[""]}' } defaultValue={where || '{"url":"","selectors":[""]}'}
error={!!errors.where} helperText={errors.where}/> error={!!errors.where} helperText={errors.where} />
<TextField multiline sx={{marginBottom: "20px"}} <TextField multiline sx={{ marginBottom: "20px" }}
id="what" label="What" variant="outlined" onChange={handleInputChange} id="what" label="What" variant="outlined" onChange={handleInputChange}
defaultValue={ what || '[{"action":"","args":[""]}]' } defaultValue={what || '[{"action":"","args":[""]}]'}
error={!!errors.what} helperText={errors.what}/> error={!!errors.what} helperText={errors.what} />
<Button <Button
type="submit" type="submit"
variant="contained" variant="contained"

View File

@@ -3,7 +3,7 @@ import { Button, Paper, Box, TextField, IconButton } from "@mui/material";
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import TextFieldsIcon from '@mui/icons-material/TextFields'; import TextFieldsIcon from '@mui/icons-material/TextFields';
import DocumentScannerIcon from '@mui/icons-material/DocumentScanner'; import DocumentScannerIcon from '@mui/icons-material/DocumentScanner';
import { SimpleBox } from "../atoms/Box"; import { SimpleBox } from "../ui/Box";
import { WorkflowFile } from "maxun-core"; import { WorkflowFile } from "maxun-core";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { useGlobalInfoStore } from "../../context/globalInfo"; import { useGlobalInfoStore } from "../../context/globalInfo";
@@ -12,7 +12,7 @@ import { useBrowserSteps } from '../../context/browserSteps';
import { useSocketStore } from '../../context/socket'; import { useSocketStore } from '../../context/socket';
import { ScreenshotSettings } from '../../shared/types'; import { ScreenshotSettings } from '../../shared/types';
import InputAdornment from '@mui/material/InputAdornment'; import InputAdornment from '@mui/material/InputAdornment';
import { SidePanelHeader } from '../molecules/SidePanelHeader'; import { SidePanelHeader } from './SidePanelHeader';
import FormControlLabel from '@mui/material/FormControlLabel'; import FormControlLabel from '@mui/material/FormControlLabel';
import FormControl from '@mui/material/FormControl'; import FormControl from '@mui/material/FormControl';
import FormLabel from '@mui/material/FormLabel'; import FormLabel from '@mui/material/FormLabel';
@@ -21,7 +21,8 @@ import RadioGroup from '@mui/material/RadioGroup';
import { emptyWorkflow } from "../../shared/constants"; import { emptyWorkflow } from "../../shared/constants";
import { getActiveWorkflow } from "../../api/workflow"; import { getActiveWorkflow } from "../../api/workflow";
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import ActionDescriptionBox from '../molecules/ActionDescriptionBox'; import ActionDescriptionBox from '../action/ActionDescriptionBox';
import { useThemeMode } from '../../context/theme-provider';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const fetchWorkflow = (id: string, callback: (response: WorkflowFile) => void) => { const fetchWorkflow = (id: string, callback: (response: WorkflowFile) => void) => {
@@ -243,9 +244,9 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
const settings: Record<string, { selector: string; tag?: string;[key: string]: any }> = {}; const settings: Record<string, { selector: string; tag?: string;[key: string]: any }> = {};
browserSteps.forEach(step => { browserSteps.forEach(step => {
if (browserStepIdList.includes(step.id)) { if (browserStepIdList.includes(step.id)) {
return; return;
} }
if (step.type === 'text' && step.label && step.selectorObj?.selector) { if (step.type === 'text' && step.label && step.selectorObj?.selector) {
settings[step.label] = step.selectorObj; settings[step.label] = step.selectorObj;
} }
@@ -451,25 +452,28 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
const isConfirmCaptureDisabled = useMemo(() => { const isConfirmCaptureDisabled = useMemo(() => {
// Check if we are in the initial stage and if there are no browser steps or no valid list selectors with fields // Check if we are in the initial stage and if there are no browser steps or no valid list selectors with fields
if (captureStage !== 'initial') return false; if (captureStage !== 'initial') return false;
const hasValidListSelector = browserSteps.some(step => const hasValidListSelector = browserSteps.some(step =>
step.type === 'list' && step.type === 'list' &&
step.listSelector && step.listSelector &&
Object.keys(step.fields).length > 0 Object.keys(step.fields).length > 0
); );
// Disable the button if there are no valid list selectors or if there are unconfirmed list text fields // Disable the button if there are no valid list selectors or if there are unconfirmed list text fields
return !hasValidListSelector || hasUnconfirmedListTextFields; return !hasValidListSelector || hasUnconfirmedListTextFields;
}, [captureStage, browserSteps, hasUnconfirmedListTextFields]); }, [captureStage, browserSteps, hasUnconfirmedListTextFields]);
const theme = useThemeMode();
const isDarkMode = theme.darkMode;
return ( return (
<Paper sx={{ height: '520px', width: 'auto', alignItems: "center", background: 'inherit' }} id="browser-actions" elevation={0}> <Paper sx={{ height: '520px', width: 'auto', alignItems: "center", background: 'inherit' }} id="browser-actions" elevation={0}>
{/* <SimpleBox height={60} width='100%' background='lightGray' radius='0%'> {/* <SimpleBox height={60} width='100%' background='lightGray' radius='0%'>
<Typography sx={{ padding: '10px' }}>Last action: {` ${lastAction}`}</Typography> <Typography sx={{ padding: '10px' }}>Last action: {` ${lastAction}`}</Typography>
</SimpleBox> */} </SimpleBox> */}
<ActionDescriptionBox /> <ActionDescriptionBox isDarkMode={isDarkMode} />
<Box display="flex" flexDirection="column" gap={2} style={{ margin: '13px' }}> <Box display="flex" flexDirection="column" gap={2} style={{ margin: '13px' }}>
{!getText && !getScreenshot && !getList && showCaptureList && <Button variant="contained" onClick={startGetList}>{t('right_panel.buttons.capture_list')}</Button>} {!getText && !getScreenshot && !getList && showCaptureList && <Button variant="contained" onClick={startGetList}>{t('right_panel.buttons.capture_list')}</Button>}
{getList && ( {getList && (
<> <>
<Box display="flex" justifyContent="space-between" gap={2} style={{ margin: '15px' }}> <Box display="flex" justifyContent="space-between" gap={2} style={{ margin: '15px' }}>
@@ -477,6 +481,11 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
<Button <Button
variant="outlined" variant="outlined"
onClick={handleBackCaptureList} onClick={handleBackCaptureList}
sx={{
color: '#ff00c3 !important',
borderColor: '#ff00c3 !important',
backgroundColor: 'whitesmoke !important',
}}
> >
{t('right_panel.buttons.back')} {t('right_panel.buttons.back')}
</Button> </Button>
@@ -485,13 +494,26 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
variant="outlined" variant="outlined"
onClick={handleConfirmListCapture} onClick={handleConfirmListCapture}
disabled={captureStage === 'initial' ? isConfirmCaptureDisabled : hasUnconfirmedListTextFields} disabled={captureStage === 'initial' ? isConfirmCaptureDisabled : hasUnconfirmedListTextFields}
sx={{
color: '#ff00c3 !important',
borderColor: '#ff00c3 !important',
backgroundColor: 'whitesmoke !important',
}}
> >
{captureStage === 'initial' ? t('right_panel.buttons.confirm_capture') : {captureStage === 'initial' ? t('right_panel.buttons.confirm_capture') :
captureStage === 'pagination' ? t('right_panel.buttons.confirm_pagination') : captureStage === 'pagination' ? t('right_panel.buttons.confirm_pagination') :
captureStage === 'limit' ? t('right_panel.buttons.confirm_limit') : captureStage === 'limit' ? t('right_panel.buttons.confirm_limit') :
t('right_panel.buttons.finish_capture')} t('right_panel.buttons.finish_capture')}
</Button> </Button>
<Button variant="outlined" color="error" onClick={discardGetList}> <Button
variant="outlined"
color="error"
onClick={discardGetList}
sx={{
color: 'red !important',
borderColor: 'red !important',
backgroundColor: 'whitesmoke !important',
}} >
{t('right_panel.buttons.discard')} {t('right_panel.buttons.discard')}
</Button> </Button>
</Box> </Box>
@@ -500,11 +522,55 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
{showPaginationOptions && ( {showPaginationOptions && (
<Box display="flex" flexDirection="column" gap={2} style={{ margin: '13px' }}> <Box display="flex" flexDirection="column" gap={2} style={{ margin: '13px' }}>
<Typography>{t('right_panel.pagination.title')}</Typography> <Typography>{t('right_panel.pagination.title')}</Typography>
<Button variant={paginationType === 'clickNext' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('clickNext')}>{t('right_panel.pagination.click_next')}</Button> <Button
<Button variant={paginationType === 'clickLoadMore' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('clickLoadMore')}>{t('right_panel.pagination.click_load_more')}</Button> variant={paginationType === 'clickNext' ? "contained" : "outlined"}
<Button variant={paginationType === 'scrollDown' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('scrollDown')}>{t('right_panel.pagination.scroll_down')}</Button> onClick={() => handlePaginationSettingSelect('clickNext')}
<Button variant={paginationType === 'scrollUp' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('scrollUp')}>{t('right_panel.pagination.scroll_up')}</Button> sx={{
<Button variant={paginationType === 'none' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('none')}>{t('right_panel.pagination.none')}</Button> color: paginationType === 'clickNext' ? 'whitesmoke !important' : '#ff00c3 !important',
borderColor: '#ff00c3 !important',
backgroundColor: paginationType === 'clickNext' ? '#ff00c3 !important' : 'whitesmoke !important',
}}>
{t('right_panel.pagination.click_next')}
</Button>
<Button
variant={paginationType === 'clickLoadMore' ? "contained" : "outlined"}
onClick={() => handlePaginationSettingSelect('clickLoadMore')}
sx={{
color: paginationType === 'clickLoadMore' ? 'whitesmoke !important' : '#ff00c3 !important',
borderColor: '#ff00c3 !important',
backgroundColor: paginationType === 'clickLoadMore' ? '#ff00c3 !important' : 'whitesmoke !important',
}}>
{t('right_panel.pagination.click_load_more')}
</Button>
<Button
variant={paginationType === 'scrollDown' ? "contained" : "outlined"}
onClick={() => handlePaginationSettingSelect('scrollDown')}
sx={{
color: paginationType === 'scrollDown' ? 'whitesmoke !important' : '#ff00c3 !important',
borderColor: '#ff00c3 !important',
backgroundColor: paginationType === 'scrollDown' ? '#ff00c3 !important' : 'whitesmoke !important',
}}>
{t('right_panel.pagination.scroll_down')}
</Button>
<Button
variant={paginationType === 'scrollUp' ? "contained" : "outlined"}
onClick={() => handlePaginationSettingSelect('scrollUp')}
sx={{
color: paginationType === 'scrollUp' ? 'whitesmoke !important' : '#ff00c3 !important',
borderColor: '#ff00c3 !important',
backgroundColor: paginationType === 'scrollUp' ? '#ff00c3 !important' : 'whitesmoke !important',
}}>
{t('right_panel.pagination.scroll_up')}
</Button>
<Button
variant={paginationType === 'none' ? "contained" : "outlined"}
onClick={() => handlePaginationSettingSelect('none')}
sx={{
color: paginationType === 'none' ? 'whitesmoke !important' : '#ff00c3 !important',
borderColor: '#ff00c3 !important',
backgroundColor: paginationType === 'none' ? '#ff00c3 !important' : 'whitesmoke !important',
}}>
{t('right_panel.pagination.none')}</Button>
</Box> </Box>
)} )}
{showLimitOptions && ( {showLimitOptions && (
@@ -527,60 +593,94 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
<FormControlLabel value="custom" control={<Radio />} label={t('right_panel.limit.custom')} /> <FormControlLabel value="custom" control={<Radio />} label={t('right_panel.limit.custom')} />
{limitType === 'custom' && ( {limitType === 'custom' && (
<TextField <TextField
type="number" type="number"
value={customLimit} value={customLimit}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(e.target.value); const value = parseInt(e.target.value);
// Only update if the value is greater than or equal to 1 or if the field is empty // Only update if the value is greater than or equal to 1 or if the field is empty
if (e.target.value === '' || value >= 1) { if (e.target.value === '' || value >= 1) {
updateCustomLimit(e.target.value); updateCustomLimit(e.target.value);
}
}}
inputProps={{
min: 1,
onKeyPress: (e: React.KeyboardEvent<HTMLInputElement>) => {
const value = (e.target as HTMLInputElement).value + e.key;
if (parseInt(value) < 1) {
e.preventDefault();
} }
} }}
}} inputProps={{
placeholder={t('right_panel.limit.enter_number')} min: 1,
sx={{ onKeyPress: (e: React.KeyboardEvent<HTMLInputElement>) => {
marginLeft: '10px', const value = (e.target as HTMLInputElement).value + e.key;
'& input': { if (parseInt(value) < 1) {
padding: '10px', e.preventDefault();
background: 'white', }
}, }
width: '150px', // Ensure the text field does not go outside the panel }}
}} placeholder={t('right_panel.limit.enter_number')}
sx={{
marginLeft: '10px',
'& input': {
padding: '10px',
},
width: '150px',
background: isDarkMode ? "#1E2124" : 'white',
color: isDarkMode ? "white" : 'black', // Ensure the text field does not go outside the panel
}}
/> />
)} )}
</div> </div>
</RadioGroup> </RadioGroup>
</FormControl> </FormControl>
)} )}
{/* {!getText && !getScreenshot && !getList && showCaptureText && <Button variant="contained" sx={{backgroundColor:"#ff00c3",color:`${isDarkMode?'white':'black'}`}} onClick={startGetText}>{t('right_panel.buttons.capture_text')}</Button>} */}
{!getText && !getScreenshot && !getList && showCaptureText && <Button variant="contained" onClick={handleStartGetText}>{t('right_panel.buttons.capture_text')}</Button>} {!getText && !getScreenshot && !getList && showCaptureText && <Button variant="contained" onClick={handleStartGetText}>{t('right_panel.buttons.capture_text')}</Button>}
{getText && {getText &&
<> <>
<Box display="flex" justifyContent="space-between" gap={2} style={{ margin: '15px' }}> <Box display="flex" justifyContent="space-between" gap={2} style={{ margin: '15px' }}>
<Button variant="outlined" onClick={stopCaptureAndEmitGetTextSettings} >{t('right_panel.buttons.confirm')}</Button> <Button
<Button variant="outlined" color="error" onClick={discardGetText} >{t('right_panel.buttons.discard')}</Button> variant="outlined"
onClick={stopCaptureAndEmitGetTextSettings}
sx={{
color: '#ff00c3 !important',
borderColor: '#ff00c3 !important',
backgroundColor: 'whitesmoke !important',
}}>
{t('right_panel.buttons.confirm')}
</Button>
<Button
variant="outlined"
color="error"
onClick={discardGetText}
sx={{
color: '#ff00c3 !important',
borderColor: '#ff00c3 !important',
backgroundColor: 'whitesmoke !important',
}}>
{t('right_panel.buttons.discard')}
</Button>
</Box> </Box>
</> </>
} }
{/* {!getText && !getScreenshot && !getList && showCaptureScreenshot && <Button variant="contained" sx={{backgroundColor:"#ff00c3",color:`${isDarkMode?'white':'black'}`}} onClick={startGetScreenshot}>{t('right_panel.buttons.capture_screenshot')}</Button>} */}
{!getText && !getScreenshot && !getList && showCaptureScreenshot && <Button variant="contained" onClick={startGetScreenshot}>{t('right_panel.buttons.capture_screenshot')}</Button>} {!getText && !getScreenshot && !getList && showCaptureScreenshot && <Button variant="contained" onClick={startGetScreenshot}>{t('right_panel.buttons.capture_screenshot')}</Button>}
{getScreenshot && ( {getScreenshot && (
<Box display="flex" flexDirection="column" gap={2}> <Box display="flex" flexDirection="column" gap={2}>
<Button variant="contained" onClick={() => captureScreenshot(true)}>{t('right_panel.screenshot.capture_fullpage')}</Button> <Button variant="contained" onClick={() => captureScreenshot(true)}>{t('right_panel.screenshot.capture_fullpage')}</Button>
<Button variant="contained" onClick={() => captureScreenshot(false)}>{t('right_panel.screenshot.capture_visible')}</Button> <Button variant="contained" onClick={() => captureScreenshot(false)}>{t('right_panel.screenshot.capture_visible')}</Button>
<Button variant="outlined" color="error" onClick={stopGetScreenshot}>{t('right_panel.buttons.discard')}</Button> <Button
variant="outlined"
color="error"
onClick={stopGetScreenshot}
sx={{
color: '#ff00c3 !important',
borderColor: '#ff00c3 !important',
backgroundColor: 'whitesmoke !important',
}}>
{t('right_panel.buttons.discard')}
</Button>
</Box> </Box>
)} )}
</Box> </Box>
<Box> <Box>
{browserSteps.map(step => ( {browserSteps.map(step => (
<Box key={step.id} onMouseEnter={() => handleMouseEnter(step.id)} onMouseLeave={() => handleMouseLeave(step.id)} sx={{ padding: '10px', margin: '11px', borderRadius: '5px', position: 'relative', background: 'white' }}> <Box key={step.id} onMouseEnter={() => handleMouseEnter(step.id)} onMouseLeave={() => handleMouseLeave(step.id)} sx={{ padding: '10px', margin: '11px', borderRadius: '5px', position: 'relative', background: isDarkMode ? "#1E2124" : 'white', color: isDarkMode ? "white" : 'black' }}>
{ {
step.type === 'text' && ( step.type === 'text' && (
<> <>
@@ -601,6 +701,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
</InputAdornment> </InputAdornment>
) )
}} }}
sx={{ background: isDarkMode ? "#1E2124" : 'white', color: isDarkMode ? "white" : 'black' }}
/> />
<TextField <TextField
label={t('right_panel.fields.data')} label={t('right_panel.fields.data')}
@@ -615,6 +716,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
</InputAdornment> </InputAdornment>
) )
}} }}
/> />
{!confirmedTextSteps[step.id] ? ( {!confirmedTextSteps[step.id] ? (
<Box display="flex" justifyContent="space-between" gap={2}> <Box display="flex" justifyContent="space-between" gap={2}>
@@ -638,8 +740,8 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<DocumentScannerIcon sx={{ mr: 1 }} /> <DocumentScannerIcon sx={{ mr: 1 }} />
<Typography> <Typography>
{step.fullPage ? {step.fullPage ?
t('right_panel.screenshot.display_fullpage') : t('right_panel.screenshot.display_fullpage') :
t('right_panel.screenshot.display_visible')} t('right_panel.screenshot.display_visible')}
</Typography> </Typography>
</Box> </Box>
@@ -648,7 +750,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
<> <>
<Typography>{t('right_panel.messages.list_selected')}</Typography> <Typography>{t('right_panel.messages.list_selected')}</Typography>
{Object.entries(step.fields).map(([key, field]) => ( {Object.entries(step.fields).map(([key, field]) => (
<Box key={key}> <Box key={key} sx={{ background: `${isDarkMode ? "#1E2124" : 'white'}` }}>
<TextField <TextField
label={t('right_panel.fields.field_label')} label={t('right_panel.fields.field_label')}
value={field.label || ''} value={field.label || ''}
@@ -677,6 +779,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
</InputAdornment> </InputAdornment>
) )
}} }}
/> />
{!confirmedListTextFields[step.id]?.[key] ? ( {!confirmedListTextFields[step.id]?.[key] ? (
<Box display="flex" justifyContent="space-between" gap={2}> <Box display="flex" justifyContent="space-between" gap={2}>

View File

@@ -1,12 +1,12 @@
import React, { useCallback, useEffect, useState, useContext } from 'react'; import React, { useCallback, useEffect, useState, useContext } from 'react';
import { Button, Box, LinearProgress, Tooltip } from "@mui/material"; import { Button, Box, LinearProgress, Tooltip } from "@mui/material";
import { GenericModal } from "../atoms/GenericModal"; import { GenericModal } from "../ui/GenericModal";
import { stopRecording } from "../../api/recording"; import { stopRecording } from "../../api/recording";
import { useGlobalInfoStore } from "../../context/globalInfo"; import { useGlobalInfoStore } from "../../context/globalInfo";
import { AuthContext } from '../../context/auth'; import { AuthContext } from '../../context/auth';
import { useSocketStore } from "../../context/socket"; import { useSocketStore } from "../../context/socket";
import { TextField, Typography } from "@mui/material"; import { TextField, Typography } from "@mui/material";
import { WarningText } from "../atoms/texts"; import { WarningText } from "../ui/texts";
import NotificationImportantIcon from "@mui/icons-material/NotificationImportant"; import NotificationImportantIcon from "@mui/icons-material/NotificationImportant";
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -77,7 +77,21 @@ export const SaveRecording = ({ fileName }: SaveRecordingProps) => {
return ( return (
<div> <div>
<Button onClick={() => setOpenModal(true)} variant="outlined" sx={{ marginRight: '20px' }} size="small" color="success"> {/* <Button onClick={() => setOpenModal(true)} variant='contained' sx={{ marginRight: '20px',backgroundColor: '#ff00c3',color: 'white' }} size="small" color="success">
Finish */}
<Button
onClick={() => setOpenModal(true)}
variant="outlined"
color="success"
sx={{
marginRight: '20px',
color: '#00c853 !important',
borderColor: '#00c853 !important',
backgroundColor: 'whitesmoke !important',
}}
size="small"
>
{t('right_panel.buttons.finish')} {t('right_panel.buttons.finish')}
</Button> </Button>
@@ -105,8 +119,8 @@ export const SaveRecording = ({ fileName }: SaveRecordingProps) => {
</WarningText> </WarningText>
</React.Fragment>) </React.Fragment>)
: <Button type="submit" variant="contained" sx={{ marginTop: '10px' }}> : <Button type="submit" variant="contained" sx={{ marginTop: '10px' }}>
{t('save_recording.buttons.save')} {t('save_recording.buttons.save')}
</Button> </Button>
} }
{waitingForSave && {waitingForSave &&
<Tooltip title={t('save_recording.tooltips.optimizing')} placement={"bottom"}> <Tooltip title={t('save_recording.tooltips.optimizing')} placement={"bottom"}>

View File

@@ -1,5 +1,5 @@
import React, { FC, useState } from 'react'; import React, { FC, useState } from 'react';
import { InterpretationButtons } from "./InterpretationButtons"; import { InterpretationButtons } from "../run/InterpretationButtons";
import { useSocketStore } from "../../context/socket"; import { useSocketStore } from "../../context/socket";
export const SidePanelHeader = () => { export const SidePanelHeader = () => {

View File

@@ -3,10 +3,10 @@ import { useSocketStore } from '../../context/socket';
import { getMappedCoordinates } from "../../helpers/inputHelpers"; import { getMappedCoordinates } from "../../helpers/inputHelpers";
import { useGlobalInfoStore } from "../../context/globalInfo"; import { useGlobalInfoStore } from "../../context/globalInfo";
import { useActionContext } from '../../context/browserActions'; import { useActionContext } from '../../context/browserActions';
import DatePicker from './DatePicker'; import DatePicker from '../pickers/DatePicker';
import Dropdown from './Dropdown'; import Dropdown from '../pickers/Dropdown';
import TimePicker from './TimePicker'; import TimePicker from '../pickers/TimePicker';
import DateTimeLocalPicker from './DateTimeLocalPicker'; import DateTimeLocalPicker from '../pickers/DateTimeLocalPicker';
interface CreateRefCallback { interface CreateRefCallback {
(ref: React.RefObject<HTMLCanvasElement>): void; (ref: React.RefObject<HTMLCanvasElement>): void;
@@ -76,7 +76,7 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
useEffect(() => { useEffect(() => {
if (socket) { if (socket) {
socket.on('showDatePicker', (info: {coordinates: Coordinates, selector: string}) => { socket.on('showDatePicker', (info: { coordinates: Coordinates, selector: string }) => {
setDatePickerInfo(info); setDatePickerInfo(info);
}); });
@@ -93,11 +93,11 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
setDropdownInfo(info); setDropdownInfo(info);
}); });
socket.on('showTimePicker', (info: {coordinates: Coordinates, selector: string}) => { socket.on('showTimePicker', (info: { coordinates: Coordinates, selector: string }) => {
setTimePickerInfo(info); setTimePickerInfo(info);
}); });
socket.on('showDateTimePicker', (info: {coordinates: Coordinates, selector: string}) => { socket.on('showDateTimePicker', (info: { coordinates: Coordinates, selector: string }) => {
setDateTimeLocalInfo(info); setDateTimeLocalInfo(info);
}); });

View File

@@ -0,0 +1,130 @@
import React, { useState } from "react";
import { RecordingsTable } from "./RecordingsTable";
import { Grid } from "@mui/material";
import { RunSettings, RunSettingsModal } from "../run/RunSettings";
import { ScheduleSettings, ScheduleSettingsModal } from "./ScheduleSettings";
import { IntegrationSettings, IntegrationSettingsModal } from "../integration/IntegrationSettings";
import { RobotSettings, RobotSettingsModal } from "./RobotSettings";
import { RobotEditModal } from "./RobotEdit";
import { RobotDuplicationModal } from "./RobotDuplicate";
import { useNavigate, useLocation, useParams } from "react-router-dom";
interface RecordingsProps {
handleEditRecording: (id: string, fileName: string) => void;
handleRunRecording: (settings: RunSettings) => void;
handleScheduleRecording: (settings: ScheduleSettings) => void;
setRecordingInfo: (id: string, name: string) => void;
}
export const Recordings = ({
handleEditRecording,
handleRunRecording,
setRecordingInfo,
handleScheduleRecording,
}: RecordingsProps) => {
const navigate = useNavigate();
const location = useLocation();
const { selectedRecordingId } = useParams();
const [params, setParams] = useState<string[]>([]);
const handleNavigate = (path: string, id: string, name: string, params: string[]) => {
setParams(params);
setRecordingInfo(id, name);
navigate(path);
};
const handleClose = () => {
setParams([]);
setRecordingInfo("", "");
navigate("/robots"); // Navigate back to the main robots page
};
// Determine which modal to open based on the current route
const getCurrentModal = () => {
const currentPath = location.pathname;
if (currentPath.endsWith("/run")) {
return (
<RunSettingsModal
isOpen={true}
handleClose={handleClose}
handleStart={handleRunRecording}
isTask={params.length !== 0}
params={params}
/>
);
} else if (currentPath.endsWith("/schedule")) {
return (
<ScheduleSettingsModal
isOpen={true}
handleClose={handleClose}
handleStart={handleScheduleRecording}
/>
);
} else if (currentPath.endsWith("/integrate")) {
return (
<IntegrationSettingsModal
isOpen={true}
handleClose={handleClose}
handleStart={() => {}}
/>
);
} else if (currentPath.endsWith("/settings")) {
return (
<RobotSettingsModal
isOpen={true}
handleClose={handleClose}
handleStart={() => {}}
/>
);
} else if (currentPath.endsWith("/edit")) {
return (
<RobotEditModal
isOpen={true}
handleClose={handleClose}
handleStart={() => {}}
/>
);
} else if (currentPath.endsWith("/duplicate")) {
return (
<RobotDuplicationModal
isOpen={true}
handleClose={handleClose}
handleStart={() => {}}
/>
);
}
return null;
};
return (
<React.Fragment>
{getCurrentModal()}
<Grid container direction="column" sx={{ padding: "30px" }}>
<Grid item xs>
<RecordingsTable
handleEditRecording={handleEditRecording}
handleRunRecording={(id, name, params) =>
handleNavigate(`/robots/${id}/run`, id, name, params)
}
handleScheduleRecording={(id, name, params) =>
handleNavigate(`/robots/${id}/schedule`, id, name, params)
}
handleIntegrateRecording={(id, name, params) =>
handleNavigate(`/robots/${id}/integrate`, id, name, params)
}
handleSettingsRecording={(id, name, params) =>
handleNavigate(`/robots/${id}/settings`, id, name, params)
}
handleEditRobot={(id, name, params) =>
handleNavigate(`/robots/${id}/edit`, id, name, params)
}
handleDuplicateRobot={(id, name, params) =>
handleNavigate(`/robots/${id}/duplicate`, id, name, params)
}
/>
</Grid>
</Grid>
</React.Fragment>
);
};

View File

@@ -18,7 +18,7 @@ import { checkRunsForRecording, deleteRecordingFromStorage, getStoredRecordings
import { Add } from "@mui/icons-material"; import { Add } from "@mui/icons-material";
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { stopRecording } from "../../api/recording"; import { stopRecording } from "../../api/recording";
import { GenericModal } from '../atoms/GenericModal'; import { GenericModal } from '../ui/GenericModal';
/** TODO: /** TODO:
@@ -53,7 +53,7 @@ interface RecordingsTableProps {
} }
export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handleScheduleRecording, handleIntegrateRecording, handleSettingsRecording, handleEditRobot, handleDuplicateRobot }: RecordingsTableProps) => { export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handleScheduleRecording, handleIntegrateRecording, handleSettingsRecording, handleEditRobot, handleDuplicateRobot }: RecordingsTableProps) => {
const {t} = useTranslation(); const { t } = useTranslation();
const [page, setPage] = React.useState(0); const [page, setPage] = React.useState(0);
const [rowsPerPage, setRowsPerPage] = React.useState(10); const [rowsPerPage, setRowsPerPage] = React.useState(10);
const [rows, setRows] = React.useState<Data[]>([]); const [rows, setRows] = React.useState<Data[]>([]);
@@ -401,7 +401,7 @@ const OptionsButton = ({ handleEdit, handleDelete, handleDuplicate }: OptionsBut
setAnchorEl(null); setAnchorEl(null);
}; };
const {t} = useTranslation(); const { t } = useTranslation();
return ( return (
<> <>

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { GenericModal } from "../atoms/GenericModal"; import { GenericModal } from "../ui/GenericModal";
import { TextField, Typography, Box, Button } from "@mui/material"; import { TextField, Typography, Box, Button } from "@mui/material";
import { modalStyle } from "./AddWhereCondModal"; import { modalStyle } from "../recorder/AddWhereCondModal";
import { useGlobalInfoStore } from '../../context/globalInfo'; import { useGlobalInfoStore } from '../../context/globalInfo';
import { duplicateRecording, getStoredRecording } from '../../api/storage'; import { duplicateRecording, getStoredRecording } from '../../api/storage';
import { WhereWhatPair } from 'maxun-core'; import { WhereWhatPair } from 'maxun-core';
@@ -99,7 +99,7 @@ export const RobotDuplicationModal = ({ isOpen, handleStart, handleClose, initia
if (success) { if (success) {
notify('success', t('robot_duplication.notifications.duplicate_success')); notify('success', t('robot_duplication.notifications.duplicate_success'));
handleStart(robot); handleStart(robot);
handleClose(); handleClose();
setTimeout(() => { setTimeout(() => {
window.location.reload(); window.location.reload();
@@ -136,7 +136,7 @@ export const RobotDuplicationModal = ({ isOpen, handleStart, handleClose, initia
url1: '<code>producthunt.com/topics/api</code>', url1: '<code>producthunt.com/topics/api</code>',
url2: '<code>producthunt.com/topics/database</code>' url2: '<code>producthunt.com/topics/database</code>'
}) })
}}/> }} />
<br /> <br />
<span> <span>
<b>{t('robot_duplication.descriptions.warning')}</b> <b>{t('robot_duplication.descriptions.warning')}</b>
@@ -152,7 +152,16 @@ export const RobotDuplicationModal = ({ isOpen, handleStart, handleClose, initia
<Button variant="contained" color="primary" onClick={handleSave}> <Button variant="contained" color="primary" onClick={handleSave}>
{t('robot_duplication.buttons.duplicate')} {t('robot_duplication.buttons.duplicate')}
</Button> </Button>
<Button onClick={handleClose} color="primary" variant="outlined" style={{ marginLeft: '10px' }}> <Button
onClick={handleClose}
color="primary"
variant="outlined"
style={{ marginLeft: '10px' }}
sx={{
color: '#ff00c3 !important',
borderColor: '#ff00c3 !important',
backgroundColor: 'whitesmoke !important',
}} >
{t('robot_duplication.buttons.cancel')} {t('robot_duplication.buttons.cancel')}
</Button> </Button>
</Box> </Box>

View File

@@ -1,8 +1,8 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { GenericModal } from "../atoms/GenericModal"; import { GenericModal } from "../ui/GenericModal";
import { TextField, Typography, Box, Button } from "@mui/material"; import { TextField, Typography, Box, Button } from "@mui/material";
import { modalStyle } from "./AddWhereCondModal"; import { modalStyle } from "../recorder/AddWhereCondModal";
import { useGlobalInfoStore } from '../../context/globalInfo'; import { useGlobalInfoStore } from '../../context/globalInfo';
import { getStoredRecording, updateRecording } from '../../api/storage'; import { getStoredRecording, updateRecording } from '../../api/storage';
import { WhereWhatPair } from 'maxun-core'; import { WhereWhatPair } from 'maxun-core';
@@ -118,7 +118,7 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin
if (success) { if (success) {
notify('success', t('robot_edit.notifications.update_success')); notify('success', t('robot_edit.notifications.update_success'));
handleStart(robot); // Inform parent about the updated robot handleStart(robot); // Inform parent about the updated robot
handleClose(); handleClose();
setTimeout(() => { setTimeout(() => {
window.location.reload(); window.location.reload();
@@ -159,11 +159,11 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin
label={t('robot_edit.robot_limit')} label={t('robot_edit.robot_limit')}
type="number" type="number"
value={robot.recording.workflow[0].what[0].args[0].limit || ''} value={robot.recording.workflow[0].what[0].args[0].limit || ''}
onChange={(e) =>{ onChange={(e) => {
const value = parseInt(e.target.value, 10); const value = parseInt(e.target.value, 10);
if (value >= 1) { if (value >= 1) {
handleLimitChange(value); handleLimitChange(value);
} }
}} }}
inputProps={{ min: 1 }} inputProps={{ min: 1 }}
style={{ marginBottom: '20px' }} style={{ marginBottom: '20px' }}
@@ -174,12 +174,16 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin
<Button variant="contained" color="primary" onClick={handleSave}> <Button variant="contained" color="primary" onClick={handleSave}>
{t('robot_edit.save')} {t('robot_edit.save')}
</Button> </Button>
<Button <Button
onClick={handleClose} onClick={handleClose}
color="primary" color="primary"
variant="outlined" variant="outlined"
style={{ marginLeft: '10px' }} style={{ marginLeft: '10px' }}
> sx={{
color: '#ff00c3 !important',
borderColor: '#ff00c3 !important',
backgroundColor: 'whitesmoke !important',
}}>
{t('robot_edit.cancel')} {t('robot_edit.cancel')}
</Button> </Button>
</Box> </Box>

View File

@@ -1,8 +1,8 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { GenericModal } from "../atoms/GenericModal"; import { GenericModal } from "../ui/GenericModal";
import { TextField, Typography, Box } from "@mui/material"; import { TextField, Typography, Box } from "@mui/material";
import { modalStyle } from "./AddWhereCondModal"; import { modalStyle } from "../recorder/AddWhereCondModal";
import { useGlobalInfoStore } from '../../context/globalInfo'; import { useGlobalInfoStore } from '../../context/globalInfo';
import { getStoredRecording } from '../../api/storage'; import { getStoredRecording } from '../../api/storage';
import { WhereWhatPair } from 'maxun-core'; import { WhereWhatPair } from 'maxun-core';

View File

@@ -1,8 +1,8 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { GenericModal } from "../atoms/GenericModal"; import { GenericModal } from "../ui/GenericModal";
import { MenuItem, TextField, Typography, Box } from "@mui/material"; import { MenuItem, TextField, Typography, Box } from "@mui/material";
import { Dropdown } from "../atoms/DropdownMui"; import { Dropdown } from "../ui/DropdownMui";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { validMomentTimezones } from '../../constants/const'; import { validMomentTimezones } from '../../constants/const';
import { useGlobalInfoStore } from '../../context/globalInfo'; import { useGlobalInfoStore } from '../../context/globalInfo';
@@ -123,12 +123,12 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initia
if (!day) return ''; if (!day) return '';
const lastDigit = day.slice(-1); const lastDigit = day.slice(-1);
const lastTwoDigits = day.slice(-2); const lastTwoDigits = day.slice(-2);
// Special cases for 11, 12, 13 // Special cases for 11, 12, 13
if (['11', '12', '13'].includes(lastTwoDigits)) { if (['11', '12', '13'].includes(lastTwoDigits)) {
return t('schedule_settings.labels.on_day.th'); return t('schedule_settings.labels.on_day.th');
} }
// Other cases // Other cases
switch (lastDigit) { switch (lastDigit) {
case '1': return t('schedule_settings.labels.on_day.st'); case '1': return t('schedule_settings.labels.on_day.st');
@@ -273,7 +273,16 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initia
<Button onClick={() => handleStart(settings)} variant="contained" color="primary"> <Button onClick={() => handleStart(settings)} variant="contained" color="primary">
{t('schedule_settings.buttons.save_schedule')} {t('schedule_settings.buttons.save_schedule')}
</Button> </Button>
<Button onClick={handleClose} color="primary" variant="outlined" style={{ marginLeft: '10px' }}> <Button
onClick={handleClose}
color="primary"
variant="outlined"
style={{ marginLeft: '10px' }}
sx={{
color: '#ff00c3 !important',
borderColor: '#ff00c3 !important',
backgroundColor: 'whitesmoke !important',
}}>
{t('schedule_settings.buttons.cancel')} {t('schedule_settings.buttons.cancel')}
</Button> </Button>
</Box> </Box>

View File

@@ -8,8 +8,8 @@ interface ToggleButtonProps {
export const ToggleButton: FC<ToggleButtonProps> = ({ isChecked = false, onChange }) => ( export const ToggleButton: FC<ToggleButtonProps> = ({ isChecked = false, onChange }) => (
<CheckBoxWrapper> <CheckBoxWrapper>
<CheckBox id="checkbox" type="checkbox" onClick={onChange} checked={isChecked}/> <CheckBox id="checkbox" type="checkbox" onClick={onChange} checked={isChecked} />
<CheckBoxLabel htmlFor="checkbox"/> <CheckBoxLabel htmlFor="checkbox" />
</CheckBoxWrapper> </CheckBoxWrapper>
); );

View File

@@ -7,10 +7,11 @@ import { DeleteForever, KeyboardArrowDown, KeyboardArrowUp, Settings } from "@mu
import { deleteRunFromStorage } from "../../api/storage"; import { deleteRunFromStorage } from "../../api/storage";
import { columns, Data } from "./RunsTable"; import { columns, Data } from "./RunsTable";
import { RunContent } from "./RunContent"; import { RunContent } from "./RunContent";
import { GenericModal } from "../atoms/GenericModal"; import { GenericModal } from "../ui/GenericModal";
import { modalStyle } from "./AddWhereCondModal"; import { modalStyle } from "../recorder/AddWhereCondModal";
import { getUserById } from "../../api/auth"; import { getUserById } from "../../api/auth";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
interface RunTypeChipProps { interface RunTypeChipProps {
runByUserId?: string; runByUserId?: string;
@@ -37,6 +38,7 @@ interface CollapsibleRowProps {
} }
export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRunHandler, runningRecordingName }: CollapsibleRowProps) => { export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRunHandler, runningRecordingName }: CollapsibleRowProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate();
const [open, setOpen] = useState(isOpen); const [open, setOpen] = useState(isOpen);
const [openSettingsModal, setOpenSettingsModal] = useState(false); const [openSettingsModal, setOpenSettingsModal] = useState(false);
const [userEmail, setUserEmail] = useState<string | null>(null); const [userEmail, setUserEmail] = useState<string | null>(null);
@@ -47,7 +49,7 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRun
: row.runByAPI : row.runByAPI
? 'API' ? 'API'
: 'Unknown'; : 'Unknown';
const logEndRef = useRef<HTMLDivElement | null>(null); const logEndRef = useRef<HTMLDivElement | null>(null);
const scrollToLogBottom = () => { const scrollToLogBottom = () => {
@@ -60,9 +62,20 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRun
abortRunHandler(); abortRunHandler();
} }
useEffect(() => { const handleRowExpand = () => {
scrollToLogBottom(); const newOpen = !open;
}, [currentLog]) setOpen(newOpen);
if (newOpen) {
navigate(`/runs/${row.robotMetaId}/run/${row.runId}`);
} else {
navigate(`/runs/${row.robotMetaId}`);
}
//scrollToLogBottom();
};
// useEffect(() => {
// scrollToLogBottom();
// }, [currentLog])
useEffect(() => { useEffect(() => {
const fetchUserEmail = async () => { const fetchUserEmail = async () => {
@@ -83,10 +96,7 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRun
<IconButton <IconButton
aria-label="expand row" aria-label="expand row"
size="small" size="small"
onClick={() => { onClick={handleRowExpand}
setOpen(!open);
scrollToLogBottom();
}}
> >
{open ? <KeyboardArrowUp /> : <KeyboardArrowDown />} {open ? <KeyboardArrowUp /> : <KeyboardArrowDown />}
</IconButton> </IconButton>
@@ -103,7 +113,7 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRun
} else { } else {
switch (column.id) { switch (column.id) {
case 'runStatus': case 'runStatus':
return ( return (
<TableCell key={column.id} align={column.align}> <TableCell key={column.id} align={column.align}>
{row.status === 'success' && <Chip label={t('runs_table.run_status_chips.success')} color="success" variant="outlined" />} {row.status === 'success' && <Chip label={t('runs_table.run_status_chips.success')} color="success" variant="outlined" />}
{row.status === 'running' && <Chip label={t('runs_table.run_status_chips.running')} color="warning" variant="outlined" />} {row.status === 'running' && <Chip label={t('runs_table.run_status_chips.running')} color="warning" variant="outlined" />}
@@ -148,11 +158,11 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRun
/> />
<TextField <TextField
label={ label={
row.runByUserId row.runByUserId
? t('runs_table.run_settings_modal.labels.run_by_user') ? t('runs_table.run_settings_modal.labels.run_by_user')
: row.runByScheduleId : row.runByScheduleId
? t('runs_table.run_settings_modal.labels.run_by_schedule') ? t('runs_table.run_settings_modal.labels.run_by_schedule')
: t('runs_table.run_settings_modal.labels.run_by_api') : t('runs_table.run_settings_modal.labels.run_by_api')
} }
value={runByLabel} value={runByLabel}
InputProps={{ readOnly: true }} InputProps={{ readOnly: true }}
@@ -161,10 +171,10 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRun
<Typography variant="body1"> <Typography variant="body1">
{t('runs_table.run_settings_modal.labels.run_type')}: {t('runs_table.run_settings_modal.labels.run_type')}:
</Typography> </Typography>
<RunTypeChip <RunTypeChip
runByUserId={row.runByUserId} runByUserId={row.runByUserId}
runByScheduledId={row.runByScheduleId} runByScheduledId={row.runByScheduleId}
runByAPI={row.runByAPI ?? false} runByAPI={row.runByAPI ?? false}
/> />
</Box> </Box>
</Box> </Box>

View File

@@ -4,7 +4,7 @@ import React, { useCallback, useEffect, useState } from "react";
import { interpretCurrentRecording, stopCurrentInterpretation } from "../../api/recording"; import { interpretCurrentRecording, stopCurrentInterpretation } from "../../api/recording";
import { useSocketStore } from "../../context/socket"; import { useSocketStore } from "../../context/socket";
import { useGlobalInfoStore } from "../../context/globalInfo"; import { useGlobalInfoStore } from "../../context/globalInfo";
import { GenericModal } from "../atoms/GenericModal"; import { GenericModal } from "../ui/GenericModal";
import { WhereWhatPair } from "maxun-core"; import { WhereWhatPair } from "maxun-core";
import HelpIcon from '@mui/icons-material/Help'; import HelpIcon from '@mui/icons-material/Help';
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -79,7 +79,7 @@ export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsP
</Typography> </Typography>
<Box style={{ marginTop: '4px' }}> <Box style={{ marginTop: '4px' }}>
<Typography> <Typography>
{t('interpretation_buttons.modal.previous_action')} <b>{decisionModal.action}</b>, {t('interpretation_buttons.modal.previous_action')} <b>{decisionModal.action}</b>,
{t('interpretation_buttons.modal.element_text')} <b>{decisionModal.innerText}</b> {t('interpretation_buttons.modal.element_text')} <b>{decisionModal.innerText}</b>
</Typography> </Typography>
</Box> </Box>

View File

@@ -15,8 +15,9 @@ import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper'; import Paper from '@mui/material/Paper';
import StorageIcon from '@mui/icons-material/Storage'; import StorageIcon from '@mui/icons-material/Storage';
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
import { SidePanelHeader } from './SidePanelHeader'; import { SidePanelHeader } from '../recorder/SidePanelHeader';
import { useGlobalInfoStore } from '../../context/globalInfo'; import { useGlobalInfoStore } from '../../context/globalInfo';
import { useThemeMode } from '../../context/theme-provider';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
interface InterpretationLogProps { interface InterpretationLogProps {
@@ -81,7 +82,7 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
setLog((prevState) => setLog((prevState) =>
prevState + '\n' + t('interpretation_log.data_sections.binary_received') + '\n' prevState + '\n' + t('interpretation_log.data_sections.binary_received') + '\n'
+ t('interpretation_log.data_sections.mimetype') + mimetype + '\n' + t('interpretation_log.data_sections.mimetype') + mimetype + '\n'
+ t('interpretation_log.data_sections.image_below') + '\n' + t('interpretation_log.data_sections.image_below') + '\n'
+ t('interpretation_log.data_sections.separator')); + t('interpretation_log.data_sections.separator'));
@@ -124,6 +125,8 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
} }
}, [hasScrapeListAction, hasScrapeSchemaAction, hasScreenshotAction, setIsOpen]); }, [hasScrapeListAction, hasScrapeSchemaAction, hasScreenshotAction, setIsOpen]);
const { darkMode } = useThemeMode();
return ( return (
<Grid container> <Grid container>
<Grid item xs={12} md={9} lg={9}> <Grid item xs={12} md={9} lg={9}>
@@ -147,7 +150,7 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
}, },
}} }}
> >
<ArrowUpwardIcon fontSize="inherit" sx={{ marginRight: '10px'}} /> <ArrowUpwardIcon fontSize="inherit" sx={{ marginRight: '10px' }} />
{t('interpretation_log.titles.output_preview')} {t('interpretation_log.titles.output_preview')}
</Button> </Button>
<SwipeableDrawer <SwipeableDrawer
@@ -157,8 +160,8 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
onOpen={toggleDrawer(true)} onOpen={toggleDrawer(true)}
PaperProps={{ PaperProps={{
sx: { sx: {
background: 'white', background: `${darkMode ? '#1e2124' : 'white'}`,
color: 'black', color: `${darkMode ? 'white' : 'black'}`,
padding: '10px', padding: '10px',
height: 500, height: 500,
width: width - 10, width: width - 10,
@@ -168,7 +171,7 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
}} }}
> >
<Typography variant="h6" gutterBottom style={{ display: 'flex', alignItems: 'center' }}> <Typography variant="h6" gutterBottom style={{ display: 'flex', alignItems: 'center' }}>
<StorageIcon style={{ marginRight: '8px' }} /> <StorageIcon style={{ marginRight: '8px' }} />
{t('interpretation_log.titles.output_preview')} {t('interpretation_log.titles.output_preview')}
</Typography> </Typography>
<div <div

View File

@@ -25,7 +25,7 @@ interface RunContentProps {
export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRef, abortRunHandler }: RunContentProps) => { export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRef, abortRunHandler }: RunContentProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [tab, setTab] = React.useState<string>('log'); const [tab, setTab] = React.useState<string>('output');
const [tableData, setTableData] = useState<any[]>([]); const [tableData, setTableData] = useState<any[]>([]);
const [columns, setColumns] = useState<string[]>([]); const [columns, setColumns] = useState<string[]>([]);
@@ -77,9 +77,49 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
<Box sx={{ width: '100%' }}> <Box sx={{ width: '100%' }}>
<TabContext value={tab}> <TabContext value={tab}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}> <Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tab} onChange={(e, newTab) => setTab(newTab)} aria-label="run-content-tabs"> <Tabs
<Tab label={t('run_content.tabs.output_data')} value='output' /> value={tab}
<Tab label={t('run_content.tabs.log')} value='log' /> onChange={(e, newTab) => setTab(newTab)}
aria-label="run-content-tabs"
sx={{
// Remove the default blue indicator
'& .MuiTabs-indicator': {
backgroundColor: '#FF00C3', // Change to pink
},
// Remove default transition effects
'& .MuiTab-root': {
'&.Mui-selected': {
color: '#FF00C3',
},
}
}}
>
<Tab
label={t('run_content.tabs.output_data')}
value='output'
sx={{
color: (theme) => theme.palette.mode === 'dark' ? '#fff' : '#000',
'&:hover': {
color: '#FF00C3'
},
'&.Mui-selected': {
color: '#FF00C3',
}
}}
/>
<Tab
label={t('run_content.tabs.log')}
value='log'
sx={{
color: (theme) => theme.palette.mode === 'dark' ? '#fff' : '#000',
'&:hover': {
color: '#FF00C3'
},
'&.Mui-selected': {
color: '#FF00C3',
}
}}
/>
</Tabs> </Tabs>
</Box> </Box>
<TabPanel value='log'> <TabPanel value='log'>
@@ -161,6 +201,7 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
background: 'rgba(0,0,0,0.06)', background: 'rgba(0,0,0,0.06)',
maxHeight: '300px', maxHeight: '300px',
overflow: 'scroll', overflow: 'scroll',
backgroundColor: '#19171c'
}}> }}>
<pre> <pre>
{JSON.stringify(row.serializableOutput, null, 2)} {JSON.stringify(row.serializableOutput, null, 2)}

View File

@@ -1,9 +1,9 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { GenericModal } from "../atoms/GenericModal"; import { GenericModal } from "../ui/GenericModal";
import { MenuItem, TextField, Typography, Switch, FormControlLabel } from "@mui/material"; import { MenuItem, TextField, Typography, Switch, FormControlLabel } from "@mui/material";
import { Dropdown } from "../atoms/DropdownMui"; import { Dropdown } from "../ui/DropdownMui";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { modalStyle } from "./AddWhereCondModal"; import { modalStyle } from "../recorder/AddWhereCondModal";
interface RunSettingsProps { interface RunSettingsProps {
isOpen: boolean; isOpen: boolean;

View File

@@ -1,6 +1,6 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { Grid } from "@mui/material"; import { Grid } from "@mui/material";
import { RunsTable } from "../molecules/RunsTable"; import { RunsTable } from "./RunsTable";
interface RunsProps { interface RunsProps {
currentInterpretationLog: string; currentInterpretationLog: string;
@@ -13,7 +13,7 @@ export const Runs = (
{ currentInterpretationLog, abortRunHandler, runId, runningRecordingName }: RunsProps) => { { currentInterpretationLog, abortRunHandler, runId, runningRecordingName }: RunsProps) => {
return ( return (
<Grid container direction="column" sx={{ padding: '30px'}}> <Grid container direction="column" sx={{ padding: '30px' }}>
<Grid item xs> <Grid item xs>
<RunsTable <RunsTable
currentInterpretationLog={currentInterpretationLog} currentInterpretationLog={currentInterpretationLog}

View File

@@ -12,7 +12,7 @@ import TableRow from '@mui/material/TableRow';
import { Accordion, AccordionSummary, AccordionDetails, Typography, Box, TextField } from '@mui/material'; import { Accordion, AccordionSummary, AccordionDetails, Typography, Box, TextField } from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import SearchIcon from '@mui/icons-material/Search'; import SearchIcon from '@mui/icons-material/Search';
import { useNavigate } from 'react-router-dom';
import { useGlobalInfoStore } from "../../context/globalInfo"; import { useGlobalInfoStore } from "../../context/globalInfo";
import { getStoredRuns } from "../../api/storage"; import { getStoredRuns } from "../../api/storage";
import { RunSettings } from "./RunSettings"; import { RunSettings } from "./RunSettings";
@@ -61,13 +61,14 @@ interface RunsTableProps {
runningRecordingName: string; runningRecordingName: string;
} }
export const RunsTable: React.FC<RunsTableProps> = ({ export const RunsTable: React.FC<RunsTableProps> = ({
currentInterpretationLog, currentInterpretationLog,
abortRunHandler, abortRunHandler,
runId, runId,
runningRecordingName runningRecordingName
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate();
// Update column labels using translation if needed // Update column labels using translation if needed
const translatedColumns = columns.map(column => ({ const translatedColumns = columns.map(column => ({
@@ -82,6 +83,14 @@ export const RunsTable: React.FC<RunsTableProps> = ({
const { notify, rerenderRuns, setRerenderRuns } = useGlobalInfoStore(); const { notify, rerenderRuns, setRerenderRuns } = useGlobalInfoStore();
const handleAccordionChange = (robotMetaId: string, isExpanded: boolean) => {
if (isExpanded) {
navigate(`/runs/${robotMetaId}`);
} else {
navigate(`/runs`);
}
};
const handleChangePage = (event: unknown, newPage: number) => { const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage); setPage(newPage);
}; };
@@ -155,7 +164,7 @@ export const RunsTable: React.FC<RunsTableProps> = ({
</Box> </Box>
<TableContainer component={Paper} sx={{ width: '100%', overflow: 'hidden' }}> <TableContainer component={Paper} sx={{ width: '100%', overflow: 'hidden' }}>
{Object.entries(groupedRows).map(([id, data]) => ( {Object.entries(groupedRows).map(([id, data]) => (
<Accordion key={id}> <Accordion key={id} onChange={(event, isExpanded) => handleAccordionChange(id, isExpanded)}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="h6">{data[data.length - 1].name}</Typography> <Typography variant="h6">{data[data.length - 1].name}</Typography>
</AccordionSummary> </AccordionSummary>

View File

@@ -1,11 +1,14 @@
import styled from "styled-components"; import styled from "styled-components";
import { Stack } from "@mui/material"; import { Stack } from "@mui/material";
import { useThemeMode } from "../../context/theme-provider";
interface LoaderProps { interface LoaderProps {
text: string; text: string;
} }
export const Loader: React.FC<LoaderProps> = ({ text }) => { export const Loader: React.FC<LoaderProps> = ({ text }) => {
const { darkMode } = useThemeMode();
return ( return (
<Stack direction="column" sx={{ margin: "30px 0px", alignItems: "center" }}> <Stack direction="column" sx={{ margin: "30px 0px", alignItems: "center" }}>
<DotsContainer> <DotsContainer>
@@ -14,15 +17,19 @@ export const Loader: React.FC<LoaderProps> = ({ text }) => {
<Dot /> <Dot />
<Dot /> <Dot />
</DotsContainer> </DotsContainer>
<StyledParagraph>{text}</StyledParagraph> <StyledParagraph darkMode={darkMode}>{text}</StyledParagraph>
</Stack> </Stack>
); );
}; };
const StyledParagraph = styled.p` interface StyledParagraphProps {
darkMode: boolean;
}
const StyledParagraph = styled.p<StyledParagraphProps>`
font-size: large; font-size: large;
font-family: inherit; font-family: inherit;
color: #333; color: ${({ darkMode }) => (darkMode ? 'white' : '#333')};
margin-top: 20px; margin-top: 20px;
`; `;

View File

@@ -1,26 +1,18 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { useThemeMode } from '../../../context/theme-provider';
export const NavBarButton = styled.button<{ disabled: boolean }>` export const NavBarButton = styled.button<{ disabled: boolean, mode: 'light' | 'dark' }>`
margin-left: 10px; margin-left: 10px;
margin-right: 5px; margin-right: 5px;
padding: 0; padding: 0;
border: none; border: none;
background-color: transparent; background-color: ${mode => mode ? '#333' : '#ffffff'};
cursor: ${({ disabled }) => disabled ? 'default' : 'pointer'}; cursor: ${({ disabled }) => disabled ? 'default' : 'pointer'};
width: 24px; width: 24px;
height: 24px; height: 24px;
border-radius: 12px; border-radius: 12px;
outline: none; outline: none;
color: ${({ disabled }) => disabled ? '#999' : '#333'}; color: ${mode => mode ? '#ffffff' : '#333333'};
${({ disabled }) => disabled ? null : `
&:hover {
background-color: #ddd;
}
&:active {
background-color: #d0d0d0;
}
`};
`; `;
export const UrlFormButton = styled.button` export const UrlFormButton = styled.button`

View File

@@ -1,5 +1,5 @@
import React, { createContext, useContext, useState } from "react"; import React, { createContext, useContext, useState } from "react";
import { AlertSnackbarProps } from "../components/atoms/AlertSnackbar"; import { AlertSnackbarProps } from "../components/ui/AlertSnackbar";
interface GlobalInfo { interface GlobalInfo {

View File

@@ -0,0 +1,256 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
const lightTheme = createTheme({
palette: {
primary: {
main: "#ff00c3",
contrastText: "#ffffff",
},
},
components: {
MuiButton: {
styleOverrides: {
root: {
// Default styles for all buttons (optional)
textTransform: "none",
},
containedPrimary: {
// Styles for 'contained' variant with 'primary' color
"&:hover": {
backgroundColor: "#ff66d9",
},
},
outlined: {
// Apply white background for all 'outlined' variant buttons
backgroundColor: "#ffffff",
"&:hover": {
backgroundColor: "#f0f0f0", // Optional lighter background on hover
},
},
},
},
MuiLink: {
styleOverrides: {
root: {
"&:hover": {
color: "#ff00c3",
},
},
},
},
MuiIconButton: {
styleOverrides: {
root: {
// '&:hover': {
// color: "#ff66d9",
// },
},
},
},
MuiTab: {
styleOverrides: {
root: {
textTransform: "none",
},
},
},
MuiAlert: {
styleOverrides: {
standardInfo: {
backgroundColor: "#fce1f4",
color: "#ff00c3",
"& .MuiAlert-icon": {
color: "#ff00c3",
},
},
},
},
MuiAlertTitle: {
styleOverrides: {
root: {
"& .MuiAlert-icon": {
color: "#ffffff",
},
},
},
},
},
});
const darkTheme = createTheme({
palette: {
mode: 'dark',
primary: {
main: "#ff00c3",
contrastText: "#ffffff",
},
background: {
default: '#121212',
paper: '#1e1e1e',
},
text: {
primary: '#ffffff',
secondary: '#b3b3b3',
},
},
components: {
MuiButton: {
styleOverrides: {
root: {
textTransform: "none",
color: '#ffffff',
'&.MuiButton-outlined': {
borderColor: '#ffffff',
color: '#ffffff',
"&:hover": {
borderColor: '#ffffff',
backgroundColor: 'rgba(255, 255, 255, 0.08)',
},
},
},
containedPrimary: {
"&:hover": {
backgroundColor: "#ff66d9",
},
},
outlined: {
// Dark mode outlined buttons
backgroundColor: '#1e1e1e',
borderColor: '#ff00c3',
color: '#ff00c3',
"&:hover": {
backgroundColor: 'rgba(255, 0, 195, 0.08)',
borderColor: '#ff66d9',
},
},
},
},
MuiLink: {
styleOverrides: {
root: {
color: '#ff66d9',
"&:hover": {
color: "#ff00c3",
},
},
},
},
MuiIconButton: {
styleOverrides: {
root: {
color: '#ffffff',
"&:hover": {
backgroundColor: 'rgba(255, 0, 195, 0.08)',
},
},
},
},
MuiTab: {
styleOverrides: {
root: {
textTransform: "none",
color: '#ffffff',
"&.Mui-selected": {
color: '#ff00c3',
},
},
},
},
MuiAlert: {
styleOverrides: {
standardInfo: {
backgroundColor: "rgba(255, 0, 195, 0.15)",
color: "#ff66d9",
"& .MuiAlert-icon": {
color: "#ff66d9",
},
},
},
},
MuiAlertTitle: {
styleOverrides: {
root: {
"& .MuiAlert-icon": {
color: "#ff66d9",
},
},
},
},
// Additional dark mode specific components
MuiPaper: {
styleOverrides: {
root: {
backgroundColor: '#1e1e1e',
},
},
},
MuiAppBar: {
styleOverrides: {
root: {
backgroundColor: '#121212',
},
},
},
MuiDrawer: {
styleOverrides: {
paper: {
backgroundColor: '#121212',
},
},
},
MuiTableCell: {
styleOverrides: {
root: {
borderBottom: '1px solid rgba(255, 255, 255, 0.12)',
},
},
},
MuiDivider: {
styleOverrides: {
root: {
borderColor: 'rgba(255, 255, 255, 0.12)',
},
},
},
},
});
const ThemeModeContext = createContext({
toggleTheme: () => {},
darkMode: false,
});
export const useThemeMode = () => useContext(ThemeModeContext);
const ThemeModeProvider = ({ children }: { children: React.ReactNode }) => {
// Load saved mode from localStorage or default to light mode
const [darkMode, setDarkMode] = useState(() => {
const savedMode = localStorage.getItem('darkMode');
return savedMode ? JSON.parse(savedMode) : false;
});
const toggleTheme = () => {
setDarkMode((prevMode: any) => {
const newMode = !prevMode;
localStorage.setItem('darkMode', JSON.stringify(newMode)); // Save new mode to localStorage
return newMode;
});
};
useEffect(() => {
localStorage.setItem('darkMode', JSON.stringify(darkMode)); // Save initial mode
}, [darkMode]);
return (
<ThemeModeContext.Provider value={{ toggleTheme, darkMode }}>
<ThemeProvider theme={darkMode ? darkTheme : lightTheme}>
<CssBaseline />
{children}
</ThemeProvider>
</ThemeModeContext.Provider>
);
};
export default ThemeModeProvider;

View File

@@ -4,7 +4,7 @@ import {
VIEWPORT_W, VIEWPORT_W,
VIEWPORT_H, VIEWPORT_H,
} from "../constants/const"; } from "../constants/const";
import { Coordinates } from '../components/atoms/canvas'; import { Coordinates } from '../components/recorder/canvas';
export const throttle = (callback: any, limit: number) => { export const throttle = (callback: any, limit: number) => {
let wait = false; let wait = false;

View File

@@ -11,6 +11,7 @@ body {
padding: 0; padding: 0;
scrollbar-gutter: stable; scrollbar-gutter: stable;
overflow-y: auto; overflow-y: auto;
} }
html { html {
@@ -43,6 +44,7 @@ code {
align-items: center; align-items: center;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
} }
#browser-content { #browser-content {
@@ -54,6 +56,11 @@ code {
transform-origin: top left; /* Keep the position fixed */ transform-origin: top left; /* Keep the position fixed */
} }
#browser {
}
#browser-window { #browser-window {
overflow-y: auto; overflow-y: auto;
height: 100%; height: 100%;

View File

@@ -1,12 +1,13 @@
import axios from "axios"; import axios from "axios";
import { useState, useContext, useEffect, FormEvent } from "react"; import { useState, useContext, useEffect } from "react";
import { useNavigate, Link } from "react-router-dom"; import { useNavigate, Link } from "react-router-dom";
import { AuthContext } from "../context/auth"; import { AuthContext } from "../context/auth";
import { Box, Typography, TextField, Button, CircularProgress, Grid } from "@mui/material"; import { Box, Typography, TextField, Button, CircularProgress } from "@mui/material";
import { useGlobalInfoStore } from "../context/globalInfo"; import { useGlobalInfoStore } from "../context/globalInfo";
import { apiUrl } from "../apiConfig"; import { apiUrl } from "../apiConfig";
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import i18n from '../i18n'; import i18n from '../i18n';
import { useThemeMode } from "../context/theme-provider";
const Login = () => { const Login = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -17,12 +18,14 @@ const Login = () => {
email: "", email: "",
password: "", password: "",
}); });
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { notify } = useGlobalInfoStore(); const { notify } = useGlobalInfoStore();
const { email, password } = form; const { email, password } = form;
const { state, dispatch } = useContext(AuthContext); const { state, dispatch } = useContext(AuthContext);
const { user } = state; const { user } = state;
const { darkMode } = useThemeMode();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -41,10 +44,11 @@ const Login = () => {
e.preventDefault(); e.preventDefault();
setLoading(true); setLoading(true);
try { try {
const { data } = await axios.post(`${apiUrl}/auth/login`, { const { data } = await axios.post(
email, `${apiUrl}/auth/login`,
password, { email, password },
}); { withCredentials: true }
);
dispatch({ type: "LOGIN", payload: data }); dispatch({ type: "LOGIN", payload: data });
notify("success", t('login.welcome_notification')); notify("success", t('login.welcome_notification'));
window.localStorage.setItem("user", JSON.stringify(data)); window.localStorage.setItem("user", JSON.stringify(data));
@@ -64,6 +68,7 @@ const Login = () => {
maxHeight: "100vh", maxHeight: "100vh",
mt: 6, mt: 6,
padding: 4, padding: 4,
backgroundColor: darkMode ? "#121212" : "#ffffff",
}} }}
> >
<Box <Box
@@ -71,14 +76,15 @@ const Login = () => {
onSubmit={submitForm} onSubmit={submitForm}
sx={{ sx={{
textAlign: "center", textAlign: "center",
backgroundColor: "#ffffff", backgroundColor: darkMode ? "#1e1e1e" : "#ffffff",
color: darkMode ? "#ffffff" : "#333333",
padding: 6, padding: 6,
borderRadius: 5, borderRadius: 5,
boxShadow: "0px 20px 40px rgba(0, 0, 0, 0.2), 0px -5px 10px rgba(0, 0, 0, 0.15)", boxShadow: "0px 20px 40px rgba(0, 0, 0, 0.2), 0px -5px 10px rgba(0, 0, 0, 0.15)",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
alignItems: "center", alignItems: "center",
maxWidth: 400, maxWidth: 500,
width: "100%", width: "100%",
}} }}
> >
@@ -112,7 +118,10 @@ const Login = () => {
fullWidth fullWidth
variant="contained" variant="contained"
color="primary" color="primary"
sx={{ mt: 2, mb: 2 }} sx={{
mt: 2,
mb: 2,
}}
disabled={loading || !email || !password} disabled={loading || !email || !password}
> >
{loading ? ( {loading ? (

View File

@@ -1,23 +1,24 @@
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { MainMenu } from "../components/organisms/MainMenu"; import { MainMenu } from "../components/dashboard/MainMenu";
import { Stack } from "@mui/material"; import { Stack } from "@mui/material";
import { Recordings } from "../components/organisms/Recordings"; import { Recordings } from "../components/robot/Recordings";
import { Runs } from "../components/organisms/Runs"; import { Runs } from "../components/run/Runs";
import ProxyForm from '../components/organisms/ProxyForm'; import ProxyForm from '../components/proxy/ProxyForm';
import ApiKey from '../components/organisms/ApiKey'; import ApiKey from '../components/api/ApiKey';
import { useGlobalInfoStore } from "../context/globalInfo"; import { useGlobalInfoStore } from "../context/globalInfo";
import { createRunForStoredRecording, interpretStoredRecording, notifyAboutAbort, scheduleStoredRecording } from "../api/storage"; import { createRunForStoredRecording, interpretStoredRecording, notifyAboutAbort, scheduleStoredRecording } from "../api/storage";
import { io, Socket } from "socket.io-client"; import { io, Socket } from "socket.io-client";
import { stopRecording } from "../api/recording"; import { stopRecording } from "../api/recording";
import { RunSettings } from "../components/molecules/RunSettings"; import { RunSettings } from "../components/run/RunSettings";
import { ScheduleSettings } from "../components/molecules/ScheduleSettings"; import { ScheduleSettings } from "../components/robot/ScheduleSettings";
import { IntegrationSettings } from "../components/molecules/IntegrationSettings"; import { IntegrationSettings } from "../components/integration/IntegrationSettings";
import { RobotSettings } from "../components/molecules/RobotSettings"; import { RobotSettings } from "../components/robot/RobotSettings";
import { apiUrl } from "../apiConfig"; import { apiUrl } from "../apiConfig";
interface MainPageProps { interface MainPageProps {
handleEditRecording: (id: string, fileName: string) => void; handleEditRecording: (id: string, fileName: string) => void;
initialContent: string;
} }
export interface CreateRunResponse { export interface CreateRunResponse {
@@ -30,9 +31,9 @@ export interface ScheduleRunResponse {
runId: string; runId: string;
} }
export const MainPage = ({ handleEditRecording }: MainPageProps) => { export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [content, setContent] = React.useState('recordings'); const [content, setContent] = React.useState(initialContent);
const [sockets, setSockets] = React.useState<Socket[]>([]); const [sockets, setSockets] = React.useState<Socket[]>([]);
const [runningRecordingId, setRunningRecordingId] = React.useState(''); const [runningRecordingId, setRunningRecordingId] = React.useState('');
const [runningRecordingName, setRunningRecordingName] = React.useState(''); const [runningRecordingName, setRunningRecordingName] = React.useState('');
@@ -123,7 +124,7 @@ export const MainPage = ({ handleEditRecording }: MainPageProps) => {
const DisplayContent = () => { const DisplayContent = () => {
switch (content) { switch (content) {
case 'recordings': case 'robots':
return <Recordings return <Recordings
handleEditRecording={handleEditRecording} handleEditRecording={handleEditRecording}
handleRunRecording={handleRunRecording} handleRunRecording={handleRunRecording}

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { NavBar } from "../components/molecules/NavBar"; import { NavBar } from "../components/dashboard/NavBar";
import { SocketProvider } from "../context/socket"; import { SocketProvider } from "../context/socket";
import { BrowserDimensionsProvider } from "../context/browserDimensions"; import { BrowserDimensionsProvider } from "../context/browserDimensions";
import { AuthProvider } from '../context/auth'; import { AuthProvider } from '../context/auth';
@@ -7,11 +7,12 @@ import { RecordingPage } from "./RecordingPage";
import { MainPage } from "./MainPage"; import { MainPage } from "./MainPage";
import { useGlobalInfoStore } from "../context/globalInfo"; import { useGlobalInfoStore } from "../context/globalInfo";
import { getActiveBrowserId } from "../api/recording"; import { getActiveBrowserId } from "../api/recording";
import { AlertSnackbar } from "../components/atoms/AlertSnackbar"; import { AlertSnackbar } from "../components/ui/AlertSnackbar";
import Login from './Login'; import Login from './Login';
import Register from './Register'; import Register from './Register';
import UserRoute from '../routes/userRoute'; import UserRoute from '../routes/userRoute';
import { Routes, Route, useNavigate } from 'react-router-dom'; import { Routes, Route, useNavigate, Navigate } from 'react-router-dom';
import { Runs } from '../components/run/Runs';
export const PageWrapper = () => { export const PageWrapper = () => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@@ -53,7 +54,11 @@ export const PageWrapper = () => {
{!browserId && <NavBar recordingName={recordingName} isRecording={!!browserId} />} {!browserId && <NavBar recordingName={recordingName} isRecording={!!browserId} />}
<Routes> <Routes>
<Route element={<UserRoute />}> <Route element={<UserRoute />}>
<Route path="/" element={<MainPage handleEditRecording={handleEditRecording} />} /> <Route path="/" element={<Navigate to="/robots" replace />} />
<Route path="/robots/*" element={<MainPage handleEditRecording={handleEditRecording} initialContent="robots" />} />
<Route path="/runs/*" element={<MainPage handleEditRecording={handleEditRecording} initialContent="runs" />} />
<Route path="/proxy" element={<MainPage handleEditRecording={handleEditRecording} initialContent="proxy" />} />
<Route path="/apikey" element={<MainPage handleEditRecording={handleEditRecording} initialContent="apikey" />} />
</Route> </Route>
<Route element={<UserRoute />}> <Route element={<UserRoute />}>
<Route path="/recording" element={ <Route path="/recording" element={

View File

@@ -1,11 +1,11 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { Grid } from '@mui/material'; import { Grid } from '@mui/material';
import { BrowserContent } from "../components/organisms/BrowserContent"; import { BrowserContent } from "../components/browser/BrowserContent";
import { InterpretationLog } from "../components/molecules/InterpretationLog"; import { InterpretationLog } from "../components/run/InterpretationLog";
import { startRecording, getActiveBrowserId } from "../api/recording"; import { startRecording, getActiveBrowserId } from "../api/recording";
import { LeftSidePanel } from "../components/organisms/LeftSidePanel"; import { LeftSidePanel } from "../components/recorder/LeftSidePanel";
import { RightSidePanel } from "../components/organisms/RightSidePanel"; import { RightSidePanel } from "../components/recorder/RightSidePanel";
import { Loader } from "../components/atoms/Loader"; import { Loader } from "../components/ui/Loader";
import { useSocketStore } from "../context/socket"; import { useSocketStore } from "../context/socket";
import { useBrowserDimensionsStore } from "../context/browserDimensions"; import { useBrowserDimensionsStore } from "../context/browserDimensions";
import { ActionProvider } from "../context/browserActions" import { ActionProvider } from "../context/browserActions"
@@ -14,7 +14,8 @@ import { useGlobalInfoStore } from "../context/globalInfo";
import { editRecordingFromStorage } from "../api/storage"; import { editRecordingFromStorage } from "../api/storage";
import { WhereWhatPair } from "maxun-core"; import { WhereWhatPair } from "maxun-core";
import styled from "styled-components"; import styled from "styled-components";
import BrowserRecordingSave from '../components/molecules/BrowserRecordingSave'; import BrowserRecordingSave from '../components/browser/BrowserRecordingSave';
import { useThemeMode } from '../context/theme-provider';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
interface RecordingPageProps { interface RecordingPageProps {
@@ -27,6 +28,7 @@ export interface PairForEdit {
} }
export const RecordingPage = ({ recordingName }: RecordingPageProps) => { export const RecordingPage = ({ recordingName }: RecordingPageProps) => {
const { darkMode } = useThemeMode();
const { t } = useTranslation(); const { t } = useTranslation();
const [isLoaded, setIsLoaded] = React.useState(false); const [isLoaded, setIsLoaded] = React.useState(false);
const [hasScrollbar, setHasScrollbar] = React.useState(false); const [hasScrollbar, setHasScrollbar] = React.useState(false);
@@ -34,6 +36,7 @@ export const RecordingPage = ({ recordingName }: RecordingPageProps) => {
pair: null, pair: null,
index: 0, index: 0,
}); });
const [showOutputData, setShowOutputData] = useState(false); const [showOutputData, setShowOutputData] = useState(false);
const browserContentRef = React.useRef<HTMLDivElement>(null); const browserContentRef = React.useRef<HTMLDivElement>(null);
@@ -57,15 +60,20 @@ export const RecordingPage = ({ recordingName }: RecordingPageProps) => {
useEffect(() => changeBrowserDimensions(), [isLoaded]) useEffect(() => changeBrowserDimensions(), [isLoaded])
useEffect(() => { useEffect(() => {
document.body.style.background = 'radial-gradient(circle, rgba(255, 255, 255, 1) 0%, rgba(232, 191, 222, 1) 100%, rgba(255, 255, 255, 1) 100%)'; if (darkMode) {
document.body.style.filter = 'progid:DXImageTransform.Microsoft.gradient(startColorstr="#ffffff",endColorstr="#ffffff",GradientType=1);'
document.body.style.background = 'rgba(18,18,18,1)';
} else {
document.body.style.background = 'radial-gradient(circle, rgba(255, 255, 255, 1) 0%, rgba(232, 191, 222, 1) 100%, rgba(255, 255, 255, 1) 100%)';
document.body.style.filter = 'progid:DXImageTransform.Microsoft.gradient(startColorstr="#ffffff",endColorstr="#ffffff",GradientType=1);'
}
return () => { return () => {
// Cleanup the background when leaving the page
document.body.style.background = ''; document.body.style.background = '';
document.body.style.filter = ''; document.body.style.filter = '';
}; };
}, []); }, [darkMode]);
useEffect(() => { useEffect(() => {
let isCancelled = false; let isCancelled = false;

View File

@@ -5,11 +5,11 @@ import { AuthContext } from "../context/auth";
import { Box, Typography, TextField, Button, CircularProgress } from "@mui/material"; import { Box, Typography, TextField, Button, CircularProgress } from "@mui/material";
import { useGlobalInfoStore } from "../context/globalInfo"; import { useGlobalInfoStore } from "../context/globalInfo";
import { apiUrl } from "../apiConfig"; import { apiUrl } from "../apiConfig";
import { useThemeMode } from "../context/theme-provider";
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import i18n from '../i18n'; import i18n from '../i18n';
const Register = () => { const Register = () => {
const {t} = useTranslation(); const {t} = useTranslation();
const [form, setForm] = useState({ const [form, setForm] = useState({
@@ -22,6 +22,7 @@ const Register = () => {
const { state, dispatch } = useContext(AuthContext); const { state, dispatch } = useContext(AuthContext);
const { user } = state; const { user } = state;
const { darkMode } = useThemeMode();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -40,18 +41,14 @@ const Register = () => {
e.preventDefault(); e.preventDefault();
setLoading(true); setLoading(true);
try { try {
const { data } = await axios.post(`${apiUrl}/auth/register`, { const { data } = await axios.post(`${apiUrl}/auth/register`, { email, password });
email, console.log(data);
password,
});
dispatch({ type: "LOGIN", payload: data }); dispatch({ type: "LOGIN", payload: data });
notify("success", t('register.welcome_notification')); notify("success", t('register.welcome_notification'));
window.localStorage.setItem("user", JSON.stringify(data)); window.localStorage.setItem("user", JSON.stringify(data));
navigate("/"); navigate("/");
} catch (error:any) { } catch (error:any) {
notify("error", error.response.data || t('register.error_notification')); notify("error", error.response.data || t('register.error_notification'));
setLoading(false); setLoading(false);
} }
}; };
@@ -65,25 +62,38 @@ const Register = () => {
maxHeight: "100vh", maxHeight: "100vh",
mt: 6, mt: 6,
padding: 4, padding: 4,
backgroundColor: darkMode ? "#121212" : "#ffffff",
}} }}
> >
<Box <Box
component="form" component="form"
onSubmit={submitForm} onSubmit={submitForm}
sx={{ sx={{
textAlign: "center", textAlign: "center",
backgroundColor: "#ffffff", backgroundColor: darkMode ? "#1e1e1e" : "#ffffff",
padding: 6, color: darkMode ? "#ffffff" : "#333333",
borderRadius: 5, padding: 6,
boxShadow: "0px 20px 40px rgba(0, 0, 0, 0.2), 0px -5px 10px rgba(0, 0, 0, 0.15)", borderRadius: 5,
display: "flex", boxShadow: "0px 20px 40px rgba(0, 0, 0, 0.2), 0px -5px 10px rgba(0, 0, 0, 0.15)",
flexDirection: "column", display: "flex",
alignItems: "center", flexDirection: "column",
maxWidth: 400, alignItems: "center",
width: "100%", maxWidth: 500,
}} width: "100%",
}}
> >
<img src="../src/assets/maxunlogo.png" alt="logo" height={55} width={60} style={{ marginBottom: 20, borderRadius: "20%", alignItems: "center" }} /> <img
src="../src/assets/maxunlogo.png"
alt="logo"
height={55}
width={60}
style={{
marginBottom: 20,
borderRadius: "20%",
alignItems: "center",
}}
/>
<Typography variant="h4" gutterBottom> <Typography variant="h4" gutterBottom>
{t('register.title')} {t('register.title')}
</Typography> </Typography>
@@ -113,7 +123,10 @@ const Register = () => {
fullWidth fullWidth
variant="contained" variant="contained"
color="primary" color="primary"
sx={{ mt: 2, mb: 2 }} sx={{
mt: 2,
mb: 2,
}}
disabled={loading || !email || !password} disabled={loading || !email || !password}
> >
{loading ? ( {loading ? (
@@ -125,10 +138,9 @@ const Register = () => {
t('register.button') t('register.button')
)} )}
</Button> </Button>
<Typography variant="body2" align="center"> <Typography variant="body2" align="center" sx={{ color: darkMode ? "#ffffff" : "#333333" }}>
{t('register.register_prompt')}{" "} {t('register.register_prompt')}{" "}
<Link to="/login" style={{ textDecoration: "none", color: "#ff33cc" }}> <Link to="/login" style={{ textDecoration: "none", color: "#ff33cc" }}>
{t('register.login_link')} {t('register.login_link')}
</Link> </Link>
</Typography> </Typography>