diff --git a/README.md b/README.md
index 47e170b5..376bf25b 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,7 @@ Maxun lets you train a robot in 2 minutes and scrape the web on auto-pilot. Web
+ Documentation |
Website |
Discord |
Twitter |
diff --git a/docker-compose.yml b/docker-compose.yml
index 874e48d6..91b72428 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -43,7 +43,7 @@ services:
#build:
#context: .
#dockerfile: server/Dockerfile
- image: getmaxun/maxun-backend:v0.0.9
+ image: getmaxun/maxun-backend:v0.0.10
ports:
- "${BACKEND_PORT:-8080}:${BACKEND_PORT:-8080}"
env_file: .env
@@ -70,7 +70,7 @@ services:
#build:
#context: .
#dockerfile: Dockerfile
- image: getmaxun/maxun-frontend:v0.0.5
+ image: getmaxun/maxun-frontend:v0.0.7
ports:
- "${FRONTEND_PORT:-5173}:${FRONTEND_PORT:-5173}"
env_file: .env
diff --git a/maxun-core/package.json b/maxun-core/package.json
index 7c92d08e..ddaaa510 100644
--- a/maxun-core/package.json
+++ b/maxun-core/package.json
@@ -1,6 +1,6 @@
{
"name": "maxun-core",
- "version": "0.0.7",
+ "version": "0.0.8",
"description": "Core package for Maxun, responsible for data extraction",
"main": "build/index.js",
"typings": "build/index.d.ts",
diff --git a/package.json b/package.json
index e89f13de..36062666 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "maxun",
- "version": "0.0.5",
+ "version": "0.0.6",
"author": "Maxun",
"license": "AGPL-3.0-or-later",
"dependencies": {
@@ -44,9 +44,10 @@
"joi": "^17.6.0",
"jsonwebtoken": "^9.0.2",
"jwt-decode": "^4.0.0",
+ "lodash": "^4.17.21",
"loglevel": "^1.8.0",
"loglevel-plugin-remote": "^0.6.8",
- "maxun-core": "^0.0.7",
+ "maxun-core": "^0.0.8",
"minio": "^8.0.1",
"moment-timezone": "^0.5.45",
"node-cron": "^3.0.3",
@@ -66,6 +67,7 @@
"react-transition-group": "^4.4.2",
"sequelize": "^6.37.3",
"sequelize-typescript": "^2.1.6",
+ "sharp": "^0.33.5",
"socket.io": "^4.4.1",
"socket.io-client": "^4.4.1",
"styled-components": "^5.3.3",
@@ -97,6 +99,7 @@
"@types/cookie-parser": "^1.4.7",
"@types/express": "^4.17.13",
"@types/js-cookie": "^3.0.6",
+ "@types/lodash": "^4.17.14",
"@types/loglevel": "^1.6.3",
"@types/node": "22.7.9",
"@types/node-cron": "^3.0.11",
diff --git a/perf/performance.ts b/perf/performance.ts
new file mode 100644
index 00000000..c50ef850
--- /dev/null
+++ b/perf/performance.ts
@@ -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
+ ): Promise {
+ 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;
+}
\ No newline at end of file
diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts
index 2c45d146..8ff4f601 100644
--- a/server/src/browser-management/classes/RemoteBrowser.ts
+++ b/server/src/browser-management/classes/RemoteBrowser.ts
@@ -9,6 +9,8 @@ import { chromium } from 'playwright-extra';
import stealthPlugin from 'puppeteer-extra-plugin-stealth';
import { PlaywrightBlocker } from '@cliqz/adblocker-playwright';
import fetch from 'cross-fetch';
+import { throttle } from 'lodash';
+import sharp from 'sharp';
import logger from '../../logger';
import { InterpreterSettings, RemoteBrowserOptions } from "../../types";
@@ -16,8 +18,30 @@ import { WorkflowGenerator } from "../../workflow-management/classes/Generator";
import { WorkflowInterpreter } from "../../workflow-management/classes/Interpreter";
import { getDecryptedProxyConfig } from '../../routes/proxy';
import { getInjectableScript } from 'idcac-playwright';
+
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.
@@ -78,6 +102,11 @@ export class RemoteBrowser {
*/
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
* assigns the socket instance everywhere.
@@ -90,6 +119,46 @@ export class RemoteBrowser {
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 {
+ 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
*/
@@ -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 10.0; Win64; x64; rv:118.0) Gecko/20100101 Firefox/118.0',
];
-
+
return userAgents[Math.floor(Math.random() * userAgents.length)];
}
@@ -178,7 +247,7 @@ export class RemoteBrowser {
"--disable-extensions",
"--no-sandbox",
"--disable-dev-shm-usage",
- ],
+ ],
}));
const proxyConfig = await getDecryptedProxyConfig(userId);
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);
await blocker.disableBlockingInPage(this.currentPage);
console.log('Adblocker initialized');
- } catch (error: any) {
+ } catch (error: any) {
console.warn('Failed to initialize adblocker, continuing without it:', error.message);
// Still need to set up the CDP session even if blocker fails
this.client = await this.currentPage.context().newCDPSession(this.currentPage);
- }
+ }
};
/**
@@ -319,7 +388,7 @@ export class RemoteBrowser {
return;
}
this.client.on('Page.screencastFrame', ({ data: base64, sessionId }) => {
- this.emitScreenshot(base64)
+ this.emitScreenshot(Buffer.from(base64, 'base64'))
setTimeout(async () => {
try {
if (!this.client) {
@@ -339,16 +408,49 @@ export class RemoteBrowser {
* If an interpretation was running it will be stopped.
* @returns {Promise}
*/
- public switchOff = async (): Promise => {
- await this.interpreter.stopInterpretation();
- if (this.browser) {
- await this.stopScreencast();
- await this.browser.close();
- } else {
- logger.log('error', 'Browser wasn\'t initialized');
- logger.log('error', 'Switching off the browser failed');
+ public async switchOff(): Promise {
+ try {
+ await this.interpreter.stopInterpretation();
+
+ if (this.screencastInterval) {
+ clearInterval(this.screencastInterval);
+ }
+
+ 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 {
+ 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.
@@ -358,7 +460,7 @@ export class RemoteBrowser {
try {
const screenshot = await this.currentPage?.screenshot();
if (screenshot) {
- this.emitScreenshot(screenshot.toString('base64'));
+ this.emitScreenshot(screenshot);
}
} catch (e) {
const { message } = e as Error;
@@ -490,37 +592,85 @@ export class RemoteBrowser {
* Should be called only once after the browser is fully initialized.
* @returns {Promise}
*/
- private startScreencast = async (): Promise => {
+ private async startScreencast(): Promise {
if (!this.client) {
- logger.log('warn', 'client is not initialized');
+ logger.warn('Client is not initialized');
return;
}
- await this.client.send('Page.startScreencast', { format: 'jpeg', quality: 75 });
- logger.log('info', `Browser started with screencasting a page.`);
- };
- /**
- * Unsubscribes the current page from the screencast session.
- * @returns {Promise}
- */
- private stopScreencast = async (): Promise => {
- if (!this.client) {
- logger.log('error', 'client is not initialized');
- logger.log('error', 'Screencast stop failed');
- } else {
- await this.client.send('Page.stopScreencast');
- logger.log('info', `Browser stopped with screencasting.`);
+ try {
+ await this.client.send('Page.startScreencast', {
+ format: SCREENCAST_CONFIG.format,
+ });
+
+ // Set up screencast frame handler
+ this.client.on('Page.screencastFrame', async ({ data, sessionId }) => {
+ try {
+ const buffer = Buffer.from(data, 'base64');
+ await this.emitScreenshot(buffer);
+ 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 {
+ 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.
* @param payload the screenshot binary data
* @returns void
*/
- private emitScreenshot = (payload: any): void => {
- const dataWithMimeType = ('data:image/jpeg;base64,').concat(payload);
- this.socket.emit('screencast', dataWithMimeType);
- logger.log('debug', `Screenshot emitted`);
+ private emitScreenshot = async (payload: Buffer): Promise => {
+ if (this.isProcessingScreenshot) {
+ if (this.screenshotQueue.length < SCREENCAST_CONFIG.maxQueueSize) {
+ 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);
+ }
+ }
+ }
};
+
}
diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts
index 084edb4b..837fcd8a 100644
--- a/server/src/routes/auth.ts
+++ b/server/src/routes/auth.ts
@@ -384,7 +384,7 @@ router.get(
httpOnly: false,
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) {
res.status(500).json({ message: `Google OAuth error: ${error.message}` });
}
diff --git a/server/src/workflow-management/classes/Interpreter.ts b/server/src/workflow-management/classes/Interpreter.ts
index b982b172..c8aec13c 100644
--- a/server/src/workflow-management/classes/Interpreter.ts
+++ b/server/src/workflow-management/classes/Interpreter.ts
@@ -6,14 +6,26 @@ import { InterpreterSettings } from "../../types";
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 checkLimit If true, it will handle the limit validation for scrapeList action.
*/
-function decryptWorkflow(workflow: WorkflowFile): WorkflowFile {
- const decryptedWorkflow = JSON.parse(JSON.stringify(workflow)) as WorkflowFile;
+function processWorkflow(workflow: WorkflowFile, checkLimit: boolean = false): WorkflowFile {
+ const processedWorkflow = JSON.parse(JSON.stringify(workflow)) as WorkflowFile;
- decryptedWorkflow.workflow.forEach((pair) => {
+ processedWorkflow.workflow.forEach((pair) => {
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) {
try {
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;
delete settings.params;
- const decryptedWorkflow = decryptWorkflow(workflow);
+ const processedWorkflow = processWorkflow(workflow, true);
const options = {
...settings,
@@ -178,7 +190,7 @@ export class WorkflowInterpreter {
}
}
- const interpreter = new Interpreter(decryptedWorkflow, options);
+ const interpreter = new Interpreter(processedWorkflow, options);
this.interpreter = interpreter;
interpreter.on('flag', async (page, resume) => {
@@ -253,7 +265,7 @@ export class WorkflowInterpreter {
const params = settings.params ? settings.params : null;
delete settings.params;
- const decryptedWorkflow = decryptWorkflow(workflow);
+ const processedWorkflow = processWorkflow(workflow);
const options = {
...settings,
@@ -277,7 +289,7 @@ export class WorkflowInterpreter {
}
}
- const interpreter = new Interpreter(decryptedWorkflow, options);
+ const interpreter = new Interpreter(processedWorkflow, options);
this.interpreter = interpreter;
interpreter.on('flag', async (page, resume) => {
diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts
index 82464e63..8a9096ec 100644
--- a/server/src/workflow-management/selector.ts
+++ b/server/src/workflow-management/selector.ts
@@ -175,7 +175,17 @@ export const getElementInformation = async (
info.innerText = targetElement.textContent ?? '';
} else if (targetElement.tagName === 'IMG') {
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 &&
(targetElement.textContent !== null &&
targetElement.textContent.trim().length > 0);
diff --git a/src/App.tsx b/src/App.tsx
index 02dff134..cdee8d40 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -4,6 +4,7 @@ import { ThemeProvider, createTheme } from "@mui/material/styles";
import { GlobalInfoProvider } from "./context/globalInfo";
import { PageWrapper } from "./pages/PageWrappper";
import i18n from "./i18n";
+import ThemeModeProvider from './context/theme-provider';
const theme = createTheme({
@@ -85,15 +86,23 @@ const theme = createTheme({
function App() {
return (
-
+
+
+
+ } />
+
+
+
+
+ //
-
-
- } />
-
-
+ //
+ //
+ // } />
+ //
+ //
-
+ //
);
}
diff --git a/src/api/storage.ts b/src/api/storage.ts
index 18c793c0..201a38cc 100644
--- a/src/api/storage.ts
+++ b/src/api/storage.ts
@@ -1,7 +1,7 @@
import { default as axios } from "axios";
import { WorkflowFile } from "maxun-core";
-import { RunSettings } from "../components/molecules/RunSettings";
-import { ScheduleSettings } from "../components/molecules/ScheduleSettings";
+import { RunSettings } from "../components/run/RunSettings";
+import { ScheduleSettings } from "../components/robot/ScheduleSettings";
import { CreateRunResponse, ScheduleRunResponse } from "../pages/MainPage";
import { apiUrl } from "../apiConfig";
diff --git a/src/components/molecules/ActionDescriptionBox.tsx b/src/components/action/ActionDescriptionBox.tsx
similarity index 74%
rename from src/components/molecules/ActionDescriptionBox.tsx
rename to src/components/action/ActionDescriptionBox.tsx
index 45ec1641..97c979ec 100644
--- a/src/components/molecules/ActionDescriptionBox.tsx
+++ b/src/components/action/ActionDescriptionBox.tsx
@@ -5,19 +5,24 @@ import { useActionContext } from '../../context/browserActions';
import MaxunLogo from "../../assets/maxunlogo.png";
import { useTranslation } from 'react-i18next';
-const CustomBoxContainer = styled.div`
+interface CustomBoxContainerProps {
+ isDarkMode: boolean;
+}
+
+const CustomBoxContainer = styled.div`
position: relative;
min-width: 250px;
width: auto;
min-height: 100px;
height: auto;
- // border: 2px solid #ff00c3;
border-radius: 5px;
- background-color: white;
+ background-color: ${({ isDarkMode }) => (isDarkMode ? '#313438' : 'white')};
+ color: ${({ isDarkMode }) => (isDarkMode ? 'white' : 'black')};
margin: 80px 13px 25px 13px;
+ box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
`;
-const Triangle = styled.div`
+const Triangle = styled.div`
position: absolute;
top: -15px;
left: 50%;
@@ -26,7 +31,7 @@ const Triangle = styled.div`
height: 0;
border-left: 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`
@@ -44,7 +49,8 @@ const Content = styled.div`
text-align: left;
`;
-const ActionDescriptionBox = () => {
+
+const ActionDescriptionBox = ({ isDarkMode }: { isDarkMode: boolean }) => {
const { t } = useTranslation();
const { getText, getScreenshot, getList, captureStage } = useActionContext() as {
getText: boolean;
@@ -93,9 +99,19 @@ const ActionDescriptionBox = () => {
}
- label={{text} }
+ label={
+
+ {text}
+
+ }
/>
))}
@@ -112,9 +128,9 @@ const ActionDescriptionBox = () => {
};
return (
-
-
-
+
+
+
{renderActionDescription()}
diff --git a/src/components/molecules/ActionSettings.tsx b/src/components/action/ActionSettings.tsx
similarity index 70%
rename from src/components/molecules/ActionSettings.tsx
rename to src/components/action/ActionSettings.tsx
index 79e120b8..a0a3aa59 100644
--- a/src/components/molecules/ActionSettings.tsx
+++ b/src/components/action/ActionSettings.tsx
@@ -1,16 +1,15 @@
import React, { useRef } from 'react';
import styled from "styled-components";
import { Button } from "@mui/material";
-//import { ActionDescription } from "../organisms/RightSidePanel";
import * as Settings from "./action-settings";
import { useSocketStore } from "../../context/socket";
interface ActionSettingsProps {
action: string;
+ darkMode?: boolean;
}
-export const ActionSettings = ({ action }: ActionSettingsProps) => {
-
+export const ActionSettings = ({ action, darkMode = false }: ActionSettingsProps) => {
const settingsRef = useRef<{ getSettings: () => object }>(null);
const { socket } = useSocketStore();
@@ -20,30 +19,27 @@ export const ActionSettings = ({ action }: ActionSettingsProps) => {
return ;
case 'scroll':
return ;
- case 'scrape':
- return ;
+ case 'scrape':
+ return ;
case 'scrapeSchema':
return ;
default:
return null;
}
- }
+ };
const handleSubmit = (event: React.SyntheticEvent) => {
event.preventDefault();
- //get the data from settings
const settings = settingsRef.current?.getSettings();
- //Send notification to the server and generate the pair
socket?.emit(`action`, {
action,
settings
});
- }
+ };
return (
- {/*
Action settings: */}
-
+
diff --git a/src/components/molecules/BrowserNavBar.tsx b/src/components/browser/BrowserNavBar.tsx
similarity index 69%
rename from src/components/molecules/BrowserNavBar.tsx
rename to src/components/browser/BrowserNavBar.tsx
index ece1abcd..c3db51c1 100644
--- a/src/components/molecules/BrowserNavBar.tsx
+++ b/src/components/browser/BrowserNavBar.tsx
@@ -1,27 +1,34 @@
-import type {
- FC,
-} from 'react';
+import type { FC } from 'react';
import styled from 'styled-components';
-
import ReplayIcon from '@mui/icons-material/Replay';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
-
-import { NavBarButton } from '../atoms/buttons/buttons';
+import { NavBarButton } from '../ui/buttons/buttons';
import { UrlForm } from './UrlForm';
import { useCallback, useEffect, useState } from "react";
import { useSocketStore } from "../../context/socket";
import { getCurrentUrl } from "../../api/recording";
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;
padding: 12px 0px;
- background-color: #f6f6f6;
+ background-color: ${({ isDarkMode }) => (isDarkMode ? '#2C2F33' : '#f6f6f6')};
width: ${({ browserWidth }) => browserWidth}px;
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 {
browserWidth: number;
handleUrlChanged: (url: string) => void;
@@ -31,6 +38,7 @@ const BrowserNavBar: FC = ({
browserWidth,
handleUrlChanged,
}) => {
+ const isDarkMode = useThemeMode().darkMode;
const { socket } = useSocketStore();
const { recordingUrl, setRecordingUrl } = useGlobalInfoStore();
@@ -67,7 +75,7 @@ const BrowserNavBar: FC = ({
socket.off('urlChanged', handleCurrentUrlChange);
}
}
- }, [socket, handleCurrentUrlChange])
+ }, [socket, handleCurrentUrlChange]);
const addAddress = (address: string) => {
if (socket) {
@@ -78,38 +86,41 @@ const BrowserNavBar: FC = ({
};
return (
-
-
+ {
socket?.emit('input:back');
}}
disabled={false}
+ mode={isDarkMode ? 'dark' : 'light'}
>
-
+
- {
socket?.emit('input:forward');
}}
disabled={false}
+ mode={isDarkMode ? 'dark' : 'light'}
>
-
+
- {
if (socket) {
- handleRefresh()
+ handleRefresh();
}
}}
disabled={false}
+ mode={isDarkMode ? 'dark' : 'light'}
>
-
+
{
@@ -31,14 +31,26 @@ const BrowserRecordingSave = () => {
position: 'absolute',
background: '#ff00c3',
border: 'none',
- borderRadius: '5px',
+ borderRadius: '0px 0px 8px 8px',
padding: '7.5px',
width: 'calc(100% - 20px)',
overflow: 'hidden',
display: 'flex',
justifyContent: 'space-between',
+ height: "48px"
}}>
- setOpenModal(true)} variant="outlined" style={{ marginLeft: "25px" }} size="small" color="error">
+ 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')}
setOpenModal(false)} modalStyle={modalStyle}>
@@ -48,7 +60,14 @@ const BrowserRecordingSave = () => {
{t('right_panel.buttons.discard')}
- setOpenModal(false)} variant="outlined">
+ setOpenModal(false)}
+ variant="outlined"
+ sx={{
+ color: '#ff00c3 !important',
+ borderColor: '#ff00c3 !important',
+ backgroundColor: 'whitesmoke !important',
+ }} >
{t('right_panel.buttons.cancel')}
diff --git a/src/components/molecules/BrowserTabs.tsx b/src/components/browser/BrowserTabs.tsx
similarity index 80%
rename from src/components/molecules/BrowserTabs.tsx
rename to src/components/browser/BrowserTabs.tsx
index 0ee05dfc..0b3f7051 100644
--- a/src/components/molecules/BrowserTabs.tsx
+++ b/src/components/browser/BrowserTabs.tsx
@@ -1,8 +1,8 @@
import * as React from 'react';
import { Box, IconButton, Tab, Tabs } from "@mui/material";
-import { AddButton } from "../atoms/buttons/AddButton";
import { useBrowserDimensionsStore } from "../../context/browserDimensions";
import { Close } from "@mui/icons-material";
+import { useThemeMode } from '../../context/theme-provider';
interface BrowserTabsProp {
tabs: string[],
@@ -28,15 +28,16 @@ export const BrowserTabs = (
handleChangeIndex(newValue);
}
};
+ const isDarkMode = useThemeMode().darkMode;
return (
-
+ {/* Synced border color */}
{
tabWasClosed = true;
@@ -60,8 +65,7 @@ export const BrowserTabs = (
if (!tabWasClosed) {
handleTabChange(index)
}
- }
- }
+ }}
label={tab}
/>
);
diff --git a/src/components/organisms/BrowserWindow.tsx b/src/components/browser/BrowserWindow.tsx
similarity index 95%
rename from src/components/organisms/BrowserWindow.tsx
rename to src/components/browser/BrowserWindow.tsx
index 84fac079..5eb72aa4 100644
--- a/src/components/organisms/BrowserWindow.tsx
+++ b/src/components/browser/BrowserWindow.tsx
@@ -1,9 +1,9 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useSocketStore } from '../../context/socket';
import { Button } from '@mui/material';
-import Canvas from "../atoms/canvas";
-import { Highlighter } from "../atoms/Highlighter";
-import { GenericModal } from '../atoms/GenericModal';
+import Canvas from "../recorder/canvas";
+import { Highlighter } from "../recorder/Highlighter";
+import { GenericModal } from '../ui/GenericModal';
import { useActionContext } from '../../context/browserActions';
import { useBrowserSteps, TextStep } from '../../context/browserSteps';
import { useGlobalInfoStore } from '../../context/globalInfo';
@@ -141,9 +141,9 @@ export const BrowserWindow = () => {
} else if (data.elementInfo?.isIframeContent && data.childSelectors) {
// Handle pure iframe elements - similar to previous shadow DOM logic but using iframe syntax
// 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
- childSelector.split(':>>').some(part =>
+ childSelector.split(':>>').some(part =>
data.selector.includes(part.trim())
)
);
@@ -152,9 +152,9 @@ export const BrowserWindow = () => {
// Handle mixed DOM cases with iframes
// Split the selector into parts and check each against child selectors
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
- data.childSelectors!.some(childSelector =>
+ data.childSelectors!.some(childSelector =>
childSelector.includes(part)
)
);
@@ -162,36 +162,36 @@ export const BrowserWindow = () => {
} else if (data.elementInfo?.isShadowRoot && data.childSelectors) {
// New case: Handle pure Shadow DOM elements
// 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
- childSelector.split('>>').some(part =>
+ childSelector.split('>>').some(part =>
data.selector.includes(part.trim())
)
);
setHighlighterData(isShadowChild ? data : null);
- } else if (data.selector.includes('>>') && hasValidChildSelectors) {
+ } else if (data.selector.includes('>>') && hasValidChildSelectors) {
// New case: Handle mixed DOM cases
// Split the selector into parts and check each against child selectors
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
- data.childSelectors!.some(childSelector =>
+ data.childSelectors!.some(childSelector =>
childSelector.includes(part)
)
);
setHighlighterData(isValidMixedSelector ? data : null);
- } else {
+ } else {
// if !valid child in normal mode, clear the highlighter
setHighlighterData(null);
- }
- } else {
+ }
+ } else {
// Set highlighterData for the initial listSelector selection
setHighlighterData(data);
- }
- } else {
+ }
+ } else {
// For non-list steps
setHighlighterData(data);
- }
+ }
}, [highlighterData, getList, socket, listSelector, paginationMode, paginationType, captureStage]);
@@ -379,7 +379,6 @@ export const BrowserWindow = () => {
}
}, [paginationMode, resetPaginationSelector]);
-
return (
{
@@ -405,6 +404,11 @@ export const BrowserWindow = () => {
overflow: 'hidden',
padding: '5px 10px',
}}
+ sx={{
+ color: '#ff00c3 !important',
+ borderColor: '#ff00c3 !important',
+ backgroundColor: 'whitesmoke !important',
+ }}
>
void;
}
-export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenuProps) => {
- const {t} = useTranslation();
+export const MainMenu = ({ value = 'robots', handleChangeContent }: MainMenuProps) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const navigate = useNavigate();
const handleChange = (event: React.SyntheticEvent, newValue: string) => {
+ navigate(`/${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 (
-
+
}
iconPosition="start"
@@ -101,17 +119,4 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
);
-}
-
-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',
};
\ No newline at end of file
diff --git a/src/components/molecules/NavBar.tsx b/src/components/dashboard/NavBar.tsx
similarity index 83%
rename from src/components/molecules/NavBar.tsx
rename to src/components/dashboard/NavBar.tsx
index 8aeeb05d..5f7ce6b3 100644
--- a/src/components/molecules/NavBar.tsx
+++ b/src/components/dashboard/NavBar.tsx
@@ -4,17 +4,17 @@ import axios from 'axios';
import styled from "styled-components";
import { stopRecording } from "../../api/recording";
import { useGlobalInfoStore } from "../../context/globalInfo";
-import { IconButton, Menu, MenuItem, Typography, Chip, Button, Modal, Tabs, Tab, Box, Snackbar } from "@mui/material";
-import { AccountCircle, Logout, Clear, YouTube, X, Update, Close, Language } from "@mui/icons-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, Brightness7, Brightness4, Description } from "@mui/icons-material";
import { useNavigate } from 'react-router-dom';
import { AuthContext } from '../../context/auth';
-import { SaveRecording } from '../molecules/SaveRecording';
-import DiscordIcon from '../atoms/DiscordIcon';
+import { SaveRecording } from '../recorder/SaveRecording';
+import DiscordIcon from '../icons/DiscordIcon';
import { apiUrl } from '../../apiConfig';
import MaxunLogo from "../../assets/maxunlogo.png";
+import { useThemeMode } from '../../context/theme-provider';
import packageJson from "../../../package.json"
-
interface NavBarProps {
recordingName: string;
isRecording: boolean;
@@ -28,6 +28,7 @@ export const NavBar: React.FC = ({
const { state, dispatch } = useContext(AuthContext);
const { user } = state;
const navigate = useNavigate();
+ const { darkMode, toggleTheme } = useThemeMode();
const { t, i18n } = useTranslation();
const [anchorEl, setAnchorEl] = useState(null);
@@ -102,6 +103,22 @@ export const NavBar: React.FC = ({
localStorage.setItem("language", lang);
};
+ const renderThemeToggle = () => (
+
+
+ {darkMode ? : }
+
+
+ );
+
useEffect(() => {
const checkForUpdates = async () => {
const latestVersion = await fetchLatestVersion();
@@ -158,13 +175,13 @@ export const NavBar: React.FC = ({
}}
/>
)}
-
+
-
{t('navbar.project_name')}
+
{t('navbar.project_name')}
= ({
docker-compose down
+ # replace existing docker-compose file with new one by copy pasting the code from
+
+ Latest Docker Compose
+
+
# pull latest docker images
docker-compose pull
@@ -283,7 +305,6 @@ export const NavBar: React.FC = ({
borderRadius: '5px',
padding: '8px',
marginRight: '10px',
- '&:hover': { backgroundColor: 'white', color: '#ff00c3' }
}}>
{user.email}
@@ -305,6 +326,11 @@ export const NavBar: React.FC = ({
{ handleMenuClose(); logout(); }}>
{t('navbar.menu_items.logout')}
+ {
+ window.open('https://docs.maxun.dev', '_blank');
+ }}>
+ Docs
+
{
window.open('https://discord.gg/5GbPjBUkws', '_blank');
}}>
@@ -376,8 +402,17 @@ export const NavBar: React.FC = ({
>
Deutsch
+ {
+ window.open('https://docs.maxun.dev/development/i18n', '_blank');
+ handleMenuClose();
+ }}
+ >
+ Add Language
+
+ {renderThemeToggle()}
>
) : (
<>
@@ -397,18 +432,19 @@ export const NavBar: React.FC = ({
)}
) : (
- <>
- {t("Language")}
-
+
+
+ {t("Language")}
+
= ({
>
Deutsch
- >
+ {
+ window.open('https://docs.maxun.dev/development/i18n', '_blank');
+ handleMenuClose();
+ }}
+ >
+ Add Language
+
+
+ {renderThemeToggle()}
+
)}
>
);
};
-const NavBarWrapper = styled.div`
+const NavBarWrapper = styled.div<{ mode: 'light' | 'dark' }>`
grid-area: navbar;
- background-color: white;
+ background-color: ${({ mode }) => (mode === 'dark' ? '#1e2124' : '#ffffff')};
padding: 5px;
display: flex;
justify-content: space-between;
- border-bottom: 1px solid #e0e0e0;
+ border-bottom: 1px solid ${({ mode }) => (mode === 'dark' ? '#333' : '#e0e0e0')};
`;
-const ProjectName = styled.b`
- color: #3f4853;
+const ProjectName = styled.b<{ mode: 'light' | 'dark' }>`
+ color: ${({ mode }) => (mode === 'dark' ? '#ffffff' : '#3f4853')};
font-size: 1.3em;
`;
+
+const NavBarRight = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ margin-left: auto;
+`;
\ No newline at end of file
diff --git a/src/components/atoms/DiscordIcon.tsx b/src/components/icons/DiscordIcon.tsx
similarity index 100%
rename from src/components/atoms/DiscordIcon.tsx
rename to src/components/icons/DiscordIcon.tsx
diff --git a/src/components/atoms/RecorderIcon.tsx b/src/components/icons/RecorderIcon.tsx
similarity index 99%
rename from src/components/atoms/RecorderIcon.tsx
rename to src/components/icons/RecorderIcon.tsx
index afed1756..ee07ec72 100644
--- a/src/components/atoms/RecorderIcon.tsx
+++ b/src/components/icons/RecorderIcon.tsx
@@ -14,7 +14,7 @@ export const RecordingIcon = () => {
textIndent: 0,
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" />
diff --git a/src/components/molecules/IntegrationSettings.tsx b/src/components/integration/IntegrationSettings.tsx
similarity index 91%
rename from src/components/molecules/IntegrationSettings.tsx
rename to src/components/integration/IntegrationSettings.tsx
index c4b13e8c..f9c397ae 100644
--- a/src/components/molecules/IntegrationSettings.tsx
+++ b/src/components/integration/IntegrationSettings.tsx
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react";
-import { GenericModal } from "../atoms/GenericModal";
+import { GenericModal } from "../ui/GenericModal";
import {
MenuItem,
Typography,
@@ -17,6 +17,7 @@ import { apiUrl } from "../../apiConfig.js";
import Cookies from 'js-cookie';
import { useTranslation } from "react-i18next";
+
interface IntegrationProps {
isOpen: boolean;
handleStart: (data: IntegrationSettings) => void;
@@ -29,6 +30,20 @@ export interface IntegrationSettings {
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 = ({
isOpen,
handleStart,
@@ -141,14 +156,14 @@ export const IntegrationSettingsModal = ({
useEffect(() => {
// Check if there is a success message in cookies
- const status = Cookies.get("robot_auth_status");
- const message = Cookies.get("robot_auth_message");
+ const status = getCookie("robot_auth_status");
+ const message = getCookie("robot_auth_message");
if (status === "success" && message) {
notify("success", message);
// Clear the cookies after reading
- Cookies.remove("robot_auth_status");
- Cookies.remove("robot_auth_message");
+ removeCookie("robot_auth_status");
+ removeCookie("robot_auth_message");
}
// Check if we're on the callback URL
@@ -172,11 +187,11 @@ export const IntegrationSettingsModal = ({
return (
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "flex-start",
+ marginLeft: "65px",
+ }}>
{t('integration_settings.title')}
@@ -220,8 +235,8 @@ export const IntegrationSettingsModal = ({
<>
{recording.google_sheet_email && (
- {t('integration_settings.descriptions.authenticated_as', {
- email: recording.google_sheet_email
+ {t('integration_settings.descriptions.authenticated_as', {
+ email: recording.google_sheet_email
})}
)}
@@ -309,4 +324,4 @@ export const modalStyle = {
height: "fit-content",
display: "block",
padding: "20px",
-};
+};
\ No newline at end of file
diff --git a/src/components/molecules/AddWhatCondModal.tsx b/src/components/molecules/AddWhatCondModal.tsx
deleted file mode 100644
index 5fadfaa6..00000000
--- a/src/components/molecules/AddWhatCondModal.tsx
+++ /dev/null
@@ -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
('');
- const [objectIndex, setObjectIndex] = React.useState(0);
- const [args, setArgs] = React.useState<({type: string, value: (string|number|object|unknown)})[]>([]);
-
- const objectRefs = useRef<({getObject: () => object}|unknown)[]>([]);
-
- const {socket} = useSocketStore();
-
- const handleSubmit = () => {
- const argsArray: (string|number|object|unknown)[] = [];
- args.map((arg, index) => {
- switch (arg.type) {
- case 'string':
- case 'number':
- argsArray[index] = arg.value;
- break;
- case 'object':
- // @ts-ignore
- argsArray[index] = objectRefs.current[arg.value].getObject();
- }
- })
- setArgs([]);
- onClose();
- pair.what.push({
- // @ts-ignore
- action,
- args: argsArray,
- })
- socket?.emit('updatePair', {index: index-1, pair: pair});
- }
-
- return (
- {
- setArgs([]);
- onClose();
- }} modalStyle={modalStyle}>
-
-
Add what condition:
-
-
Action:
-
setAction(e.target.value)}
- value={action}
- label='action'
- />
-
- Add new argument of type:
- setArgs([...args,{type: 'string', value: null}]) }>string
- setArgs([...args,{type: 'number', value: null}]) }>number
- {
- setArgs([...args,{type: 'object', value: objectIndex}])
- setObjectIndex(objectIndex+1);
- } }>object
-
- args:
- {args.map((arg, index) => {
- // @ts-ignore
- return (
-
- {
- args.splice(index,1);
- setArgs([...args]);
- }}/>
- {index}:
- {arg.type === 'string' ?
- setArgs([
- ...args.slice(0, index),
- {type: arg.type, value: e.target.value},
- ...args.slice(index + 1)
- ])}
- value={args[index].value || ''}
- label="string"
- key={`arg-${arg.type}-${index}`}
- /> : arg.type === 'number' ?
- setArgs([
- ...args.slice(0, index),
- {type: arg.type, value: Number(e.target.value)},
- ...args.slice(index + 1)
- ])}
- value={args[index].value || ''}
- label="number"
- /> :
-
- //@ts-ignore
- objectRefs.current[arg.value] = el} key={`arg-${arg.type}-${index}`}/>
- }
-
- )})}
-
- {"Add Condition"}
-
-
-
-
- )
-}
diff --git a/src/components/molecules/AddWhereCondModal.tsx b/src/components/molecules/AddWhereCondModal.tsx
deleted file mode 100644
index e5c34015..00000000
--- a/src/components/molecules/AddWhereCondModal.tsx
+++ /dev/null
@@ -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('');
- const [additionalSettings, setAdditionalSettings] = React.useState('');
- const [newValue, setNewValue] = React.useState('');
- const [checked, setChecked] = React.useState(new Array(Object.keys(pair.where).length).fill(false));
-
- const keyValueFormRef = useRef<{getObject: () => object}>(null);
-
- const {socket} = useSocketStore();
-
- const handlePropSelect = (event: SelectChangeEvent) => {
- setWhereProp(event.target.value);
- switch (event.target.value) {
- case 'url': setNewValue(''); break;
- case 'selectors': setNewValue(['']); break;
- case 'default': return;
- }
- }
-
- const handleSubmit = () => {
- switch (whereProp) {
- case 'url':
- if (additionalSettings === 'string'){
- pair.where.url = newValue;
- } else {
- pair.where.url = { $regex: newValue };
- }
- break;
- case 'selectors':
- pair.where.selectors = newValue;
- break;
- case 'cookies':
- pair.where.cookies = keyValueFormRef.current?.getObject() as Record
- break;
- case 'before':
- pair.where.$before = newValue;
- break;
- case 'after':
- pair.where.$after = newValue;
- break;
- case 'boolean':
- const booleanArr = [];
- const deleteKeys: string[] = [];
- for (let i = 0; i < checked.length; i++) {
- if (checked[i]) {
- if (Object.keys(pair.where)[i]) {
- //@ts-ignore
- if (pair.where[Object.keys(pair.where)[i]]) {
- booleanArr.push({
- //@ts-ignore
- [Object.keys(pair.where)[i]]: pair.where[Object.keys(pair.where)[i]]});
- }
- deleteKeys.push(Object.keys(pair.where)[i]);
- }
- }
- }
- // @ts-ignore
- deleteKeys.forEach((key: string) => delete pair.where[key]);
- //@ts-ignore
- pair.where[`$${additionalSettings}`] = booleanArr;
- break;
- default:
- return;
- }
- onClose();
- setWhereProp('');
- setAdditionalSettings('');
- setNewValue('');
- socket?.emit('updatePair', {index: index-1, pair: pair});
- }
-
- return (
- {
- setWhereProp('');
- setAdditionalSettings('');
- setNewValue('');
- onClose();
- }} modalStyle={modalStyle}>
-
-
Add where condition:
-
-
- url
- selectors
- cookies
- before
- after
- boolean logic
-
-
- {whereProp ?
-
-
-
- {"Add Condition"}
-
-
- : null}
-
-
- )
-}
-
-export const modalStyle = {
- top: '40%',
- left: '50%',
- transform: 'translate(-50%, -50%)',
- width: '30%',
- backgroundColor: 'background.paper',
- p: 4,
- height:'fit-content',
- display:'block',
- padding: '20px',
-};
diff --git a/src/components/molecules/LeftSidePanelSettings.tsx b/src/components/molecules/LeftSidePanelSettings.tsx
deleted file mode 100644
index a25ff2d1..00000000
--- a/src/components/molecules/LeftSidePanelSettings.tsx
+++ /dev/null
@@ -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 (
-
- { params.length !== 0 && (
-
- Parameters:
- { params?.map((item: string, index: number) => {
- return setSettings(
- {
- ...settings,
- params: settings.params
- ? {
- ...settings.params,
- [item]: e.target.value,
- }
- : {
- [item]: e.target.value,
- },
- })}
- />
- }) }
-
- )}
- Interpreter:
- setSettings(
- {
- ...settings,
- maxConcurrency: parseInt(e.target.value),
- })}
- defaultValue={settings.maxConcurrency}
- />
- setSettings(
- {
- ...settings,
- maxRepeats: parseInt(e.target.value),
- })}
- defaultValue={settings.maxRepeats}
- />
- setSettings(
- {
- ...settings,
- debug: e.target.value === "true",
- })}
- >
- true
- false
-
- socket?.emit('settings', settings)}>change
-
- );
-}
diff --git a/src/components/molecules/PairDetail.tsx b/src/components/molecules/PairDetail.tsx
deleted file mode 100644
index aea6191b..00000000
--- a/src/components/molecules/PairDetail.tsx
+++ /dev/null
@@ -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(
- pair ? Object.keys(pair.where).map((key, index) => `${key}-${index}`) : []
- );
- const [addWhereCondOpen, setAddWhereCondOpen] = useState(false);
- const [addWhatCondOpen, setAddWhatCondOpen] = useState(false);
-
- const { socket } = useSocketStore();
-
- const handleCollapseWhere = () => {
- setCollapseWhere(!collapseWhere);
- }
-
- const handleCollapseWhat = () => {
- setCollapseWhat(!collapseWhat);
- }
-
- const handleToggle = (event: React.SyntheticEvent, nodeIds: string[]) => {
- setExpanded(nodeIds);
- };
-
- useLayoutEffect(() => {
- if (pair) {
- setPairIsSelected(true);
- }
- }, [pair])
-
- const handleChangeValue = (value: any, where: boolean, keys: (string|number)[]) => {
- // a moving reference to internal objects within pair.where or pair.what
- let schema: any = where ? pair?.where : pair?.what;
- const length = keys.length;
- for(let i = 0; i < length-1; i++) {
- const elem = keys[i];
- if( !schema[elem] ) schema[elem] = {}
- schema = schema[elem];
- }
-
- schema[keys[length-1]] = value;
- if (pair && socket) {
- socket.emit('updatePair', {index: index-1, pair: pair});
- }
- setRerender(!rerender);
- }
-
-
- const DisplayValueContent = (value: any, keys: (string|number)[], where: boolean = true) => {
- switch (typeof(value)) {
- case 'string':
- return {
- try {
- const obj = JSON.parse(e.target.value);
- handleChangeValue(obj, where, keys);
- } catch (error) {
- const num = Number(e.target.value);
- if (!isNaN(num)) {
- handleChangeValue(num, where, keys);
- }
- handleChangeValue(e.target.value, where, keys)
- }
- }}
- defaultValue={value}
- key={`text-field-${keys.join('-')}-${where}`}
- />
- case 'number':
- return handleChangeValue(Number(e.target.value), where, keys)}
- defaultValue={value}
- key={`text-field-${keys.join('-')}-${where}`}
- />
- case 'object':
- if (value) {
- if (Array.isArray(value)) {
- return (
-
- {
- value.map((element, index) => {
- return DisplayValueContent(element, [...keys, index], where);
- })
- }
- {
- let prevValue:any = where ? pair?.where : pair?.what;
- for (const key of keys) {
- prevValue = prevValue[key];
- }
- handleChangeValue([...prevValue, ''], where, keys);
- setRerender(!rerender);
- }} hoverEffect={false}/>
- {
- let prevValue:any = where ? pair?.where : pair?.what;
- for (const key of keys) {
- prevValue = prevValue[key];
- }
- prevValue.splice(-1);
- handleChangeValue(prevValue, where, keys);
- setRerender(!rerender);
- }}/>
-
- )
- } else {
- return (
- }
- defaultExpandIcon={ }
- sx={{ flexGrow: 1, overflowY: 'auto' }}
- key={`tree-view-nested-${keys.join('-')}-${where}`}
- >
- {
- Object.keys(value).map((key2, index) =>
- {
- return (
-
- { DisplayValueContent(value[key2], [...keys, key2], where) }
-
- )
- })
- }
-
- )
- }
- }
- break;
- default:
- return null;
- }
- }
-
- return (
-
- { pair &&
-
- setAddWhatCondOpen(false)}
- pair={pair} index={index}/>
- setAddWhereCondOpen(false)}
- pair={pair} index={index}/>
-
- }
- {
- pairIsSelected
- ? (
-
-
Pair number: {index}
-
{
- if (pair && socket) {
- socket.emit('updatePair', {index: index-1, pair: pair});
- pair.id = e.target.value;
- }
- }}
- value={pair ? pair.id ? pair.id : '' : ''}
- />
-
-
- Where
-
-
-
{
- setAddWhereCondOpen(true);
- }} style={{color:'rgba(0, 0, 0, 0.54)', background:'transparent'}}/>
-
-
-
- {(collapseWhere && pair && pair.where)
- ?
-
- { Object.keys(pair.where).map((key, index) => {
- return (
- }
- defaultExpandIcon={ }
- sx={{ flexGrow: 1, overflowY: 'auto' }}
- onNodeToggle={handleToggle}
- key={`tree-view-${key}-${index}`}
- >
-
- {
- // @ts-ignore
- DisplayValueContent(pair.where[key], [key])
- }
-
-
- );
- })}
-
- : null
- }
-
-
- What
-
-
-
-
{
- setAddWhatCondOpen(true);
- }} style={{color:'rgba(0, 0, 0, 0.54)', background:'transparent'}}/>
-
-
-
- {(collapseWhat && pair && pair.what)
- ?(
-
- { Object.keys(pair.what).map((key, index) => {
- return (
- }
- defaultExpandIcon={ }
- sx={{ flexGrow: 1, overflowY: 'auto' }}
- key={`tree-view-2-${key}-${index}`}
- >
-
- {
- // @ts-ignore
- DisplayValueContent(pair.what[key], [key], false)
- }
-
-
- {
- //@ts-ignore
- pair.what.splice(key, 1);
- setRerender(!rerender);
- }}/>
-
-
-
-
- );
- })}
-
- )
- : null
- }
-
- )
- :
-
- No pair from the left side panel was selected.
-
- }
-
- );
-}
-
-interface CollapseButtonProps {
- handleClick: () => void;
- isCollapsed?: boolean;
-}
-
-const CollapseButton = ({handleClick, isCollapsed } : CollapseButtonProps) => {
- return (
-
- { isCollapsed ? : }
-
- );
-}
-
-const CloseButton = ({handleClick } : CollapseButtonProps) => {
- return (
-
-
-
- );
-}
diff --git a/src/components/organisms/Recordings.tsx b/src/components/organisms/Recordings.tsx
deleted file mode 100644
index 495a25f8..00000000
--- a/src/components/organisms/Recordings.tsx
+++ /dev/null
@@ -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([]);
- const [selectedRecordingId, setSelectedRecordingId] = useState('');
- 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 (
-
- handleRunRecording(settings)}
- isTask={params.length !== 0}
- params={params}
- />
- handleScheduleRecording(settings)}
- />
- handleIntegrateRecording(selectedRecordingId, settings)}
- />
- handleSettingsRecording(selectedRecordingId, settings)}
- />
- handleEditRobot(selectedRecordingId,settings)}
- />
- handleDuplicateRobot(selectedRecordingId, settings)}
- />
-
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/src/components/atoms/DatePicker.tsx b/src/components/pickers/DatePicker.tsx
similarity index 86%
rename from src/components/atoms/DatePicker.tsx
rename to src/components/pickers/DatePicker.tsx
index 30d3b869..61363984 100644
--- a/src/components/atoms/DatePicker.tsx
+++ b/src/components/pickers/DatePicker.tsx
@@ -27,7 +27,7 @@ const DatePicker: React.FC = ({ coordinates, selector, onClose
};
return (
- = ({ coordinates, selector, onClose
autoFocus
/>
-
Cancel
-
Confirm
diff --git a/src/components/atoms/DateTimeLocalPicker.tsx b/src/components/pickers/DateTimeLocalPicker.tsx
similarity index 87%
rename from src/components/atoms/DateTimeLocalPicker.tsx
rename to src/components/pickers/DateTimeLocalPicker.tsx
index dc62a79b..c51e3540 100644
--- a/src/components/atoms/DateTimeLocalPicker.tsx
+++ b/src/components/pickers/DateTimeLocalPicker.tsx
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { useSocketStore } from '../../context/socket';
-import { Coordinates } from './canvas';
+import { Coordinates } from '../recorder/canvas';
interface DateTimeLocalPickerProps {
coordinates: Coordinates;
@@ -27,7 +27,7 @@ const DateTimeLocalPicker: React.FC
= ({ coordinates,
};
return (
- = ({ coordinates,
autoFocus
/>
-
Cancel
-
Confirm
diff --git a/src/components/atoms/Dropdown.tsx b/src/components/pickers/Dropdown.tsx
similarity index 86%
rename from src/components/atoms/Dropdown.tsx
rename to src/components/pickers/Dropdown.tsx
index c7ead64b..df695efa 100644
--- a/src/components/atoms/Dropdown.tsx
+++ b/src/components/pickers/Dropdown.tsx
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { useSocketStore } from '../../context/socket';
-import { Coordinates } from './canvas';
+import { Coordinates } from '../recorder/canvas';
interface DropdownProps {
coordinates: Coordinates;
@@ -47,20 +47,20 @@ const Dropdown = ({ coordinates, selector, options, onClose }: DropdownProps) =>
lineHeight: '18px',
padding: '0 3px',
cursor: option.disabled ? 'default' : 'default',
- backgroundColor: hoveredIndex === index ? '#0078D7' :
- option.selected ? '#0078D7' :
- option.disabled ? '#f8f8f8' : 'white',
- color: (hoveredIndex === index || option.selected) ? 'white' :
- option.disabled ? '#a0a0a0' : 'black',
+ backgroundColor: hoveredIndex === index ? '#0078D7' :
+ option.selected ? '#0078D7' :
+ option.disabled ? '#f8f8f8' : 'white',
+ color: (hoveredIndex === index || option.selected) ? 'white' :
+ option.disabled ? '#a0a0a0' : 'black',
userSelect: 'none',
});
return (
-
-
e.stopPropagation()}
>
diff --git a/src/components/atoms/TimePicker.tsx b/src/components/pickers/TimePicker.tsx
similarity index 95%
rename from src/components/atoms/TimePicker.tsx
rename to src/components/pickers/TimePicker.tsx
index 31353c7a..7877787e 100644
--- a/src/components/atoms/TimePicker.tsx
+++ b/src/components/pickers/TimePicker.tsx
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { useSocketStore } from '../../context/socket';
-import { Coordinates } from './canvas';
+import { Coordinates } from '../recorder/canvas';
interface TimePickerProps {
coordinates: Coordinates;
@@ -69,7 +69,7 @@ const TimePicker = ({ coordinates, selector, onClose }: TimePickerProps) => {
const getOptionStyle = (value: number, isHour: boolean): React.CSSProperties => {
const isHovered = isHour ? hoveredHour === value : hoveredMinute === value;
const isSelected = isHour ? selectedHour === value : selectedMinute === value;
-
+
return {
fontSize: '13.333px',
lineHeight: '18px',
@@ -85,11 +85,11 @@ const TimePicker = ({ coordinates, selector, onClose }: TimePickerProps) => {
const minutes = Array.from({ length: 60 }, (_, i) => i);
return (
-
-
e.stopPropagation()}
>
@@ -109,7 +109,7 @@ const TimePicker = ({ coordinates, selector, onClose }: TimePickerProps) => {
{/* Minutes column */}
-
+
{minutes.map((minute) => (
{
fetchProxyConfig();
}, []);
+ const theme = useThemeMode();
+ const isDarkMode = theme.darkMode;
+
return (
<>
@@ -144,6 +166,7 @@ const ProxyForm: React.FC = () => {
+
{tabIndex === 0 && (
isProxyConfigured ? (
@@ -236,14 +259,20 @@ const ProxyForm: React.FC = () => {
{t('proxy.coming_soon')}
+
+ {/*
+ Join Maxun Cloud Waitlist */}
+
{t('proxy.join_waitlist')}
+
>
)}
-
+
+
{t('proxy.alert.title')}
{t('proxy.alert.right_way')}
@@ -257,6 +286,7 @@ const ProxyForm: React.FC = () => {
{t('proxy.alert.wrong_way')}
+
{t('proxy.alert.proxy_url')} http://myusername:mypassword@proxy.com:1337
>
diff --git a/src/components/recorder/AddWhatCondModal.tsx b/src/components/recorder/AddWhatCondModal.tsx
new file mode 100644
index 00000000..a6b50fc9
--- /dev/null
+++ b/src/components/recorder/AddWhatCondModal.tsx
@@ -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('');
+ const [objectIndex, setObjectIndex] = React.useState(0);
+ const [args, setArgs] = React.useState<({ type: string, value: (string | number | object | unknown) })[]>([]);
+
+ const objectRefs = useRef<({ getObject: () => object } | unknown)[]>([]);
+
+ const { socket } = useSocketStore();
+
+ const handleSubmit = () => {
+ const argsArray: (string | number | object | unknown)[] = [];
+ args.map((arg, index) => {
+ switch (arg.type) {
+ case 'string':
+ case 'number':
+ argsArray[index] = arg.value;
+ break;
+ case 'object':
+ // @ts-ignore
+ argsArray[index] = objectRefs.current[arg.value].getObject();
+ }
+ })
+ setArgs([]);
+ onClose();
+ pair.what.push({
+ // @ts-ignore
+ action,
+ args: argsArray,
+ })
+ socket?.emit('updatePair', { index: index - 1, pair: pair });
+ }
+
+ return (
+ {
+ setArgs([]);
+ onClose();
+ }} modalStyle={modalStyle}>
+
+
Add what condition:
+
+
Action:
+
setAction(e.target.value)}
+ value={action}
+ label='action'
+ />
+
+ Add new argument of type:
+ setArgs([...args, { type: 'string', value: null }])}>string
+ setArgs([...args, { type: 'number', value: null }])}>number
+ {
+ setArgs([...args, { type: 'object', value: objectIndex }])
+ setObjectIndex(objectIndex + 1);
+ }}>object
+
+ args:
+ {args.map((arg, index) => {
+ // @ts-ignore
+ return (
+
+ {
+ args.splice(index, 1);
+ setArgs([...args]);
+ }} />
+ {index}:
+ {arg.type === 'string' ?
+ setArgs([
+ ...args.slice(0, index),
+ { type: arg.type, value: e.target.value },
+ ...args.slice(index + 1)
+ ])}
+ value={args[index].value || ''}
+ label="string"
+ key={`arg-${arg.type}-${index}`}
+ /> : arg.type === 'number' ?
+ setArgs([
+ ...args.slice(0, index),
+ { type: arg.type, value: Number(e.target.value) },
+ ...args.slice(index + 1)
+ ])}
+ value={args[index].value || ''}
+ label="number"
+ /> :
+
+ //@ts-ignore
+ objectRefs.current[arg.value] = el} key={`arg-${arg.type}-${index}`} />
+ }
+
+ )
+ })}
+
+ {"Add Condition"}
+
+
+
+
+ )
+}
diff --git a/src/components/recorder/AddWhereCondModal.tsx b/src/components/recorder/AddWhereCondModal.tsx
new file mode 100644
index 00000000..7c5c284c
--- /dev/null
+++ b/src/components/recorder/AddWhereCondModal.tsx
@@ -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('');
+ const [additionalSettings, setAdditionalSettings] = React.useState('');
+ const [newValue, setNewValue] = React.useState('');
+ const [checked, setChecked] = React.useState(new Array(Object.keys(pair.where).length).fill(false));
+
+ const keyValueFormRef = useRef<{ getObject: () => object }>(null);
+
+ const { socket } = useSocketStore();
+
+ const handlePropSelect = (event: SelectChangeEvent) => {
+ setWhereProp(event.target.value);
+ switch (event.target.value) {
+ case 'url': setNewValue(''); break;
+ case 'selectors': setNewValue(['']); break;
+ case 'default': return;
+ }
+ }
+
+ const handleSubmit = () => {
+ switch (whereProp) {
+ case 'url':
+ if (additionalSettings === 'string') {
+ pair.where.url = newValue;
+ } else {
+ pair.where.url = { $regex: newValue };
+ }
+ break;
+ case 'selectors':
+ pair.where.selectors = newValue;
+ break;
+ case 'cookies':
+ pair.where.cookies = keyValueFormRef.current?.getObject() as Record
+ break;
+ case 'before':
+ pair.where.$before = newValue;
+ break;
+ case 'after':
+ pair.where.$after = newValue;
+ break;
+ case 'boolean':
+ const booleanArr = [];
+ const deleteKeys: string[] = [];
+ for (let i = 0; i < checked.length; i++) {
+ if (checked[i]) {
+ if (Object.keys(pair.where)[i]) {
+ //@ts-ignore
+ if (pair.where[Object.keys(pair.where)[i]]) {
+ booleanArr.push({
+ //@ts-ignore
+ [Object.keys(pair.where)[i]]: pair.where[Object.keys(pair.where)[i]]
+ });
+ }
+ deleteKeys.push(Object.keys(pair.where)[i]);
+ }
+ }
+ }
+ // @ts-ignore
+ deleteKeys.forEach((key: string) => delete pair.where[key]);
+ //@ts-ignore
+ pair.where[`$${additionalSettings}`] = booleanArr;
+ break;
+ default:
+ return;
+ }
+ onClose();
+ setWhereProp('');
+ setAdditionalSettings('');
+ setNewValue('');
+ socket?.emit('updatePair', { index: index - 1, pair: pair });
+ }
+
+ return (
+ {
+ setWhereProp('');
+ setAdditionalSettings('');
+ setNewValue('');
+ onClose();
+ }} modalStyle={modalStyle}>
+
+
Add where condition:
+
+
+ url
+ selectors
+ cookies
+ before
+ after
+ boolean logic
+
+
+ {whereProp ?
+
+
+
+ {"Add Condition"}
+
+
+ : null}
+
+
+ )
+}
+
+export const modalStyle = {
+ top: '40%',
+ left: '50%',
+ transform: 'translate(-50%, -50%)',
+ width: '30%',
+ backgroundColor: 'background.paper',
+ p: 4,
+ height: 'fit-content',
+ display: 'block',
+ padding: '20px',
+};
diff --git a/src/components/molecules/DisplayWhereConditionSettings.tsx b/src/components/recorder/DisplayWhereConditionSettings.tsx
similarity index 71%
rename from src/components/molecules/DisplayWhereConditionSettings.tsx
rename to src/components/recorder/DisplayWhereConditionSettings.tsx
index 98d63459..784d26c4 100644
--- a/src/components/molecules/DisplayWhereConditionSettings.tsx
+++ b/src/components/recorder/DisplayWhereConditionSettings.tsx
@@ -1,10 +1,10 @@
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 { AddButton } from "../atoms/buttons/AddButton";
-import { RemoveButton } from "../atoms/buttons/RemoveButton";
+import { AddButton } from "../ui/buttons/AddButton";
+import { RemoveButton } from "../ui/buttons/RemoveButton";
import { KeyValueForm } from "./KeyValueForm";
-import { WarningText } from "../atoms/texts";
+import { WarningText } from "../ui/texts";
interface DisplayConditionSettingsProps {
whereProp: string;
@@ -12,15 +12,15 @@ interface DisplayConditionSettingsProps {
setAdditionalSettings: (value: any) => void;
newValue: any;
setNewValue: (value: any) => void;
- keyValueFormRef: React.RefObject<{getObject: () => object}>;
+ keyValueFormRef: React.RefObject<{ getObject: () => object }>;
whereKeys: string[];
checked: boolean[];
setChecked: (value: boolean[]) => void;
}
export const DisplayConditionSettings = (
- {whereProp, setAdditionalSettings, additionalSettings,
- setNewValue, newValue, keyValueFormRef, whereKeys, checked, setChecked}
+ { whereProp, setAdditionalSettings, additionalSettings,
+ setNewValue, newValue, keyValueFormRef, whereKeys, checked, setChecked }
: DisplayConditionSettingsProps) => {
switch (whereProp) {
case 'url':
@@ -34,7 +34,7 @@ export const DisplayConditionSettings = (
string
regex
- { additionalSettings ? setNewValue(e.target.value)}
@@ -56,20 +56,20 @@ export const DisplayConditionSettings = (
...newValue.slice(0, index),
e.target.value,
...newValue.slice(index + 1)
- ])}/>
+ ])} />
})
}
- setNewValue([...newValue, ''])}/>
- {
+ setNewValue([...newValue, ''])} />
+ {
const arr = newValue;
arr.splice(-1);
setNewValue([...arr]);
- }}/>
+ }} />
)
case 'cookies':
- return
+ return
case 'before':
return or
- {
- whereKeys.map((key: string, index: number) => {
- return (
- setChecked([
- ...checked.slice(0, index),
- !checked[index],
- ...checked.slice(index + 1)
- ])}
- key={`checkbox-${key}-${index}`}
- />
- } label={key} key={`control-label-form-${key}-${index}`}/>
- )
- })
- }
+ {
+ whereKeys.map((key: string, index: number) => {
+ return (
+ setChecked([
+ ...checked.slice(0, index),
+ !checked[index],
+ ...checked.slice(index + 1)
+ ])}
+ key={`checkbox-${key}-${index}`}
+ />
+ } label={key} key={`control-label-form-${key}-${index}`} />
+ )
+ })
+ }
Choose at least 2 where conditions. Nesting of boolean operators
diff --git a/src/components/atoms/Highlighter.tsx b/src/components/recorder/Highlighter.tsx
similarity index 100%
rename from src/components/atoms/Highlighter.tsx
rename to src/components/recorder/Highlighter.tsx
diff --git a/src/components/molecules/KeyValueForm.tsx b/src/components/recorder/KeyValueForm.tsx
similarity index 71%
rename from src/components/molecules/KeyValueForm.tsx
rename to src/components/recorder/KeyValueForm.tsx
index c6dd374a..3db70135 100644
--- a/src/components/molecules/KeyValueForm.tsx
+++ b/src/components/recorder/KeyValueForm.tsx
@@ -1,11 +1,11 @@
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
-import { KeyValuePair } from "../atoms/KeyValuePair";
-import { AddButton } from "../atoms/buttons/AddButton";
-import { RemoveButton } from "../atoms/buttons/RemoveButton";
+import { KeyValuePair } from "./KeyValuePair";
+import { AddButton } from "../ui/buttons/AddButton";
+import { RemoveButton } from "../ui/buttons/RemoveButton";
export const KeyValueForm = forwardRef((props, ref) => {
const [numberOfPairs, setNumberOfPairs] = React.useState(1);
- const keyValuePairRefs = useRef<{getKeyValuePair: () => { key: string, value: string }}[]>([]);
+ const keyValuePairRefs = useRef<{ getKeyValuePair: () => { key: string, value: string } }[]>([]);
useImperativeHandle(ref, () => ({
getObject() {
@@ -28,12 +28,12 @@ export const KeyValueForm = forwardRef((props, ref) => {
{
new Array(numberOfPairs).fill(1).map((_, index) => {
return keyValuePairRefs.current[index] = el}/>
+ //@ts-ignore
+ ref={el => keyValuePairRefs.current[index] = el} />
})
}
- setNumberOfPairs(numberOfPairs + 1)} hoverEffect={false}/>
- setNumberOfPairs(numberOfPairs - 1)}/>
+ setNumberOfPairs(numberOfPairs + 1)} hoverEffect={false} />
+ setNumberOfPairs(numberOfPairs - 1)} />
);
});
diff --git a/src/components/atoms/KeyValuePair.tsx b/src/components/recorder/KeyValuePair.tsx
similarity index 100%
rename from src/components/atoms/KeyValuePair.tsx
rename to src/components/recorder/KeyValuePair.tsx
diff --git a/src/components/organisms/LeftSidePanel.tsx b/src/components/recorder/LeftSidePanel.tsx
similarity index 93%
rename from src/components/organisms/LeftSidePanel.tsx
rename to src/components/recorder/LeftSidePanel.tsx
index 29196d20..afe56ffe 100644
--- a/src/components/organisms/LeftSidePanel.tsx
+++ b/src/components/recorder/LeftSidePanel.tsx
@@ -3,14 +3,14 @@ import React, { useCallback, useEffect, useState } from "react";
import { getActiveWorkflow, getParamsOfActiveWorkflow } from "../../api/workflow";
import { useSocketStore } from '../../context/socket';
import { WhereWhatPair, WorkflowFile } from "maxun-core";
-import { SidePanelHeader } from "../molecules/SidePanelHeader";
+import { SidePanelHeader } from "./SidePanelHeader";
import { emptyWorkflow } from "../../shared/constants";
-import { LeftSidePanelContent } from "../molecules/LeftSidePanelContent";
+import { LeftSidePanelContent } from "./LeftSidePanelContent";
import { useBrowserDimensionsStore } from "../../context/browserDimensions";
import { useGlobalInfoStore } from "../../context/globalInfo";
import { TabContext, TabPanel } from "@mui/lab";
-import { LeftSidePanelSettings } from "../molecules/LeftSidePanelSettings";
-import { RunSettings } from "../molecules/RunSettings";
+import { LeftSidePanelSettings } from "./LeftSidePanelSettings";
+import { RunSettings } from "../run/RunSettings";
const fetchWorkflow = (id: string, callback: (response: WorkflowFile) => void) => {
getActiveWorkflow(id).then(
diff --git a/src/components/molecules/LeftSidePanelContent.tsx b/src/components/recorder/LeftSidePanelContent.tsx
similarity index 77%
rename from src/components/molecules/LeftSidePanelContent.tsx
rename to src/components/recorder/LeftSidePanelContent.tsx
index 79750d8f..4a980505 100644
--- a/src/components/molecules/LeftSidePanelContent.tsx
+++ b/src/components/recorder/LeftSidePanelContent.tsx
@@ -5,9 +5,9 @@ import { WhereWhatPair, WorkflowFile } from "maxun-core";
import { useSocketStore } from "../../context/socket";
import { Add } from "@mui/icons-material";
import { Socket } from "socket.io-client";
-import { AddButton } from "../atoms/buttons/AddButton";
+import { AddButton } from "../ui/buttons/AddButton";
import { AddPair } from "../../api/workflow";
-import { GenericModal } from "../atoms/GenericModal";
+import { GenericModal } from "../ui/GenericModal";
import { PairEditForm } from "./PairEditForm";
import { Fab, Tooltip, Typography } from "@mui/material";
@@ -18,7 +18,7 @@ interface LeftSidePanelContentProps {
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
(0);
const [breakpoints, setBreakpoints] = React.useState([]);
const [showEditModal, setShowEditModal] = useState(false);
@@ -67,12 +67,12 @@ export const LeftSidePanelContent = ({ workflow, updateWorkflow, recordingName,
return (
-
+
@@ -86,20 +86,20 @@ export const LeftSidePanelContent = ({ workflow, updateWorkflow, recordingName,
/>
- {
- workflow.workflow.map((pair, i, workflow, ) =>
-
handleBreakpointClick(i)}
- isActive={ activeId === i + 1}
- key={workflow.length - i}
- index={workflow.length - i}
- pair={pair}
- updateWorkflow={updateWorkflow}
- numberOfPairs={workflow.length}
- handleSelectPairForEdit={handleSelectPairForEdit}
- />)
- }
-
+ {
+ workflow.workflow.map((pair, i, workflow,) =>
+
handleBreakpointClick(i)}
+ isActive={activeId === i + 1}
+ key={workflow.length - i}
+ index={workflow.length - i}
+ pair={pair}
+ updateWorkflow={updateWorkflow}
+ numberOfPairs={workflow.length}
+ handleSelectPairForEdit={handleSelectPairForEdit}
+ />)
+ }
+
);
};
diff --git a/src/components/recorder/LeftSidePanelSettings.tsx b/src/components/recorder/LeftSidePanelSettings.tsx
new file mode 100644
index 00000000..87c73ce1
--- /dev/null
+++ b/src/components/recorder/LeftSidePanelSettings.tsx
@@ -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 (
+
+ {params.length !== 0 && (
+
+ Parameters:
+ {params?.map((item: string, index: number) => {
+ return setSettings(
+ {
+ ...settings,
+ params: settings.params
+ ? {
+ ...settings.params,
+ [item]: e.target.value,
+ }
+ : {
+ [item]: e.target.value,
+ },
+ })}
+ />
+ })}
+
+ )}
+ Interpreter:
+ setSettings(
+ {
+ ...settings,
+ maxConcurrency: parseInt(e.target.value),
+ })}
+ defaultValue={settings.maxConcurrency}
+ />
+ setSettings(
+ {
+ ...settings,
+ maxRepeats: parseInt(e.target.value),
+ })}
+ defaultValue={settings.maxRepeats}
+ />
+ setSettings(
+ {
+ ...settings,
+ debug: e.target.value === "true",
+ })}
+ >
+ true
+ false
+
+ socket?.emit('settings', settings)}>change
+
+ );
+}
diff --git a/src/components/molecules/Pair.tsx b/src/components/recorder/Pair.tsx
similarity index 63%
rename from src/components/molecules/Pair.tsx
rename to src/components/recorder/Pair.tsx
index 3c332600..12d2eca7 100644
--- a/src/components/molecules/Pair.tsx
+++ b/src/components/recorder/Pair.tsx
@@ -2,12 +2,12 @@ import React, { FC, useState } from 'react';
import { Stack, Button, IconButton, Tooltip, Badge } from "@mui/material";
import { AddPair, deletePair, UpdatePair } from "../../api/workflow";
import { WorkflowFile } from "maxun-core";
-import { ClearButton } from "../atoms/buttons/ClearButton";
-import { GenericModal } from "../atoms/GenericModal";
+import { ClearButton } from "../ui/buttons/ClearButton";
+import { GenericModal } from "../ui/GenericModal";
import { PairEditForm } from "./PairEditForm";
-import { PairDisplayDiv } from "../atoms/PairDisplayDiv";
-import { EditButton } from "../atoms/buttons/EditButton";
-import { BreakpointButton } from "../atoms/buttons/BreakpointButton";
+import { PairDisplayDiv } from "./PairDisplayDiv";
+import { EditButton } from "../ui/buttons/EditButton";
+import { BreakpointButton } from "../ui/buttons/BreakpointButton";
import VisibilityIcon from '@mui/icons-material/Visibility';
import styled from "styled-components";
import { LoadingButton } from "@mui/lab";
@@ -54,19 +54,19 @@ export const Pair: FC = (
};
const handleEdit = (pair: WhereWhatPair, newIndex: number) => {
- if (newIndex !== index){
- AddPair((newIndex - 1), pair).then((updatedWorkflow) => {
- updateWorkflow(updatedWorkflow);
- }).catch((error) => {
- console.error(error);
- });
- } else {
- UpdatePair((index - 1), pair).then((updatedWorkflow) => {
- updateWorkflow(updatedWorkflow);
- }).catch((error) => {
- console.error(error);
- });
- }
+ if (newIndex !== index) {
+ AddPair((newIndex - 1), pair).then((updatedWorkflow) => {
+ updateWorkflow(updatedWorkflow);
+ }).catch((error) => {
+ console.error(error);
+ });
+ } else {
+ UpdatePair((index - 1), pair).then((updatedWorkflow) => {
+ updateWorkflow(updatedWorkflow);
+ }).catch((error) => {
+ console.error(error);
+ });
+ }
handleClose();
};
@@ -78,10 +78,10 @@ export const Pair: FC = (
return (
-
- {isActive ?
- : breakpoint ?
- :
+
+ {isActive ?
+ : breakpoint ?
+ :
}
@@ -92,53 +92,53 @@ export const Pair: FC = (
fontSize: '1rem',
textTransform: 'none',
}} variant='text' key={`pair-${index}`}
- onClick={() => handleSelectPairForEdit(pair, index)}>
+ onClick={() => handleSelectPairForEdit(pair, index)}>
index: {index}
+ "&:hover": {
+ color: 'inherit',
+ }
+ }}>
-
-
-
+
+
+
- {
- enableEdit();
- handleOpen();
- }}
- />
+ {
+ enableEdit();
+ handleOpen();
+ }}
+ />
-
+
-
+
- { edit
+ {edit
?
-
+
:
= (
}
- );
+ );
};
interface ViewButtonProps {
handleClick: () => void;
}
-const ViewButton = ({handleClick}: ViewButtonProps) => {
+const ViewButton = ({ handleClick }: ViewButtonProps) => {
return (
-
+ sx={{ color: 'inherit', '&:hover': { color: '#1976d2', backgroundColor: 'transparent' } }}>
+
);
}
const PairWrapper = styled.div<{ isActive: boolean }>`
- background-color: ${({ isActive }) => isActive ? 'rgba(255, 0, 0, 0.1)' : 'transparent' };
- border: ${({ isActive }) => isActive ? 'solid 2px red' : 'none' };
+ background-color: ${({ isActive }) => isActive ? 'rgba(255, 0, 0, 0.1)' : 'transparent'};
+ border: ${({ isActive }) => isActive ? 'solid 2px red' : 'none'};
display: flex;
flex-direction: row;
flex-grow: 1;
@@ -176,6 +176,6 @@ const PairWrapper = styled.div<{ isActive: boolean }>`
color: gray;
&:hover {
color: dimgray;
- background: ${({ isActive }) => isActive ? 'rgba(255, 0, 0, 0.1)' : 'transparent' };
+ background: ${({ isActive }) => isActive ? 'rgba(255, 0, 0, 0.1)' : 'transparent'};
}
`;
diff --git a/src/components/recorder/PairDetail.tsx b/src/components/recorder/PairDetail.tsx
new file mode 100644
index 00000000..d330db1d
--- /dev/null
+++ b/src/components/recorder/PairDetail.tsx
@@ -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(
+ pair ? Object.keys(pair.where).map((key, index) => `${key}-${index}`) : []
+ );
+ const [addWhereCondOpen, setAddWhereCondOpen] = useState(false);
+ const [addWhatCondOpen, setAddWhatCondOpen] = useState(false);
+
+ const { socket } = useSocketStore();
+
+ const handleCollapseWhere = () => {
+ setCollapseWhere(!collapseWhere);
+ }
+
+ const handleCollapseWhat = () => {
+ setCollapseWhat(!collapseWhat);
+ }
+
+ const handleToggle = (event: React.SyntheticEvent, nodeIds: string[]) => {
+ setExpanded(nodeIds);
+ };
+
+ useLayoutEffect(() => {
+ if (pair) {
+ setPairIsSelected(true);
+ }
+ }, [pair])
+
+ const handleChangeValue = (value: any, where: boolean, keys: (string | number)[]) => {
+ // a moving reference to internal objects within pair.where or pair.what
+ let schema: any = where ? pair?.where : pair?.what;
+ const length = keys.length;
+ for (let i = 0; i < length - 1; i++) {
+ const elem = keys[i];
+ if (!schema[elem]) schema[elem] = {}
+ schema = schema[elem];
+ }
+
+ schema[keys[length - 1]] = value;
+ if (pair && socket) {
+ socket.emit('updatePair', { index: index - 1, pair: pair });
+ }
+ setRerender(!rerender);
+ }
+
+
+ const DisplayValueContent = (value: any, keys: (string | number)[], where: boolean = true) => {
+ switch (typeof (value)) {
+ case 'string':
+ return {
+ try {
+ const obj = JSON.parse(e.target.value);
+ handleChangeValue(obj, where, keys);
+ } catch (error) {
+ const num = Number(e.target.value);
+ if (!isNaN(num)) {
+ handleChangeValue(num, where, keys);
+ }
+ handleChangeValue(e.target.value, where, keys)
+ }
+ }}
+ defaultValue={value}
+ key={`text-field-${keys.join('-')}-${where}`}
+ />
+ case 'number':
+ return handleChangeValue(Number(e.target.value), where, keys)}
+ defaultValue={value}
+ key={`text-field-${keys.join('-')}-${where}`}
+ />
+ case 'object':
+ if (value) {
+ if (Array.isArray(value)) {
+ return (
+
+ {
+ value.map((element, index) => {
+ return DisplayValueContent(element, [...keys, index], where);
+ })
+ }
+ {
+ let prevValue: any = where ? pair?.where : pair?.what;
+ for (const key of keys) {
+ prevValue = prevValue[key];
+ }
+ handleChangeValue([...prevValue, ''], where, keys);
+ setRerender(!rerender);
+ }} hoverEffect={false} />
+ {
+ let prevValue: any = where ? pair?.where : pair?.what;
+ for (const key of keys) {
+ prevValue = prevValue[key];
+ }
+ prevValue.splice(-1);
+ handleChangeValue(prevValue, where, keys);
+ setRerender(!rerender);
+ }} />
+
+ )
+ } else {
+ return (
+ }
+ defaultExpandIcon={ }
+ sx={{ flexGrow: 1, overflowY: 'auto' }}
+ key={`tree-view-nested-${keys.join('-')}-${where}`}
+ >
+ {
+ Object.keys(value).map((key2, index) => {
+ return (
+
+ {DisplayValueContent(value[key2], [...keys, key2], where)}
+
+ )
+ })
+ }
+
+ )
+ }
+ }
+ break;
+ default:
+ return null;
+ }
+ }
+
+ return (
+
+ {pair &&
+
+ setAddWhatCondOpen(false)}
+ pair={pair} index={index} />
+ setAddWhereCondOpen(false)}
+ pair={pair} index={index} />
+
+ }
+ {
+ pairIsSelected
+ ? (
+
+
Pair number: {index}
+
{
+ if (pair && socket) {
+ socket.emit('updatePair', { index: index - 1, pair: pair });
+ pair.id = e.target.value;
+ }
+ }}
+ value={pair ? pair.id ? pair.id : '' : ''}
+ />
+
+
+ Where
+
+
+
{
+ setAddWhereCondOpen(true);
+ }} style={{ color: 'rgba(0, 0, 0, 0.54)', background: 'transparent' }} />
+
+
+
+ {(collapseWhere && pair && pair.where)
+ ?
+
+ {Object.keys(pair.where).map((key, index) => {
+ return (
+ }
+ defaultExpandIcon={ }
+ sx={{ flexGrow: 1, overflowY: 'auto' }}
+ onNodeToggle={handleToggle}
+ key={`tree-view-${key}-${index}`}
+ >
+
+ {
+ // @ts-ignore
+ DisplayValueContent(pair.where[key], [key])
+ }
+
+
+ );
+ })}
+
+ : null
+ }
+
+
+ What
+
+
+
+
{
+ setAddWhatCondOpen(true);
+ }} style={{ color: 'rgba(0, 0, 0, 0.54)', background: 'transparent' }} />
+
+
+
+ {(collapseWhat && pair && pair.what)
+ ? (
+
+ {Object.keys(pair.what).map((key, index) => {
+ return (
+ }
+ defaultExpandIcon={ }
+ sx={{ flexGrow: 1, overflowY: 'auto' }}
+ key={`tree-view-2-${key}-${index}`}
+ >
+
+ {
+ // @ts-ignore
+ DisplayValueContent(pair.what[key], [key], false)
+ }
+
+
+ {
+ //@ts-ignore
+ pair.what.splice(key, 1);
+ setRerender(!rerender);
+ }} />
+
+
+
+
+ );
+ })}
+
+ )
+ : null
+ }
+
+ )
+ :
+
+ No pair from the left side panel was selected.
+
+ }
+
+ );
+}
+
+interface CollapseButtonProps {
+ handleClick: () => void;
+ isCollapsed?: boolean;
+}
+
+const CollapseButton = ({ handleClick, isCollapsed }: CollapseButtonProps) => {
+ return (
+
+ {isCollapsed ? : }
+
+ );
+}
+
+const CloseButton = ({ handleClick }: CollapseButtonProps) => {
+ return (
+
+
+
+ );
+}
diff --git a/src/components/atoms/PairDisplayDiv.tsx b/src/components/recorder/PairDisplayDiv.tsx
similarity index 100%
rename from src/components/atoms/PairDisplayDiv.tsx
rename to src/components/recorder/PairDisplayDiv.tsx
diff --git a/src/components/molecules/PairEditForm.tsx b/src/components/recorder/PairEditForm.tsx
similarity index 59%
rename from src/components/molecules/PairEditForm.tsx
rename to src/components/recorder/PairEditForm.tsx
index 7ab9c381..aa3354ec 100644
--- a/src/components/molecules/PairEditForm.tsx
+++ b/src/components/recorder/PairEditForm.tsx
@@ -56,30 +56,30 @@ export const PairEditForm: FC = (
event.preventDefault();
let whereFromPair, whatFromPair;
// validate where
- whereFromPair = {
- where: pairProps.where && pairProps.where !== '{"url":"","selectors":[""] }'
- ? JSON.parse(pairProps.where)
- : {},
- what: [],
- };
- const validationError = Preprocessor.validateWorkflow({workflow: [whereFromPair]});
- setErrors({ ...errors, where: null });
+ whereFromPair = {
+ where: pairProps.where && pairProps.where !== '{"url":"","selectors":[""] }'
+ ? JSON.parse(pairProps.where)
+ : {},
+ what: [],
+ };
+ const validationError = Preprocessor.validateWorkflow({ workflow: [whereFromPair] });
+ setErrors({ ...errors, where: null });
if (validationError) {
setErrors({ ...errors, where: validationError.message });
return;
}
// validate what
- whatFromPair = {
- where: {},
- what: pairProps.what && pairProps.what !== '[{"action":"","args":[""] }]'
- ? JSON.parse(pairProps.what): [],
- };
- const validationErrorWhat = Preprocessor.validateWorkflow({workflow: [whatFromPair]});
- setErrors({ ...errors, "what": null });
- if (validationErrorWhat) {
- setErrors({ ...errors, what: validationErrorWhat.message });
- return;
- }
+ whatFromPair = {
+ where: {},
+ what: pairProps.what && pairProps.what !== '[{"action":"","args":[""] }]'
+ ? JSON.parse(pairProps.what) : [],
+ };
+ const validationErrorWhat = Preprocessor.validateWorkflow({ workflow: [whatFromPair] });
+ setErrors({ ...errors, "what": null });
+ if (validationErrorWhat) {
+ setErrors({ ...errors, what: validationErrorWhat.message });
+ return;
+ }
//validate index
const index = parseInt(pairProps?.index, 10);
if (index > (numberOfPairs + 1)) {
@@ -99,18 +99,18 @@ export const PairEditForm: FC = (
} else {
setErrors({ ...errors, index: '' });
}
- // submit the pair
- onSubmitOfPair(pairProps.id
+ // submit the pair
+ onSubmitOfPair(pairProps.id
? {
id: pairProps.id,
where: whereFromPair?.where || {},
what: whatFromPair?.what || [],
}
: {
- where: whereFromPair?.where || {},
- what: whatFromPair?.what || [],
- }
- , index);
+ where: whereFromPair?.where || {},
+ what: whatFromPair?.what || [],
+ }
+ , index);
};
return (
@@ -122,33 +122,33 @@ export const PairEditForm: FC = (
marginTop: "36px",
}}
>
- Raw pair edit form:
+ Raw pair edit form:
-
-
+
+
void) => {
@@ -243,9 +244,9 @@ export const RightSidePanel: React.FC = ({ onFinishCapture
const settings: Record = {};
browserSteps.forEach(step => {
if (browserStepIdList.includes(step.id)) {
- return;
+ return;
}
-
+
if (step.type === 'text' && step.label && step.selectorObj?.selector) {
settings[step.label] = step.selectorObj;
}
@@ -451,25 +452,28 @@ export const RightSidePanel: React.FC = ({ onFinishCapture
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
if (captureStage !== 'initial') return false;
-
- const hasValidListSelector = browserSteps.some(step =>
- step.type === 'list' &&
- step.listSelector &&
+
+ const hasValidListSelector = browserSteps.some(step =>
+ step.type === 'list' &&
+ step.listSelector &&
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;
}, [captureStage, browserSteps, hasUnconfirmedListTextFields]);
+ const theme = useThemeMode();
+ const isDarkMode = theme.darkMode;
return (
{/*
Last action: {` ${lastAction}`}
*/}
-
+
{!getText && !getScreenshot && !getList && showCaptureList && {t('right_panel.buttons.capture_list')} }
+
{getList && (
<>
@@ -477,6 +481,11 @@ export const RightSidePanel: React.FC = ({ onFinishCapture
{t('right_panel.buttons.back')}
@@ -485,13 +494,26 @@ export const RightSidePanel: React.FC = ({ onFinishCapture
variant="outlined"
onClick={handleConfirmListCapture}
disabled={captureStage === 'initial' ? isConfirmCaptureDisabled : hasUnconfirmedListTextFields}
+ sx={{
+ color: '#ff00c3 !important',
+ borderColor: '#ff00c3 !important',
+ backgroundColor: 'whitesmoke !important',
+ }}
>
{captureStage === 'initial' ? t('right_panel.buttons.confirm_capture') :
- captureStage === 'pagination' ? t('right_panel.buttons.confirm_pagination') :
- captureStage === 'limit' ? t('right_panel.buttons.confirm_limit') :
- t('right_panel.buttons.finish_capture')}
+ captureStage === 'pagination' ? t('right_panel.buttons.confirm_pagination') :
+ captureStage === 'limit' ? t('right_panel.buttons.confirm_limit') :
+ t('right_panel.buttons.finish_capture')}
-
+
{t('right_panel.buttons.discard')}
@@ -500,11 +522,55 @@ export const RightSidePanel: React.FC = ({ onFinishCapture
{showPaginationOptions && (
{t('right_panel.pagination.title')}
- handlePaginationSettingSelect('clickNext')}>{t('right_panel.pagination.click_next')}
- handlePaginationSettingSelect('clickLoadMore')}>{t('right_panel.pagination.click_load_more')}
- handlePaginationSettingSelect('scrollDown')}>{t('right_panel.pagination.scroll_down')}
- handlePaginationSettingSelect('scrollUp')}>{t('right_panel.pagination.scroll_up')}
- handlePaginationSettingSelect('none')}>{t('right_panel.pagination.none')}
+ handlePaginationSettingSelect('clickNext')}
+ sx={{
+ color: paginationType === 'clickNext' ? 'whitesmoke !important' : '#ff00c3 !important',
+ borderColor: '#ff00c3 !important',
+ backgroundColor: paginationType === 'clickNext' ? '#ff00c3 !important' : 'whitesmoke !important',
+ }}>
+ {t('right_panel.pagination.click_next')}
+
+ 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')}
+
+ 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')}
+
+ 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')}
+
+ 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')}
)}
{showLimitOptions && (
@@ -527,60 +593,94 @@ export const RightSidePanel: React.FC = ({ onFinishCapture
} label={t('right_panel.limit.custom')} />
{limitType === 'custom' && (
) => {
- const value = parseInt(e.target.value);
- // Only update if the value is greater than or equal to 1 or if the field is empty
- if (e.target.value === '' || value >= 1) {
- updateCustomLimit(e.target.value);
- }
- }}
- inputProps={{
- min: 1,
- onKeyPress: (e: React.KeyboardEvent) => {
- const value = (e.target as HTMLInputElement).value + e.key;
- if (parseInt(value) < 1) {
- e.preventDefault();
+ type="number"
+ value={customLimit}
+ onChange={(e: React.ChangeEvent) => {
+ const value = parseInt(e.target.value);
+ // Only update if the value is greater than or equal to 1 or if the field is empty
+ if (e.target.value === '' || value >= 1) {
+ updateCustomLimit(e.target.value);
}
- }
- }}
- placeholder={t('right_panel.limit.enter_number')}
- sx={{
- marginLeft: '10px',
- '& input': {
- padding: '10px',
- background: 'white',
- },
- width: '150px', // Ensure the text field does not go outside the panel
- }}
+ }}
+ inputProps={{
+ min: 1,
+ onKeyPress: (e: React.KeyboardEvent) => {
+ const value = (e.target as HTMLInputElement).value + e.key;
+ if (parseInt(value) < 1) {
+ e.preventDefault();
+ }
+ }
+ }}
+ 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
+ }}
/>
)}
)}
+ {/* {!getText && !getScreenshot && !getList && showCaptureText && {t('right_panel.buttons.capture_text')} } */}
+
{!getText && !getScreenshot && !getList && showCaptureText && {t('right_panel.buttons.capture_text')} }
{getText &&
<>
- {t('right_panel.buttons.confirm')}
- {t('right_panel.buttons.discard')}
+
+ {t('right_panel.buttons.confirm')}
+
+
+ {t('right_panel.buttons.discard')}
+
>
}
+ {/* {!getText && !getScreenshot && !getList && showCaptureScreenshot && {t('right_panel.buttons.capture_screenshot')} } */}
{!getText && !getScreenshot && !getList && showCaptureScreenshot && {t('right_panel.buttons.capture_screenshot')} }
{getScreenshot && (
captureScreenshot(true)}>{t('right_panel.screenshot.capture_fullpage')}
captureScreenshot(false)}>{t('right_panel.screenshot.capture_visible')}
- {t('right_panel.buttons.discard')}
+
+ {t('right_panel.buttons.discard')}
+
)}
{browserSteps.map(step => (
- handleMouseEnter(step.id)} onMouseLeave={() => handleMouseLeave(step.id)} sx={{ padding: '10px', margin: '11px', borderRadius: '5px', position: 'relative', background: 'white' }}>
+ 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' && (
<>
@@ -601,6 +701,7 @@ export const RightSidePanel: React.FC = ({ onFinishCapture
)
}}
+ sx={{ background: isDarkMode ? "#1E2124" : 'white', color: isDarkMode ? "white" : 'black' }}
/>
= ({ onFinishCapture
)
}}
+
/>
{!confirmedTextSteps[step.id] ? (
@@ -638,8 +740,8 @@ export const RightSidePanel: React.FC = ({ onFinishCapture
- {step.fullPage ?
- t('right_panel.screenshot.display_fullpage') :
+ {step.fullPage ?
+ t('right_panel.screenshot.display_fullpage') :
t('right_panel.screenshot.display_visible')}
@@ -648,7 +750,7 @@ export const RightSidePanel: React.FC = ({ onFinishCapture
<>
{t('right_panel.messages.list_selected')}
{Object.entries(step.fields).map(([key, field]) => (
-
+
= ({ onFinishCapture
)
}}
+
/>
{!confirmedListTextFields[step.id]?.[key] ? (
diff --git a/src/components/molecules/SaveRecording.tsx b/src/components/recorder/SaveRecording.tsx
similarity index 86%
rename from src/components/molecules/SaveRecording.tsx
rename to src/components/recorder/SaveRecording.tsx
index cc51f238..835930e4 100644
--- a/src/components/molecules/SaveRecording.tsx
+++ b/src/components/recorder/SaveRecording.tsx
@@ -1,12 +1,12 @@
import React, { useCallback, useEffect, useState, useContext } from 'react';
import { Button, Box, LinearProgress, Tooltip } from "@mui/material";
-import { GenericModal } from "../atoms/GenericModal";
+import { GenericModal } from "../ui/GenericModal";
import { stopRecording } from "../../api/recording";
import { useGlobalInfoStore } from "../../context/globalInfo";
import { AuthContext } from '../../context/auth';
import { useSocketStore } from "../../context/socket";
import { TextField, Typography } from "@mui/material";
-import { WarningText } from "../atoms/texts";
+import { WarningText } from "../ui/texts";
import NotificationImportantIcon from "@mui/icons-material/NotificationImportant";
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
@@ -77,7 +77,21 @@ export const SaveRecording = ({ fileName }: SaveRecordingProps) => {
return (
-
setOpenModal(true)} variant="outlined" sx={{ marginRight: '20px' }} size="small" color="success">
+ {/* setOpenModal(true)} variant='contained' sx={{ marginRight: '20px',backgroundColor: '#ff00c3',color: 'white' }} size="small" color="success">
+ Finish */}
+
+ 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')}
@@ -105,8 +119,8 @@ export const SaveRecording = ({ fileName }: SaveRecordingProps) => {
)
:
- {t('save_recording.buttons.save')}
-
+ {t('save_recording.buttons.save')}
+
}
{waitingForSave &&
diff --git a/src/components/molecules/SidePanelHeader.tsx b/src/components/recorder/SidePanelHeader.tsx
similarity index 90%
rename from src/components/molecules/SidePanelHeader.tsx
rename to src/components/recorder/SidePanelHeader.tsx
index 9e93f432..41b33ae7 100644
--- a/src/components/molecules/SidePanelHeader.tsx
+++ b/src/components/recorder/SidePanelHeader.tsx
@@ -1,5 +1,5 @@
import React, { FC, useState } from 'react';
-import { InterpretationButtons } from "./InterpretationButtons";
+import { InterpretationButtons } from "../run/InterpretationButtons";
import { useSocketStore } from "../../context/socket";
export const SidePanelHeader = () => {
diff --git a/src/components/atoms/canvas.tsx b/src/components/recorder/canvas.tsx
similarity index 94%
rename from src/components/atoms/canvas.tsx
rename to src/components/recorder/canvas.tsx
index e71a4d93..83e48274 100644
--- a/src/components/atoms/canvas.tsx
+++ b/src/components/recorder/canvas.tsx
@@ -3,10 +3,10 @@ import { useSocketStore } from '../../context/socket';
import { getMappedCoordinates } from "../../helpers/inputHelpers";
import { useGlobalInfoStore } from "../../context/globalInfo";
import { useActionContext } from '../../context/browserActions';
-import DatePicker from './DatePicker';
-import Dropdown from './Dropdown';
-import TimePicker from './TimePicker';
-import DateTimeLocalPicker from './DateTimeLocalPicker';
+import DatePicker from '../pickers/DatePicker';
+import Dropdown from '../pickers/Dropdown';
+import TimePicker from '../pickers/TimePicker';
+import DateTimeLocalPicker from '../pickers/DateTimeLocalPicker';
interface CreateRefCallback {
(ref: React.RefObject): void;
@@ -76,7 +76,7 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
useEffect(() => {
if (socket) {
- socket.on('showDatePicker', (info: {coordinates: Coordinates, selector: string}) => {
+ socket.on('showDatePicker', (info: { coordinates: Coordinates, selector: string }) => {
setDatePickerInfo(info);
});
@@ -93,11 +93,11 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
setDropdownInfo(info);
});
- socket.on('showTimePicker', (info: {coordinates: Coordinates, selector: string}) => {
+ socket.on('showTimePicker', (info: { coordinates: Coordinates, selector: string }) => {
setTimePickerInfo(info);
});
- socket.on('showDateTimePicker', (info: {coordinates: Coordinates, selector: string}) => {
+ socket.on('showDateTimePicker', (info: { coordinates: Coordinates, selector: string }) => {
setDateTimeLocalInfo(info);
});
diff --git a/src/components/robot/Recordings.tsx b/src/components/robot/Recordings.tsx
new file mode 100644
index 00000000..46484cb1
--- /dev/null
+++ b/src/components/robot/Recordings.tsx
@@ -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([]);
+
+ 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 (
+
+ );
+ } else if (currentPath.endsWith("/schedule")) {
+ return (
+
+ );
+ } else if (currentPath.endsWith("/integrate")) {
+ return (
+ {}}
+ />
+ );
+ } else if (currentPath.endsWith("/settings")) {
+ return (
+ {}}
+ />
+ );
+ } else if (currentPath.endsWith("/edit")) {
+ return (
+ {}}
+ />
+ );
+ } else if (currentPath.endsWith("/duplicate")) {
+ return (
+ {}}
+ />
+ );
+ }
+ return null;
+ };
+
+ return (
+
+ {getCurrentModal()}
+
+
+
+ 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)
+ }
+ />
+
+
+
+ );
+};
diff --git a/src/components/molecules/RecordingsTable.tsx b/src/components/robot/RecordingsTable.tsx
similarity index 99%
rename from src/components/molecules/RecordingsTable.tsx
rename to src/components/robot/RecordingsTable.tsx
index f8a0ba37..3c738dd9 100644
--- a/src/components/molecules/RecordingsTable.tsx
+++ b/src/components/robot/RecordingsTable.tsx
@@ -18,7 +18,7 @@ import { checkRunsForRecording, deleteRecordingFromStorage, getStoredRecordings
import { Add } from "@mui/icons-material";
import { useNavigate } from 'react-router-dom';
import { stopRecording } from "../../api/recording";
-import { GenericModal } from '../atoms/GenericModal';
+import { GenericModal } from '../ui/GenericModal';
/** TODO:
@@ -53,7 +53,7 @@ interface 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 [rowsPerPage, setRowsPerPage] = React.useState(10);
const [rows, setRows] = React.useState([]);
@@ -401,7 +401,7 @@ const OptionsButton = ({ handleEdit, handleDelete, handleDuplicate }: OptionsBut
setAnchorEl(null);
};
- const {t} = useTranslation();
+ const { t } = useTranslation();
return (
<>
diff --git a/src/components/molecules/RobotDuplicate.tsx b/src/components/robot/RobotDuplicate.tsx
similarity index 87%
rename from src/components/molecules/RobotDuplicate.tsx
rename to src/components/robot/RobotDuplicate.tsx
index ce3ee5ca..a78e523b 100644
--- a/src/components/molecules/RobotDuplicate.tsx
+++ b/src/components/robot/RobotDuplicate.tsx
@@ -1,7 +1,7 @@
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 { modalStyle } from "./AddWhereCondModal";
+import { modalStyle } from "../recorder/AddWhereCondModal";
import { useGlobalInfoStore } from '../../context/globalInfo';
import { duplicateRecording, getStoredRecording } from '../../api/storage';
import { WhereWhatPair } from 'maxun-core';
@@ -99,7 +99,7 @@ export const RobotDuplicationModal = ({ isOpen, handleStart, handleClose, initia
if (success) {
notify('success', t('robot_duplication.notifications.duplicate_success'));
handleStart(robot);
- handleClose();
+ handleClose();
setTimeout(() => {
window.location.reload();
@@ -136,7 +136,7 @@ export const RobotDuplicationModal = ({ isOpen, handleStart, handleClose, initia
url1: 'producthunt.com/topics/api',
url2: 'producthunt.com/topics/database'
})
- }}/>
+ }} />
{t('robot_duplication.descriptions.warning')}
@@ -152,7 +152,16 @@ export const RobotDuplicationModal = ({ isOpen, handleStart, handleClose, initia
{t('robot_duplication.buttons.duplicate')}
-
+
{t('robot_duplication.buttons.cancel')}
diff --git a/src/components/molecules/RobotEdit.tsx b/src/components/robot/RobotEdit.tsx
similarity index 89%
rename from src/components/molecules/RobotEdit.tsx
rename to src/components/robot/RobotEdit.tsx
index 6547d93b..946af330 100644
--- a/src/components/molecules/RobotEdit.tsx
+++ b/src/components/robot/RobotEdit.tsx
@@ -1,8 +1,8 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
-import { GenericModal } from "../atoms/GenericModal";
+import { GenericModal } from "../ui/GenericModal";
import { TextField, Typography, Box, Button } from "@mui/material";
-import { modalStyle } from "./AddWhereCondModal";
+import { modalStyle } from "../recorder/AddWhereCondModal";
import { useGlobalInfoStore } from '../../context/globalInfo';
import { getStoredRecording, updateRecording } from '../../api/storage';
import { WhereWhatPair } from 'maxun-core';
@@ -118,7 +118,7 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin
if (success) {
notify('success', t('robot_edit.notifications.update_success'));
handleStart(robot); // Inform parent about the updated robot
- handleClose();
+ handleClose();
setTimeout(() => {
window.location.reload();
@@ -159,11 +159,11 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin
label={t('robot_edit.robot_limit')}
type="number"
value={robot.recording.workflow[0].what[0].args[0].limit || ''}
- onChange={(e) =>{
+ onChange={(e) => {
const value = parseInt(e.target.value, 10);
if (value >= 1) {
handleLimitChange(value);
- }
+ }
}}
inputProps={{ min: 1 }}
style={{ marginBottom: '20px' }}
@@ -174,12 +174,16 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin
{t('robot_edit.save')}
-
+ sx={{
+ color: '#ff00c3 !important',
+ borderColor: '#ff00c3 !important',
+ backgroundColor: 'whitesmoke !important',
+ }}>
{t('robot_edit.cancel')}
diff --git a/src/components/molecules/RobotSettings.tsx b/src/components/robot/RobotSettings.tsx
similarity index 98%
rename from src/components/molecules/RobotSettings.tsx
rename to src/components/robot/RobotSettings.tsx
index d952f43d..2dd7ebe3 100644
--- a/src/components/molecules/RobotSettings.tsx
+++ b/src/components/robot/RobotSettings.tsx
@@ -1,8 +1,8 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
-import { GenericModal } from "../atoms/GenericModal";
+import { GenericModal } from "../ui/GenericModal";
import { TextField, Typography, Box } from "@mui/material";
-import { modalStyle } from "./AddWhereCondModal";
+import { modalStyle } from "../recorder/AddWhereCondModal";
import { useGlobalInfoStore } from '../../context/globalInfo';
import { getStoredRecording } from '../../api/storage';
import { WhereWhatPair } from 'maxun-core';
diff --git a/src/components/molecules/ScheduleSettings.tsx b/src/components/robot/ScheduleSettings.tsx
similarity index 95%
rename from src/components/molecules/ScheduleSettings.tsx
rename to src/components/robot/ScheduleSettings.tsx
index 917696c9..658ce594 100644
--- a/src/components/molecules/ScheduleSettings.tsx
+++ b/src/components/robot/ScheduleSettings.tsx
@@ -1,8 +1,8 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
-import { GenericModal } from "../atoms/GenericModal";
+import { GenericModal } from "../ui/GenericModal";
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 { validMomentTimezones } from '../../constants/const';
import { useGlobalInfoStore } from '../../context/globalInfo';
@@ -123,12 +123,12 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initia
if (!day) return '';
const lastDigit = day.slice(-1);
const lastTwoDigits = day.slice(-2);
-
+
// Special cases for 11, 12, 13
if (['11', '12', '13'].includes(lastTwoDigits)) {
return t('schedule_settings.labels.on_day.th');
}
-
+
// Other cases
switch (lastDigit) {
case '1': return t('schedule_settings.labels.on_day.st');
@@ -273,7 +273,16 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initia
handleStart(settings)} variant="contained" color="primary">
{t('schedule_settings.buttons.save_schedule')}
-
+
{t('schedule_settings.buttons.cancel')}
diff --git a/src/components/molecules/ToggleButton.tsx b/src/components/robot/ToggleButton.tsx
similarity index 94%
rename from src/components/molecules/ToggleButton.tsx
rename to src/components/robot/ToggleButton.tsx
index a0917427..db10e645 100644
--- a/src/components/molecules/ToggleButton.tsx
+++ b/src/components/robot/ToggleButton.tsx
@@ -8,8 +8,8 @@ interface ToggleButtonProps {
export const ToggleButton: FC = ({ isChecked = false, onChange }) => (
-
-
+
+
);
diff --git a/src/components/molecules/ColapsibleRow.tsx b/src/components/run/ColapsibleRow.tsx
similarity index 86%
rename from src/components/molecules/ColapsibleRow.tsx
rename to src/components/run/ColapsibleRow.tsx
index 5e4be9da..8cf27d6d 100644
--- a/src/components/molecules/ColapsibleRow.tsx
+++ b/src/components/run/ColapsibleRow.tsx
@@ -7,10 +7,11 @@ import { DeleteForever, KeyboardArrowDown, KeyboardArrowUp, Settings } from "@mu
import { deleteRunFromStorage } from "../../api/storage";
import { columns, Data } from "./RunsTable";
import { RunContent } from "./RunContent";
-import { GenericModal } from "../atoms/GenericModal";
-import { modalStyle } from "./AddWhereCondModal";
+import { GenericModal } from "../ui/GenericModal";
+import { modalStyle } from "../recorder/AddWhereCondModal";
import { getUserById } from "../../api/auth";
import { useTranslation } from "react-i18next";
+import { useNavigate } from "react-router-dom";
interface RunTypeChipProps {
runByUserId?: string;
@@ -37,6 +38,7 @@ interface CollapsibleRowProps {
}
export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRunHandler, runningRecordingName }: CollapsibleRowProps) => {
const { t } = useTranslation();
+ const navigate = useNavigate();
const [open, setOpen] = useState(isOpen);
const [openSettingsModal, setOpenSettingsModal] = useState(false);
const [userEmail, setUserEmail] = useState(null);
@@ -47,7 +49,7 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRun
: row.runByAPI
? 'API'
: 'Unknown';
-
+
const logEndRef = useRef(null);
const scrollToLogBottom = () => {
@@ -60,9 +62,20 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRun
abortRunHandler();
}
- useEffect(() => {
- scrollToLogBottom();
- }, [currentLog])
+ const handleRowExpand = () => {
+ const newOpen = !open;
+ setOpen(newOpen);
+ if (newOpen) {
+ navigate(`/runs/${row.robotMetaId}/run/${row.runId}`);
+ } else {
+ navigate(`/runs/${row.robotMetaId}`);
+ }
+ //scrollToLogBottom();
+ };
+
+ // useEffect(() => {
+ // scrollToLogBottom();
+ // }, [currentLog])
useEffect(() => {
const fetchUserEmail = async () => {
@@ -83,10 +96,7 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRun
{
- setOpen(!open);
- scrollToLogBottom();
- }}
+ onClick={handleRowExpand}
>
{open ? : }
@@ -103,7 +113,7 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRun
} else {
switch (column.id) {
case 'runStatus':
- return (
+ return (
{row.status === 'success' && }
{row.status === 'running' && }
@@ -148,11 +158,11 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRun
/>
{t('runs_table.run_settings_modal.labels.run_type')}:
-
diff --git a/src/components/molecules/InterpretationButtons.tsx b/src/components/run/InterpretationButtons.tsx
similarity index 98%
rename from src/components/molecules/InterpretationButtons.tsx
rename to src/components/run/InterpretationButtons.tsx
index 624a57b4..0edcf682 100644
--- a/src/components/molecules/InterpretationButtons.tsx
+++ b/src/components/run/InterpretationButtons.tsx
@@ -4,7 +4,7 @@ import React, { useCallback, useEffect, useState } from "react";
import { interpretCurrentRecording, stopCurrentInterpretation } from "../../api/recording";
import { useSocketStore } from "../../context/socket";
import { useGlobalInfoStore } from "../../context/globalInfo";
-import { GenericModal } from "../atoms/GenericModal";
+import { GenericModal } from "../ui/GenericModal";
import { WhereWhatPair } from "maxun-core";
import HelpIcon from '@mui/icons-material/Help';
import { useTranslation } from "react-i18next";
@@ -79,7 +79,7 @@ export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsP
- {t('interpretation_buttons.modal.previous_action')} {decisionModal.action} ,
+ {t('interpretation_buttons.modal.previous_action')} {decisionModal.action} ,
{t('interpretation_buttons.modal.element_text')} {decisionModal.innerText}
diff --git a/src/components/molecules/InterpretationLog.tsx b/src/components/run/InterpretationLog.tsx
similarity index 96%
rename from src/components/molecules/InterpretationLog.tsx
rename to src/components/run/InterpretationLog.tsx
index 227e621c..925a95ba 100644
--- a/src/components/molecules/InterpretationLog.tsx
+++ b/src/components/run/InterpretationLog.tsx
@@ -15,8 +15,9 @@ import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import StorageIcon from '@mui/icons-material/Storage';
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
-import { SidePanelHeader } from './SidePanelHeader';
+import { SidePanelHeader } from '../recorder/SidePanelHeader';
import { useGlobalInfoStore } from '../../context/globalInfo';
+import { useThemeMode } from '../../context/theme-provider';
import { useTranslation } from 'react-i18next';
interface InterpretationLogProps {
@@ -81,7 +82,7 @@ export const InterpretationLog: React.FC = ({ isOpen, se
setLog((prevState) =>
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.separator'));
@@ -124,6 +125,8 @@ export const InterpretationLog: React.FC = ({ isOpen, se
}
}, [hasScrapeListAction, hasScrapeSchemaAction, hasScreenshotAction, setIsOpen]);
+ const { darkMode } = useThemeMode();
+
return (
@@ -147,7 +150,7 @@ export const InterpretationLog: React.FC = ({ isOpen, se
},
}}
>
-
+
{t('interpretation_log.titles.output_preview')}
= ({ isOpen, se
onOpen={toggleDrawer(true)}
PaperProps={{
sx: {
- background: 'white',
- color: 'black',
+ background: `${darkMode ? '#1e2124' : 'white'}`,
+ color: `${darkMode ? 'white' : 'black'}`,
padding: '10px',
height: 500,
width: width - 10,
@@ -168,7 +171,7 @@ export const InterpretationLog: React.FC = ({ isOpen, se
}}
>
-
+
{t('interpretation_log.titles.output_preview')}
{
const { t } = useTranslation();
- const [tab, setTab] = React.useState
('log');
+ const [tab, setTab] = React.useState('output');
const [tableData, setTableData] = useState([]);
const [columns, setColumns] = useState([]);
@@ -77,9 +77,49 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
- setTab(newTab)} aria-label="run-content-tabs">
-
-
+ 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',
+ },
+ }
+ }}
+ >
+ theme.palette.mode === 'dark' ? '#fff' : '#000',
+ '&:hover': {
+ color: '#FF00C3'
+ },
+ '&.Mui-selected': {
+ color: '#FF00C3',
+ }
+ }}
+ />
+ theme.palette.mode === 'dark' ? '#fff' : '#000',
+ '&:hover': {
+ color: '#FF00C3'
+ },
+ '&.Mui-selected': {
+ color: '#FF00C3',
+ }
+ }}
+ />
@@ -161,6 +201,7 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
background: 'rgba(0,0,0,0.06)',
maxHeight: '300px',
overflow: 'scroll',
+ backgroundColor: '#19171c'
}}>
{JSON.stringify(row.serializableOutput, null, 2)}
diff --git a/src/components/molecules/RunSettings.tsx b/src/components/run/RunSettings.tsx
similarity index 95%
rename from src/components/molecules/RunSettings.tsx
rename to src/components/run/RunSettings.tsx
index 76b2c4d7..a35d2f28 100644
--- a/src/components/molecules/RunSettings.tsx
+++ b/src/components/run/RunSettings.tsx
@@ -1,9 +1,9 @@
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 { Dropdown } from "../atoms/DropdownMui";
+import { Dropdown } from "../ui/DropdownMui";
import Button from "@mui/material/Button";
-import { modalStyle } from "./AddWhereCondModal";
+import { modalStyle } from "../recorder/AddWhereCondModal";
interface RunSettingsProps {
isOpen: boolean;
diff --git a/src/components/organisms/Runs.tsx b/src/components/run/Runs.tsx
similarity index 84%
rename from src/components/organisms/Runs.tsx
rename to src/components/run/Runs.tsx
index e3927fd4..cedbf348 100644
--- a/src/components/organisms/Runs.tsx
+++ b/src/components/run/Runs.tsx
@@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import { Grid } from "@mui/material";
-import { RunsTable } from "../molecules/RunsTable";
+import { RunsTable } from "./RunsTable";
interface RunsProps {
currentInterpretationLog: string;
@@ -13,7 +13,7 @@ export const Runs = (
{ currentInterpretationLog, abortRunHandler, runId, runningRecordingName }: RunsProps) => {
return (
-
+
= ({
- currentInterpretationLog,
- abortRunHandler,
- runId,
- runningRecordingName
+export const RunsTable: React.FC = ({
+ currentInterpretationLog,
+ abortRunHandler,
+ runId,
+ runningRecordingName
}) => {
const { t } = useTranslation();
+ const navigate = useNavigate();
// Update column labels using translation if needed
const translatedColumns = columns.map(column => ({
@@ -82,6 +83,14 @@ export const RunsTable: React.FC = ({
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) => {
setPage(newPage);
};
@@ -155,7 +164,7 @@ export const RunsTable: React.FC = ({
{Object.entries(groupedRows).map(([id, data]) => (
-
+ handleAccordionChange(id, isExpanded)}>
}>
{data[data.length - 1].name}
diff --git a/src/components/atoms/AlertSnackbar.tsx b/src/components/ui/AlertSnackbar.tsx
similarity index 100%
rename from src/components/atoms/AlertSnackbar.tsx
rename to src/components/ui/AlertSnackbar.tsx
diff --git a/src/components/atoms/Box.tsx b/src/components/ui/Box.tsx
similarity index 100%
rename from src/components/atoms/Box.tsx
rename to src/components/ui/Box.tsx
diff --git a/src/components/atoms/ConfirmationBox.tsx b/src/components/ui/ConfirmationBox.tsx
similarity index 100%
rename from src/components/atoms/ConfirmationBox.tsx
rename to src/components/ui/ConfirmationBox.tsx
diff --git a/src/components/atoms/DropdownMui.tsx b/src/components/ui/DropdownMui.tsx
similarity index 100%
rename from src/components/atoms/DropdownMui.tsx
rename to src/components/ui/DropdownMui.tsx
diff --git a/src/components/atoms/GenericModal.tsx b/src/components/ui/GenericModal.tsx
similarity index 100%
rename from src/components/atoms/GenericModal.tsx
rename to src/components/ui/GenericModal.tsx
diff --git a/src/components/atoms/Loader.tsx b/src/components/ui/Loader.tsx
similarity index 79%
rename from src/components/atoms/Loader.tsx
rename to src/components/ui/Loader.tsx
index 9d376bc7..0924b32f 100644
--- a/src/components/atoms/Loader.tsx
+++ b/src/components/ui/Loader.tsx
@@ -1,11 +1,14 @@
import styled from "styled-components";
import { Stack } from "@mui/material";
+import { useThemeMode } from "../../context/theme-provider";
interface LoaderProps {
text: string;
}
export const Loader: React.FC = ({ text }) => {
+ const { darkMode } = useThemeMode();
+
return (
@@ -14,15 +17,19 @@ export const Loader: React.FC = ({ text }) => {
- {text}
+ {text}
);
};
-const StyledParagraph = styled.p`
+interface StyledParagraphProps {
+ darkMode: boolean;
+}
+
+const StyledParagraph = styled.p`
font-size: large;
font-family: inherit;
- color: #333;
+ color: ${({ darkMode }) => (darkMode ? 'white' : '#333')};
margin-top: 20px;
`;
diff --git a/src/components/atoms/buttons/AddButton.tsx b/src/components/ui/buttons/AddButton.tsx
similarity index 100%
rename from src/components/atoms/buttons/AddButton.tsx
rename to src/components/ui/buttons/AddButton.tsx
diff --git a/src/components/atoms/buttons/BreakpointButton.tsx b/src/components/ui/buttons/BreakpointButton.tsx
similarity index 100%
rename from src/components/atoms/buttons/BreakpointButton.tsx
rename to src/components/ui/buttons/BreakpointButton.tsx
diff --git a/src/components/atoms/buttons/ClearButton.tsx b/src/components/ui/buttons/ClearButton.tsx
similarity index 100%
rename from src/components/atoms/buttons/ClearButton.tsx
rename to src/components/ui/buttons/ClearButton.tsx
diff --git a/src/components/atoms/buttons/EditButton.tsx b/src/components/ui/buttons/EditButton.tsx
similarity index 100%
rename from src/components/atoms/buttons/EditButton.tsx
rename to src/components/ui/buttons/EditButton.tsx
diff --git a/src/components/atoms/buttons/RemoveButton.tsx b/src/components/ui/buttons/RemoveButton.tsx
similarity index 100%
rename from src/components/atoms/buttons/RemoveButton.tsx
rename to src/components/ui/buttons/RemoveButton.tsx
diff --git a/src/components/atoms/buttons/buttons.tsx b/src/components/ui/buttons/buttons.tsx
similarity index 66%
rename from src/components/atoms/buttons/buttons.tsx
rename to src/components/ui/buttons/buttons.tsx
index afc4a483..50b630f5 100644
--- a/src/components/atoms/buttons/buttons.tsx
+++ b/src/components/ui/buttons/buttons.tsx
@@ -1,26 +1,18 @@
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-right: 5px;
padding: 0;
border: none;
- background-color: transparent;
+ background-color: ${mode => mode ? '#333' : '#ffffff'};
cursor: ${({ disabled }) => disabled ? 'default' : 'pointer'};
width: 24px;
height: 24px;
border-radius: 12px;
outline: none;
- color: ${({ disabled }) => disabled ? '#999' : '#333'};
-
- ${({ disabled }) => disabled ? null : `
- &:hover {
- background-color: #ddd;
- }
- &:active {
- background-color: #d0d0d0;
- }
- `};
+ color: ${mode => mode ? '#ffffff' : '#333333'};
`;
export const UrlFormButton = styled.button`
diff --git a/src/components/atoms/form.tsx b/src/components/ui/form.tsx
similarity index 100%
rename from src/components/atoms/form.tsx
rename to src/components/ui/form.tsx
diff --git a/src/components/atoms/texts.tsx b/src/components/ui/texts.tsx
similarity index 100%
rename from src/components/atoms/texts.tsx
rename to src/components/ui/texts.tsx
diff --git a/src/context/globalInfo.tsx b/src/context/globalInfo.tsx
index ac281630..805fa0d3 100644
--- a/src/context/globalInfo.tsx
+++ b/src/context/globalInfo.tsx
@@ -1,5 +1,5 @@
import React, { createContext, useContext, useState } from "react";
-import { AlertSnackbarProps } from "../components/atoms/AlertSnackbar";
+import { AlertSnackbarProps } from "../components/ui/AlertSnackbar";
interface GlobalInfo {
diff --git a/src/context/theme-provider.tsx b/src/context/theme-provider.tsx
new file mode 100644
index 00000000..d53ced24
--- /dev/null
+++ b/src/context/theme-provider.tsx
@@ -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 (
+
+
+
+ {children}
+
+
+ );
+};
+
+export default ThemeModeProvider;
diff --git a/src/helpers/inputHelpers.ts b/src/helpers/inputHelpers.ts
index d7861734..4d81dc59 100644
--- a/src/helpers/inputHelpers.ts
+++ b/src/helpers/inputHelpers.ts
@@ -4,7 +4,7 @@ import {
VIEWPORT_W,
VIEWPORT_H,
} from "../constants/const";
-import { Coordinates } from '../components/atoms/canvas';
+import { Coordinates } from '../components/recorder/canvas';
export const throttle = (callback: any, limit: number) => {
let wait = false;
diff --git a/src/index.css b/src/index.css
index 721e4d67..252b737a 100644
--- a/src/index.css
+++ b/src/index.css
@@ -11,6 +11,7 @@ body {
padding: 0;
scrollbar-gutter: stable;
overflow-y: auto;
+
}
html {
@@ -43,6 +44,7 @@ code {
align-items: center;
overflow: hidden;
position: relative;
+
}
#browser-content {
@@ -54,6 +56,11 @@ code {
transform-origin: top left; /* Keep the position fixed */
}
+
+#browser {
+
+}
+
#browser-window {
overflow-y: auto;
height: 100%;
diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx
index ba0f377e..1b34a1db 100644
--- a/src/pages/Login.tsx
+++ b/src/pages/Login.tsx
@@ -1,12 +1,13 @@
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 { 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 { apiUrl } from "../apiConfig";
import { useTranslation } from 'react-i18next';
import i18n from '../i18n';
+import { useThemeMode } from "../context/theme-provider";
const Login = () => {
const { t } = useTranslation();
@@ -17,12 +18,14 @@ const Login = () => {
email: "",
password: "",
});
+
const [loading, setLoading] = useState(false);
const { notify } = useGlobalInfoStore();
const { email, password } = form;
const { state, dispatch } = useContext(AuthContext);
const { user } = state;
+ const { darkMode } = useThemeMode();
const navigate = useNavigate();
@@ -41,10 +44,11 @@ const Login = () => {
e.preventDefault();
setLoading(true);
try {
- const { data } = await axios.post(`${apiUrl}/auth/login`, {
- email,
- password,
- });
+ const { data } = await axios.post(
+ `${apiUrl}/auth/login`,
+ { email, password },
+ { withCredentials: true }
+ );
dispatch({ type: "LOGIN", payload: data });
notify("success", t('login.welcome_notification'));
window.localStorage.setItem("user", JSON.stringify(data));
@@ -64,6 +68,7 @@ const Login = () => {
maxHeight: "100vh",
mt: 6,
padding: 4,
+ backgroundColor: darkMode ? "#121212" : "#ffffff",
}}
>
{
onSubmit={submitForm}
sx={{
textAlign: "center",
- backgroundColor: "#ffffff",
+ backgroundColor: darkMode ? "#1e1e1e" : "#ffffff",
+ color: darkMode ? "#ffffff" : "#333333",
padding: 6,
borderRadius: 5,
boxShadow: "0px 20px 40px rgba(0, 0, 0, 0.2), 0px -5px 10px rgba(0, 0, 0, 0.15)",
display: "flex",
flexDirection: "column",
alignItems: "center",
- maxWidth: 400,
+ maxWidth: 500,
width: "100%",
}}
>
@@ -112,7 +118,10 @@ const Login = () => {
fullWidth
variant="contained"
color="primary"
- sx={{ mt: 2, mb: 2 }}
+ sx={{
+ mt: 2,
+ mb: 2,
+ }}
disabled={loading || !email || !password}
>
{loading ? (
diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx
index 4a82170e..b9a4f24f 100644
--- a/src/pages/MainPage.tsx
+++ b/src/pages/MainPage.tsx
@@ -1,23 +1,24 @@
import React, { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
-import { MainMenu } from "../components/organisms/MainMenu";
+import { MainMenu } from "../components/dashboard/MainMenu";
import { Stack } from "@mui/material";
-import { Recordings } from "../components/organisms/Recordings";
-import { Runs } from "../components/organisms/Runs";
-import ProxyForm from '../components/organisms/ProxyForm';
-import ApiKey from '../components/organisms/ApiKey';
+import { Recordings } from "../components/robot/Recordings";
+import { Runs } from "../components/run/Runs";
+import ProxyForm from '../components/proxy/ProxyForm';
+import ApiKey from '../components/api/ApiKey';
import { useGlobalInfoStore } from "../context/globalInfo";
import { createRunForStoredRecording, interpretStoredRecording, notifyAboutAbort, scheduleStoredRecording } from "../api/storage";
import { io, Socket } from "socket.io-client";
import { stopRecording } from "../api/recording";
-import { RunSettings } from "../components/molecules/RunSettings";
-import { ScheduleSettings } from "../components/molecules/ScheduleSettings";
-import { IntegrationSettings } from "../components/molecules/IntegrationSettings";
-import { RobotSettings } from "../components/molecules/RobotSettings";
+import { RunSettings } from "../components/run/RunSettings";
+import { ScheduleSettings } from "../components/robot/ScheduleSettings";
+import { IntegrationSettings } from "../components/integration/IntegrationSettings";
+import { RobotSettings } from "../components/robot/RobotSettings";
import { apiUrl } from "../apiConfig";
interface MainPageProps {
handleEditRecording: (id: string, fileName: string) => void;
+ initialContent: string;
}
export interface CreateRunResponse {
@@ -30,9 +31,9 @@ export interface ScheduleRunResponse {
runId: string;
}
-export const MainPage = ({ handleEditRecording }: MainPageProps) => {
+export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) => {
const { t } = useTranslation();
- const [content, setContent] = React.useState('recordings');
+ const [content, setContent] = React.useState(initialContent);
const [sockets, setSockets] = React.useState([]);
const [runningRecordingId, setRunningRecordingId] = React.useState('');
const [runningRecordingName, setRunningRecordingName] = React.useState('');
@@ -123,7 +124,7 @@ export const MainPage = ({ handleEditRecording }: MainPageProps) => {
const DisplayContent = () => {
switch (content) {
- case 'recordings':
+ case 'robots':
return {
const [open, setOpen] = useState(false);
@@ -53,7 +54,11 @@ export const PageWrapper = () => {
{!browserId && }
}>
- } />
+ } />
+ } />
+ } />
+ } />
+ } />
}>
{
+ const { darkMode } = useThemeMode();
const { t } = useTranslation();
const [isLoaded, setIsLoaded] = React.useState(false);
const [hasScrollbar, setHasScrollbar] = React.useState(false);
@@ -34,6 +36,7 @@ export const RecordingPage = ({ recordingName }: RecordingPageProps) => {
pair: null,
index: 0,
});
+
const [showOutputData, setShowOutputData] = useState(false);
const browserContentRef = React.useRef(null);
@@ -57,15 +60,20 @@ export const RecordingPage = ({ recordingName }: RecordingPageProps) => {
useEffect(() => changeBrowserDimensions(), [isLoaded])
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%)';
- document.body.style.filter = 'progid:DXImageTransform.Microsoft.gradient(startColorstr="#ffffff",endColorstr="#ffffff",GradientType=1);'
+ if (darkMode) {
+
+ 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 () => {
- // Cleanup the background when leaving the page
document.body.style.background = '';
document.body.style.filter = '';
};
- }, []);
+ }, [darkMode]);
useEffect(() => {
let isCancelled = false;
diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx
index b1d2428f..d9c698a2 100644
--- a/src/pages/Register.tsx
+++ b/src/pages/Register.tsx
@@ -5,11 +5,11 @@ import { AuthContext } from "../context/auth";
import { Box, Typography, TextField, Button, CircularProgress } from "@mui/material";
import { useGlobalInfoStore } from "../context/globalInfo";
import { apiUrl } from "../apiConfig";
+import { useThemeMode } from "../context/theme-provider";
import { useTranslation } from 'react-i18next';
import i18n from '../i18n';
-
const Register = () => {
const {t} = useTranslation();
const [form, setForm] = useState({
@@ -22,6 +22,7 @@ const Register = () => {
const { state, dispatch } = useContext(AuthContext);
const { user } = state;
+ const { darkMode } = useThemeMode();
const navigate = useNavigate();
@@ -40,18 +41,14 @@ const Register = () => {
e.preventDefault();
setLoading(true);
try {
- const { data } = await axios.post(`${apiUrl}/auth/register`, {
- email,
- password,
- });
+ const { data } = await axios.post(`${apiUrl}/auth/register`, { email, password });
+ console.log(data);
dispatch({ type: "LOGIN", payload: data });
notify("success", t('register.welcome_notification'));
window.localStorage.setItem("user", JSON.stringify(data));
navigate("/");
} catch (error:any) {
-
notify("error", error.response.data || t('register.error_notification'));
-
setLoading(false);
}
};
@@ -65,25 +62,38 @@ const Register = () => {
maxHeight: "100vh",
mt: 6,
padding: 4,
+ backgroundColor: darkMode ? "#121212" : "#ffffff",
+
}}
>
-
+
{t('register.title')}
@@ -113,7 +123,10 @@ const Register = () => {
fullWidth
variant="contained"
color="primary"
- sx={{ mt: 2, mb: 2 }}
+ sx={{
+ mt: 2,
+ mb: 2,
+ }}
disabled={loading || !email || !password}
>
{loading ? (
@@ -125,10 +138,9 @@ const Register = () => {
t('register.button')
)}
-
+
{t('register.register_prompt')}{" "}
-
-
+
{t('register.login_link')}