diff --git a/README.md b/README.md index 376bf25b..13c994a0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- +
Maxun diff --git a/maxun-core/package.json b/maxun-core/package.json index d403360a..5b732a6b 100644 --- a/maxun-core/package.json +++ b/maxun-core/package.json @@ -1,6 +1,6 @@ { "name": "maxun-core", - "version": "0.0.9", + "version": "0.0.10", "description": "Core package for Maxun, responsible for data extraction", "main": "build/index.js", "typings": "build/index.d.ts", diff --git a/maxun-core/src/interpret.ts b/maxun-core/src/interpret.ts index 70d425a1..d87e2477 100644 --- a/maxun-core/src/interpret.ts +++ b/maxun-core/src/interpret.ts @@ -490,6 +490,14 @@ export default class Interpreter extends EventEmitter { const executeAction = async (invokee: any, methodName: string, args: any) => { console.log("Executing action:", methodName, args); + + if (methodName === 'press' || methodName === 'type') { + // Extract only the first two arguments for these methods + const limitedArgs = Array.isArray(args) ? args.slice(0, 2) : [args]; + await (invokee[methodName])(...limitedArgs); + return; + } + if (!args || Array.isArray(args)) { await (invokee[methodName])(...(args ?? [])); } else { @@ -547,221 +555,225 @@ export default class Interpreter extends EventEmitter { let scrapedItems: Set = new Set(); let visitedUrls: string[] = []; + // Debug logging helper + const debugLog = (message: string, ...args: any[]) => { + console.log(`[Page ${visitedUrls.length + 1}] ${message}`, ...args); + }; + let availableSelectors = config.pagination.selector.split(','); while (true) { - switch (config.pagination.type) { - case 'scrollDown': - await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); - await page.waitForTimeout(2000); + switch (config.pagination.type) { + case 'scrollDown': + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + await page.waitForTimeout(2000); - const currentHeight = await page.evaluate(() => document.body.scrollHeight); - if (currentHeight === previousHeight) { - const finalResults = await page.evaluate((cfg) => window.scrapeList(cfg), config); - allResults = allResults.concat(finalResults); - return allResults; - } + const currentHeight = await page.evaluate(() => document.body.scrollHeight); + if (currentHeight === previousHeight) { + const finalResults = await page.evaluate((cfg) => window.scrapeList(cfg), config); + allResults = allResults.concat(finalResults); + return allResults; + } - previousHeight = currentHeight; - break; - case 'scrollUp': - await page.evaluate(() => window.scrollTo(0, 0)); - await page.waitForTimeout(2000); - - const currentTopHeight = await page.evaluate(() => document.documentElement.scrollTop); - if (currentTopHeight === 0) { - const finalResults = await page.evaluate((cfg) => window.scrapeList(cfg), config); - allResults = allResults.concat(finalResults); - return allResults; - } - - previousHeight = currentTopHeight; - break; - case 'clickNext': - console.log("Page URL:", page.url()); - const pageResults = await page.evaluate((cfg) => window.scrapeList(cfg), config); - - // console.log("Page results:", pageResults); - - // Filter out already scraped items - const newResults = pageResults.filter(item => { - const uniqueKey = JSON.stringify(item); - if (scrapedItems.has(uniqueKey)) return false; // Ignore if already scraped - scrapedItems.add(uniqueKey); // Mark as scraped - return true; - }); - - allResults = allResults.concat(newResults); - console.log("Results so far:", allResults.length); - - if (config.limit && allResults.length >= config.limit) { - return allResults.slice(0, config.limit); - } - - let checkButton = null; - let workingSelector = null; - - for (let i = 0; i < availableSelectors.length; i++) { - const selector = availableSelectors[i]; - try { - // Wait for selector with a short timeout - checkButton = await page.waitForSelector(selector, { state: 'attached' }); - if (checkButton) { - workingSelector = selector; + previousHeight = currentHeight; break; + + case 'scrollUp': + await page.evaluate(() => window.scrollTo(0, 0)); + await page.waitForTimeout(2000); + + const currentTopHeight = await page.evaluate(() => document.documentElement.scrollTop); + if (currentTopHeight === 0) { + const finalResults = await page.evaluate((cfg) => window.scrapeList(cfg), config); + allResults = allResults.concat(finalResults); + return allResults; + } + + previousHeight = currentTopHeight; + break; + + case 'clickNext': + debugLog("Current URL:", page.url()); + const pageResults = await page.evaluate((cfg) => window.scrapeList(cfg), config); + + // Filter out already scraped items + const newResults = pageResults.filter(item => { + const uniqueKey = JSON.stringify(item); + if (scrapedItems.has(uniqueKey)) return false; + scrapedItems.add(uniqueKey); + return true; + }); + + allResults = allResults.concat(newResults); + debugLog("Results collected so far:", allResults.length); + + if (config.limit && allResults.length >= config.limit) { + return allResults.slice(0, config.limit); } - } catch (error) { - console.log(`Selector failed: ${selector}`); - } - } - if (!workingSelector) { - return allResults; - } + await page.waitForLoadState('networkidle', { timeout: 30000 }); + await page.waitForTimeout(2000); - // const nextButton = await page.$(config.pagination.selector); - const nextButton = await page.$(workingSelector); - if (!nextButton) { - return allResults; // No more pages to scrape - } + let checkButton = null; + let workingSelector = null; - const selectorIndex = availableSelectors.indexOf(workingSelector!); - availableSelectors = availableSelectors.slice(selectorIndex); + // Try each selector with explicit waiting + for (const selector of availableSelectors) { + try { + checkButton = await page.waitForSelector(selector, { + state: 'attached', + timeout: 30000 + }); + if (checkButton) { + workingSelector = selector; + debugLog('Found working selector:', selector); + break; + } + } catch (error) { + debugLog(`Selector failed: ${selector} - ${error.message}`); + } + } - // await Promise.all([ - // nextButton.dispatchEvent('click'), - // page.waitForNavigation({ waitUntil: 'networkidle' }) - // ]); + if (!workingSelector) { + debugLog('No working selector found after trying all options'); + return allResults; + } - const previousUrl = page.url(); - visitedUrls.push(previousUrl); + const nextButton = await page.$(workingSelector); + if (!nextButton) { + debugLog('Next button not found'); + return allResults; + } - try { - // Try both click methods simultaneously - await Promise.race([ - Promise.all([ - page.waitForNavigation({ waitUntil: 'networkidle', timeout: 30000 }), - nextButton.click() - ]), - Promise.all([ - page.waitForNavigation({ waitUntil: 'networkidle', timeout: 30000 }), - nextButton.dispatchEvent('click') - ]) - ]); - } catch (error) { - // Verify if navigation actually succeeded - const currentUrl = page.url(); - if (currentUrl === previousUrl) { - console.log("Previous URL same as current URL. Navigation failed."); - } - } + const selectorIndex = availableSelectors.indexOf(workingSelector); + availableSelectors = availableSelectors.slice(selectorIndex); - const currentUrl = page.url(); - if (visitedUrls.includes(currentUrl)) { - console.log(`Detected navigation to a previously visited URL: ${currentUrl}`); - - // Extract the current page number from the URL - const match = currentUrl.match(/\d+/); - if (match) { - const currentNumber = match[0]; - // Use visitedUrls.length + 1 as the next page number - const nextNumber = visitedUrls.length + 1; - - // Create new URL by replacing the current number with the next number - const nextUrl = currentUrl.replace(currentNumber, nextNumber.toString()); - - console.log(`Navigating to constructed URL: ${nextUrl}`); - - // Navigate to the next page - await Promise.all([ - page.waitForNavigation({ waitUntil: 'networkidle' }), - page.goto(nextUrl) - ]); - } - } - - // Give the page a moment to stabilize after navigation - await page.waitForTimeout(1000); - break; - case 'clickLoadMore': - while (true) { - let checkButton = null; - let workingSelector = null; - - for (let i = 0; i < availableSelectors.length; i++) { - const selector = availableSelectors[i]; try { - // Wait for selector with a short timeout - checkButton = await page.waitForSelector(selector, { state: 'attached' }); - if (checkButton) { - workingSelector = selector; + // Store current URL to check if navigation succeeded + const previousUrl = page.url(); + visitedUrls.push(previousUrl); + + // Try both click methods in sequence + try { + await Promise.all([ + page.waitForNavigation({ + waitUntil: 'networkidle', + timeout: 15000 + }), + nextButton.click() + ]); + } catch (error) { + // If we're still on the same URL, try dispatch event + if (page.url() === previousUrl) { + await Promise.all([ + page.waitForNavigation({ + waitUntil: 'networkidle', + timeout: 15000 + }), + nextButton.dispatchEvent('click') + ]); + } + } + + await page.waitForLoadState('domcontentloaded'); + await page.waitForLoadState('networkidle', { timeout: 30000 }); + + const currentUrl = page.url(); + if (visitedUrls.includes(currentUrl)) { + debugLog(`Navigation failed/Detected navigation to previously visited URL: ${currentUrl}`); + return allResults; + } + + // Give the page a moment to stabilize after navigation + await page.waitForTimeout(1000); + + } catch (error) { + debugLog(`Navigation failed completely: ${error.message}`); + return allResults; + } + break; + + case 'clickLoadMore': + while (true) { + let checkButton = null; + let workingSelector = null; + + for (const selector of availableSelectors) { + try { + checkButton = await page.waitForSelector(selector, { + state: 'attached', + timeout: 30000 + }); + if (checkButton) { + workingSelector = selector; + debugLog('Found working selector:', selector); + break; + } + } catch (error) { + debugLog(`Load More selector failed: ${selector}`); + } + } + + if (!workingSelector) { + debugLog('No working Load More selector found'); + const finalResults = await page.evaluate((cfg) => window.scrapeList(cfg), config); + allResults = allResults.concat(finalResults); + return allResults; + } + + const loadMoreButton = await page.$(workingSelector); + if (!loadMoreButton) { + debugLog('Load More button not found'); + const finalResults = await page.evaluate((cfg) => window.scrapeList(cfg), config); + allResults = allResults.concat(finalResults); + return allResults; + } + + const selectorIndex = availableSelectors.indexOf(workingSelector); + availableSelectors = availableSelectors.slice(selectorIndex); + + try { + try { + await loadMoreButton.click(); + } catch (error) { + await loadMoreButton.dispatchEvent('click'); + } + } catch (error) { + const finalResults = await page.evaluate((cfg) => window.scrapeList(cfg), config); + allResults = allResults.concat(finalResults); + return allResults; + } + + await page.waitForTimeout(2000); + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + await page.waitForTimeout(2000); + + const currentHeight = await page.evaluate(() => document.body.scrollHeight); + if (currentHeight === previousHeight) { + debugLog('No more items loaded after Load More'); + const finalResults = await page.evaluate((cfg) => window.scrapeList(cfg), config); + allResults = allResults.concat(finalResults); + return allResults; + } + previousHeight = currentHeight; + + if (config.limit && allResults.length >= config.limit) { + allResults = allResults.slice(0, config.limit); break; } - } catch (error) { - console.log(`Selector failed: ${selector}`); } - } - - if (!workingSelector) { - // No more working selectors available, so scrape the remaining items - const finalResults = await page.evaluate((cfg) => window.scrapeList(cfg), config); - allResults = allResults.concat(finalResults); - return allResults; - } - - const loadMoreButton = await page.$(workingSelector); - if (!loadMoreButton) { - // No more "Load More" button, so scrape the remaining items - const finalResults = await page.evaluate((cfg) => window.scrapeList(cfg), config); - allResults = allResults.concat(finalResults); - return allResults; - } - - const selectorIndex = availableSelectors.indexOf(workingSelector!); - availableSelectors = availableSelectors.slice(selectorIndex); - - // Click the 'Load More' button to load additional items - // await loadMoreButton.dispatchEvent('click'); - try { - await Promise.race([ - loadMoreButton.click(), - loadMoreButton.dispatchEvent('click') - ]); - } catch (error) { - console.log('Both click attempts failed'); - } - await page.waitForTimeout(2000); // Wait for new items to load - // After clicking 'Load More', scroll down to load more items - await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); - await page.waitForTimeout(2000); - - // Check if more items are available - const currentHeight = await page.evaluate(() => document.body.scrollHeight); - if (currentHeight === previousHeight) { - // No more items loaded, return the scraped results - const finalResults = await page.evaluate((cfg) => window.scrapeList(cfg), config); - allResults = allResults.concat(finalResults); - return allResults; - } - previousHeight = currentHeight; - - if (config.limit && allResults.length >= config.limit) { - // If limit is set and reached, return the limited results - allResults = allResults.slice(0, config.limit); break; - } - } - break; - default: - const results = await page.evaluate((cfg) => window.scrapeList(cfg), config); - allResults = allResults.concat(results); - return allResults; - } - if (config.limit && allResults.length >= config.limit) { - allResults = allResults.slice(0, config.limit); - break; - } + default: + const results = await page.evaluate((cfg) => window.scrapeList(cfg), config); + allResults = allResults.concat(results); + return allResults; + } + + if (config.limit && allResults.length >= config.limit) { + allResults = allResults.slice(0, config.limit); + break; + } } return allResults; diff --git a/package.json b/package.json index 5dae78a2..768b970f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "maxun", - "version": "0.0.7", + "version": "0.0.8", "author": "Maxun", "license": "AGPL-3.0-or-later", "dependencies": { @@ -47,7 +47,7 @@ "lodash": "^4.17.21", "loglevel": "^1.8.0", "loglevel-plugin-remote": "^0.6.8", - "maxun-core": "^0.0.9", + "maxun-core": "^0.0.10", "minio": "^8.0.1", "moment-timezone": "^0.5.45", "node-cron": "^3.0.3", diff --git a/public/locales/de.json b/public/locales/de.json index 14664774..51bbc782 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -32,6 +32,7 @@ "new": "Roboter erstellen", "modal": { "title": "Geben Sie die URL ein", + "login_title": "Erfordert diese Seite eine Anmeldung?", "label": "URL", "button": "Aufnahme starten" }, @@ -40,7 +41,8 @@ "duplicate": "Duplizieren", "notifications": { "delete_warning": "Roboter kann nicht gelöscht werden, da zugehörige Ausführungen vorhanden sind", - "delete_success": "Roboter erfolgreich gelöscht" + "delete_success": "Roboter erfolgreich gelöscht", + "auth_success": "Roboter erfolgreich authentifiziert" } }, "mainmenu": { @@ -204,7 +206,8 @@ "confirm_text_fields": "Bitte bestätigen Sie alle Textfelder", "unable_create_settings": "Listeneinstellungen können nicht erstellt werden. Stellen Sie sicher, dass Sie ein Feld für die Liste definiert haben.", "capture_text_discarded": "Texterfassung verworfen", - "capture_list_discarded": "Listenerfassung verworfen" + "capture_list_discarded": "Listenerfassung verworfen", + "label_required": "Beschriftung darf nicht leer sein" } }, "save_recording": { diff --git a/public/locales/en.json b/public/locales/en.json index 88622f20..a7843bd0 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -32,6 +32,7 @@ "new":"Create Robot", "modal":{ "title":"Enter the URL", + "login_title": "Does this website require login?", "label":"URL", "button":"Start Recording" }, @@ -41,7 +42,8 @@ "search":"Search Robots...", "notifications": { "delete_warning": "Cannot delete robot as it has associated runs", - "delete_success": "Robot deleted successfully" + "delete_success": "Robot deleted successfully", + "auth_success": "Robot successfully authenticated" } }, "mainmenu":{ @@ -206,7 +208,8 @@ "confirm_text_fields": "Please confirm all text fields", "unable_create_settings": "Unable to create list settings. Make sure you have defined a field for the list.", "capture_text_discarded": "Capture Text Discarded", - "capture_list_discarded": "Capture List Discarded" + "capture_list_discarded": "Capture List Discarded", + "label_required": "Label cannot be empty" } }, "save_recording": { diff --git a/public/locales/es.json b/public/locales/es.json index 7f669096..78e5a9d4 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -32,6 +32,7 @@ "new": "Crear Robot", "modal": { "title": "Ingresa la URL", + "login_title": "¿Este sitio requiere inicio de sesión?", "label": "URL", "button": "Comenzar grabación" }, @@ -41,7 +42,8 @@ "search": "Buscar robots...", "notifications": { "delete_warning": "No se puede eliminar el robot ya que tiene ejecuciones asociadas", - "delete_success": "Robot eliminado exitosamente" + "delete_success": "Robot eliminado exitosamente", + "auth_success": "Robot autenticado exitosamente" } }, "mainmenu": { @@ -205,7 +207,8 @@ "confirm_text_fields": "Por favor confirme todos los campos de texto", "unable_create_settings": "No se pueden crear las configuraciones de la lista. Asegúrese de haber definido un campo para la lista.", "capture_text_discarded": "Captura de texto descartada", - "capture_list_discarded": "Captura de lista descartada" + "capture_list_discarded": "Captura de lista descartada", + "label_required": "La etiqueta no puede estar vacía" } }, "save_recording": { diff --git a/public/locales/ja.json b/public/locales/ja.json index 5680471d..fc88abac 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -32,6 +32,7 @@ "new": "ロボットを作成", "modal": { "title": "URLを入力してください", + "login_title": "このサイトはログインが必要ですか?", "label": "URL", "button": "録画を開始" }, @@ -41,7 +42,8 @@ "search": "ロボットを検索...", "notifications": { "delete_warning": "関連する実行があるため、ロボットを削除できません", - "delete_success": "ロボットが正常に削除されました" + "delete_success": "ロボットが正常に削除されました", + "auth_success": "ロボットの認証に成功しました" } }, "mainmenu": { @@ -205,7 +207,8 @@ "confirm_text_fields": "すべてのテキストフィールドを確認してください", "unable_create_settings": "リスト設定を作成できません。リストのフィールドを定義したことを確認してください。", "capture_text_discarded": "テキスト取得が破棄されました", - "capture_list_discarded": "リスト取得が破棄されました" + "capture_list_discarded": "リスト取得が破棄されました", + "label_required": "ラベルは空にできません" } }, "save_recording": { diff --git a/public/locales/zh.json b/public/locales/zh.json index 536ea70c..1c45464b 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -32,6 +32,7 @@ "new": "创建机器人", "modal": { "title": "输入URL", + "login_title": "此网站需要登录吗?", "label": "URL", "button": "开始录制" }, @@ -41,7 +42,8 @@ "search": "搜索机器人...", "notifications": { "delete_warning": "无法删除机器人,因为它有关联的运行记录", - "delete_success": "机器人删除成功" + "delete_success": "机器人删除成功", + "auth_success": "机器人认证成功" } }, "mainmenu": { @@ -205,7 +207,8 @@ "confirm_text_fields": "请确认所有文本字段", "unable_create_settings": "无法创建列表设置。请确保您已为列表定义了字段。", "capture_text_discarded": "文本捕获已放弃", - "capture_list_discarded": "列表捕获已放弃" + "capture_list_discarded": "列表捕获已放弃", + "label_required": "标签不能为空" } }, "save_recording": { diff --git a/server/Dockerfile b/server/Dockerfile index becad25c..65e472eb 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -25,7 +25,7 @@ RUN mkdir -p /tmp/chromium-data-dir && \ # Install dependencies RUN apt-get update && apt-get install -y \ - libgbm-dev \ + libgbm1 \ libnss3 \ libatk1.0-0 \ libatk-bridge2.0-0 \ @@ -44,14 +44,8 @@ RUN apt-get update && apt-get install -y \ && rm -rf /var/lib/apt/lists/* \ && mkdir -p /tmp/.X11-unix && chmod 1777 /tmp/.X11-unix -# Add a dbus configuration to prevent connection errors -# RUN mkdir -p /var/run/dbus - -# Make the script executable -# RUN chmod +x ./start.sh - # Expose the backend port EXPOSE ${BACKEND_PORT:-8080} # Start the backend using the start script -CMD ["npm", "run", "server"] \ No newline at end of file +CMD ["npm", "run", "server"] diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 837fcd8a..49af7090 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -380,11 +380,19 @@ router.get( httpOnly: false, maxAge: 60000, }); // 1-minute expiration - res.cookie("robot_auth_message", "Robot successfully authenticated", { + // res.cookie("robot_auth_message", "Robot successfully authenticated", { + // httpOnly: false, + // maxAge: 60000, + // }); + res.cookie('robot_auth_robotId', robotId, { httpOnly: false, maxAge: 60000, }); - res.redirect(`${process.env.PUBLIC_URL}/robots/${robotId}/integrate` as string || `http://localhost:5173/robots/${robotId}/integrate`); + + const baseUrl = process.env.PUBLIC_URL || "http://localhost:5173"; + const redirectUrl = `${baseUrl}/robots/`; + + res.redirect(redirectUrl); } catch (error: any) { res.status(500).json({ message: `Google OAuth error: ${error.message}` }); } diff --git a/server/src/routes/storage.ts b/server/src/routes/storage.ts index ddadf240..01f1ca6b 100644 --- a/server/src/routes/storage.ts +++ b/server/src/routes/storage.ts @@ -18,12 +18,43 @@ import { AuthenticatedRequest } from './record'; import { computeNextRun } from '../utils/schedule'; import { capture } from "../utils/analytics"; import { tryCatch } from 'bullmq'; +import { encrypt, decrypt } from '../utils/auth'; import { WorkflowFile } from 'maxun-core'; import { Page } from 'playwright'; chromium.use(stealthPlugin()); export const router = Router(); +export const decryptWorkflowActions = async (workflow: any[],): Promise => { + // Create a deep copy to avoid mutating the original workflow + const processedWorkflow = JSON.parse(JSON.stringify(workflow)); + + // Process each step in the workflow + for (const step of processedWorkflow) { + if (!step.what) continue; + + // Process each action in the step + for (const action of step.what) { + // Only process type and press actions + if ((action.action === 'type' || action.action === 'press') && Array.isArray(action.args) && action.args.length > 1) { + // The second argument contains the encrypted value + const encryptedValue = action.args[1]; + if (typeof encryptedValue === 'string') { + try { + // Decrypt the value and update the args array + action.args[1] = await decrypt(encryptedValue); + } catch (error) { + console.error('Failed to decrypt value:', error); + // Keep the encrypted value if decryption fails + } + } + } + } + } + + return processedWorkflow; +}; + /** * Logs information about recordings API. */ @@ -55,6 +86,13 @@ router.get('/recordings/:id', requireSignIn, async (req, res) => { raw: true } ); + + if (data?.recording?.workflow) { + data.recording.workflow = await decryptWorkflowActions( + data.recording.workflow, + ); + } + return res.send(data); } catch (e) { logger.log('info', 'Error while reading robots'); @@ -116,13 +154,74 @@ function formatRunResponse(run: any) { return formattedRun; } +interface CredentialInfo { + value: string; + type: string; +} + +interface Credentials { + [key: string]: CredentialInfo; +} + +function updateTypeActionsInWorkflow(workflow: any[], credentials: Credentials) { + return workflow.map(step => { + if (!step.what) return step; + + const indicesToRemove = new Set(); + step.what.forEach((action: any, index: number) => { + if (!action.action || !action.args?.[0]) return; + + if ((action.action === 'type' || action.action === 'press') && credentials[action.args[0]]) { + indicesToRemove.add(index); + + if (step.what[index + 1]?.action === 'waitForLoadState') { + indicesToRemove.add(index + 1); + } + } + }); + + const filteredWhat = step.what.filter((_: any, index: number) => !indicesToRemove.has(index)); + + Object.entries(credentials).forEach(([selector, credentialInfo]) => { + const clickIndex = filteredWhat.findIndex((action: any) => + action.action === 'click' && action.args?.[0] === selector + ); + + if (clickIndex !== -1) { + const chars = credentialInfo.value.split(''); + + chars.forEach((char, i) => { + filteredWhat.splice(clickIndex + 1 + (i * 2), 0, { + action: 'type', + args: [ + selector, + encrypt(char), + credentialInfo.type + ] + }); + + filteredWhat.splice(clickIndex + 2 + (i * 2), 0, { + action: 'waitForLoadState', + args: ['networkidle'] + }); + }); + } + }); + + return { + ...step, + what: filteredWhat + }; + }); +} + /** * PUT endpoint to update the name and limit of a robot. */ router.put('/recordings/:id', requireSignIn, async (req: AuthenticatedRequest, res) => { try { const { id } = req.params; - const { name, limit } = req.body; + const { name, limit, credentials } = req.body; // Validate input if (!name && limit === undefined) { @@ -141,17 +240,21 @@ router.put('/recordings/:id', requireSignIn, async (req: AuthenticatedRequest, r robot.set('recording_meta', { ...robot.recording_meta, name }); } + let workflow = [...robot.recording.workflow]; // Create a copy of the workflow + + if (credentials) { + workflow = updateTypeActionsInWorkflow(workflow, credentials); + } + // Update the limit if (limit !== undefined) { - const workflow = [...robot.recording.workflow]; // Create a copy of the workflow - // Ensure the workflow structure is valid before updating if ( workflow.length > 0 && workflow[0]?.what?.[0] ) { // Create a new workflow object with the updated limit - const updatedWorkflow = workflow.map((step, index) => { + workflow = workflow.map((step, index) => { if (index === 0) { // Assuming you want to update the first step return { ...step, @@ -173,14 +276,13 @@ router.put('/recordings/:id', requireSignIn, async (req: AuthenticatedRequest, r } return step; }); - - // Replace the workflow in the recording object - robot.set('recording', { ...robot.recording, workflow: updatedWorkflow }); } else { return res.status(400).json({ error: 'Invalid workflow structure for updating limit.' }); } } + robot.set('recording', { ...robot.recording, workflow }); + await robot.save(); const updatedRobot = await Robot.findOne({ where: { 'recording_meta.id': id } }); diff --git a/server/src/storage/db.ts b/server/src/storage/db.ts index 2f0fcde4..cdd84655 100644 --- a/server/src/storage/db.ts +++ b/server/src/storage/db.ts @@ -29,7 +29,13 @@ export const connectDB = async () => { export const syncDB = async () => { try { //setupAssociations(); - await sequelize.sync({ force: false }); // force: true will drop and recreate tables on every run + const isDevelopment = process.env.NODE_ENV === 'development'; + // force: true will drop and recreate tables on every run + // Use `alter: true` only in development mode + await sequelize.sync({ + force: false, + alter: isDevelopment + }); console.log('Database synced successfully!'); } catch (error) { console.error('Failed to sync database:', error); diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 7368f0cb..6e36f287 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -39,6 +39,7 @@ interface MetaData { pairs: number; updatedAt: string; params: string[], + isLogin?: boolean; } /** @@ -97,6 +98,7 @@ export class WorkflowGenerator { pairs: 0, updatedAt: '', params: [], + isLogin: false, } /** @@ -134,9 +136,9 @@ export class WorkflowGenerator { */ private registerEventHandlers = (socket: Socket) => { socket.on('save', (data) => { - const { fileName, userId } = data; + const { fileName, userId, isLogin } = data; logger.log('debug', `Saving workflow ${fileName} for user ID ${userId}`); - this.saveNewWorkflow(fileName, userId); + this.saveNewWorkflow(fileName, userId, isLogin); }); socket.on('new-recording', () => this.workflowRecord = { workflow: [], @@ -425,6 +427,40 @@ export class WorkflowGenerator { return; } + if ((elementInfo?.tagName === 'INPUT' || elementInfo?.tagName === 'TEXTAREA') && selector) { + // Calculate the exact position within the element + const elementPos = await page.evaluate((selector) => { + const element = document.querySelector(selector); + if (!element) return null; + const rect = element.getBoundingClientRect(); + return { + x: rect.left, + y: rect.top + }; + }, selector); + + if (elementPos) { + const relativeX = coordinates.x - elementPos.x; + const relativeY = coordinates.y - elementPos.y; + + const pair: WhereWhatPair = { + where, + what: [{ + action: 'click', + args: [selector, { position: { x: relativeX, y: relativeY } }] + }] + }; + + if (selector) { + this.generatedData.lastUsedSelector = selector; + this.generatedData.lastAction = 'click'; + } + + await this.addPairToWorkflowAndNotifyClient(pair, page); + return; + } + } + //const element = await getElementMouseIsOver(page, coordinates); //logger.log('debug', `Element: ${JSON.stringify(element, null, 2)}`); if (selector) { @@ -474,6 +510,10 @@ export class WorkflowGenerator { public onKeyboardInput = async (key: string, coordinates: Coordinates, page: Page) => { let where: WhereWhatPair["where"] = { url: this.getBestUrl(page.url()) }; const selector = await this.generateSelector(page, coordinates, ActionType.Keydown); + + const elementInfo = await getElementInformation(page, coordinates, '', false); + const inputType = elementInfo?.attributes?.type || "text"; + if (selector) { where.selectors = [selector]; } @@ -481,7 +521,7 @@ export class WorkflowGenerator { where, what: [{ action: 'press', - args: [selector, encrypt(key)], + args: [selector, encrypt(key), inputType], }], } if (selector) { @@ -660,7 +700,7 @@ export class WorkflowGenerator { * @param fileName The name of the file. * @returns {Promise} */ - public saveNewWorkflow = async (fileName: string, userId: number) => { + public saveNewWorkflow = async (fileName: string, userId: number, isLogin: boolean) => { const recording = this.optimizeWorkflow(this.workflowRecord); try { this.recordingMeta = { @@ -670,6 +710,7 @@ export class WorkflowGenerator { pairs: recording.workflow.length, updatedAt: new Date().toLocaleString(), params: this.getParams() || [], + isLogin: isLogin, } const robot = await Robot.create({ userId, @@ -991,6 +1032,7 @@ export class WorkflowGenerator { let input = { selector: '', value: '', + type: '', actionCounter: 0, }; @@ -1005,7 +1047,7 @@ export class WorkflowGenerator { // when more than one press action is present, add a type action pair.what.splice(index - input.actionCounter, input.actionCounter, { action: 'type', - args: [input.selector, encrypt(input.value)], + args: [input.selector, encrypt(input.value), input.type], }, { action: 'waitForLoadState', args: ['networkidle'], @@ -1033,13 +1075,14 @@ export class WorkflowGenerator { action: 'waitForLoadState', args: ['networkidle'], }) - input = { selector: '', value: '', actionCounter: 0 }; + input = { selector: '', value: '', type: '', actionCounter: 0 }; } } else { pushTheOptimizedAction(pair, index); input = { selector: condition.args[0], value: condition.args[1], + type: condition.args[2], actionCounter: 1, }; } @@ -1048,7 +1091,7 @@ export class WorkflowGenerator { if (input.value.length !== 0) { pushTheOptimizedAction(pair, index); // clear the input - input = { selector: '', value: '', actionCounter: 0 }; + input = { selector: '', value: '', type: '', actionCounter: 0 }; } } }); diff --git a/src/App.test.tsx b/src/App.test.tsx deleted file mode 100644 index 2a68616d..00000000 --- a/src/App.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/src/App.tsx b/src/App.tsx index cdee8d40..b8497f58 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Routes, Route } from "react-router-dom"; import { ThemeProvider, createTheme } from "@mui/material/styles"; import { GlobalInfoProvider } from "./context/globalInfo"; -import { PageWrapper } from "./pages/PageWrappper"; +import { PageWrapper } from "./pages/PageWrapper"; import i18n from "./i18n"; import ThemeModeProvider from './context/theme-provider'; diff --git a/src/api/storage.ts b/src/api/storage.ts index 201a38cc..290f6e7f 100644 --- a/src/api/storage.ts +++ b/src/api/storage.ts @@ -5,6 +5,15 @@ import { ScheduleSettings } from "../components/robot/ScheduleSettings"; import { CreateRunResponse, ScheduleRunResponse } from "../pages/MainPage"; import { apiUrl } from "../apiConfig"; +interface CredentialInfo { + value: string; + type: string; +} + +interface Credentials { + [key: string]: CredentialInfo; +} + export const getStoredRecordings = async (): Promise => { try { const response = await axios.get(`${apiUrl}/storage/recordings`); @@ -19,7 +28,7 @@ export const getStoredRecordings = async (): Promise => { } }; -export const updateRecording = async (id: string, data: { name?: string; limit?: number }): Promise => { +export const updateRecording = async (id: string, data: { name?: string; limit?: number, credentials?: Credentials }): Promise => { try { const response = await axios.put(`${apiUrl}/storage/recordings/${id}`, data); if (response.status === 200) { diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index 663f6900..31eed9f1 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -5,7 +5,7 @@ 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, Tooltip } from "@mui/material"; -import { AccountCircle, Logout, Clear, YouTube, X, Update, Close, Language, Brightness7, Brightness4, Description } from "@mui/icons-material"; +import { AccountCircle, Logout, Clear, YouTube, X, Update, Close, Language, Description, LightMode, DarkMode } from "@mui/icons-material"; import { useNavigate } from 'react-router-dom'; import { AuthContext } from '../../context/auth'; import { SaveRecording } from '../recorder/SaveRecording'; @@ -114,7 +114,7 @@ export const NavBar: React.FC = ({ } }} > - {darkMode ? : } + {darkMode ? : } ); diff --git a/src/components/dashboard/NotFound.tsx b/src/components/dashboard/NotFound.tsx new file mode 100644 index 00000000..9b81acb0 --- /dev/null +++ b/src/components/dashboard/NotFound.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +export function NotFoundPage() { + return ( +
+

404 - Page Not Found

+

Oops! This page does not exist.

+
Take me to the homepage +
+ ); +} \ No newline at end of file diff --git a/src/components/integration/IntegrationSettings.tsx b/src/components/integration/IntegrationSettings.tsx index f9c397ae..31c93368 100644 --- a/src/components/integration/IntegrationSettings.tsx +++ b/src/components/integration/IntegrationSettings.tsx @@ -14,7 +14,6 @@ import axios from "axios"; import { useGlobalInfoStore } from "../../context/globalInfo"; import { getStoredRecording } from "../../api/storage"; import { apiUrl } from "../../apiConfig.js"; -import Cookies from 'js-cookie'; import { useTranslation } from "react-i18next"; @@ -30,20 +29,6 @@ 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, @@ -155,17 +140,6 @@ export const IntegrationSettingsModal = ({ }; useEffect(() => { - // Check if there is a success message in cookies - const status = getCookie("robot_auth_status"); - const message = getCookie("robot_auth_message"); - - if (status === "success" && message) { - notify("success", message); - // Clear the cookies after reading - removeCookie("robot_auth_status"); - removeCookie("robot_auth_message"); - } - // Check if we're on the callback URL const urlParams = new URLSearchParams(window.location.search); const code = urlParams.get("code"); diff --git a/src/components/pickers/DatePicker.tsx b/src/components/pickers/DatePicker.tsx index 61363984..00687115 100644 --- a/src/components/pickers/DatePicker.tsx +++ b/src/components/pickers/DatePicker.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 DatePickerProps { coordinates: Coordinates; diff --git a/src/components/recorder/AddWhatCondModal.tsx b/src/components/recorder/AddWhatCondModal.tsx index a6b50fc9..4dd333b5 100644 --- a/src/components/recorder/AddWhatCondModal.tsx +++ b/src/components/recorder/AddWhatCondModal.tsx @@ -3,7 +3,6 @@ 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"; diff --git a/src/components/recorder/Highlighter.tsx b/src/components/recorder/Highlighter.tsx index 3771b149..562fd81e 100644 --- a/src/components/recorder/Highlighter.tsx +++ b/src/components/recorder/Highlighter.tsx @@ -1,5 +1,4 @@ import React from 'react'; - import styled from "styled-components"; interface HighlighterProps { diff --git a/src/components/recorder/LeftSidePanel.tsx b/src/components/recorder/LeftSidePanel.tsx index afe56ffe..939f0329 100644 --- a/src/components/recorder/LeftSidePanel.tsx +++ b/src/components/recorder/LeftSidePanel.tsx @@ -3,7 +3,6 @@ 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 "./SidePanelHeader"; import { emptyWorkflow } from "../../shared/constants"; import { LeftSidePanelContent } from "./LeftSidePanelContent"; import { useBrowserDimensionsStore } from "../../context/browserDimensions"; diff --git a/src/components/recorder/LeftSidePanelContent.tsx b/src/components/recorder/LeftSidePanelContent.tsx index 4a980505..745e0e31 100644 --- a/src/components/recorder/LeftSidePanelContent.tsx +++ b/src/components/recorder/LeftSidePanelContent.tsx @@ -1,15 +1,13 @@ import React, { useCallback, useEffect, useState } from 'react'; -import Box from "@mui/material/Box"; import { Pair } from "./Pair"; 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 "../ui/buttons/AddButton"; import { AddPair } from "../../api/workflow"; import { GenericModal } from "../ui/GenericModal"; import { PairEditForm } from "./PairEditForm"; -import { Fab, Tooltip, Typography } from "@mui/material"; +import { Tooltip } from "@mui/material"; interface LeftSidePanelContentProps { workflow: WorkflowFile; diff --git a/src/components/recorder/PairDetail.tsx b/src/components/recorder/PairDetail.tsx index d330db1d..7ed081a9 100644 --- a/src/components/recorder/PairDetail.tsx +++ b/src/components/recorder/PairDetail.tsx @@ -1,6 +1,6 @@ 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 { IconButton, 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'; @@ -11,7 +11,6 @@ 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"; diff --git a/src/components/recorder/RightSidePanel.tsx b/src/components/recorder/RightSidePanel.tsx index deb45ff9..f324183d 100644 --- a/src/components/recorder/RightSidePanel.tsx +++ b/src/components/recorder/RightSidePanel.tsx @@ -3,7 +3,6 @@ import { Button, Paper, Box, TextField, IconButton } from "@mui/material"; import EditIcon from '@mui/icons-material/Edit'; import TextFieldsIcon from '@mui/icons-material/TextFields'; import DocumentScannerIcon from '@mui/icons-material/DocumentScanner'; -import { SimpleBox } from "../ui/Box"; import { WorkflowFile } from "maxun-core"; import Typography from "@mui/material/Typography"; import { useGlobalInfoStore } from "../../context/globalInfo"; @@ -12,15 +11,12 @@ import { useBrowserSteps } from '../../context/browserSteps'; import { useSocketStore } from '../../context/socket'; import { ScreenshotSettings } from '../../shared/types'; import InputAdornment from '@mui/material/InputAdornment'; -import { SidePanelHeader } from './SidePanelHeader'; import FormControlLabel from '@mui/material/FormControlLabel'; import FormControl from '@mui/material/FormControl'; import FormLabel from '@mui/material/FormLabel'; import Radio from '@mui/material/Radio'; import RadioGroup from '@mui/material/RadioGroup'; -import { emptyWorkflow } from "../../shared/constants"; import { getActiveWorkflow } from "../../api/workflow"; -import DeleteIcon from '@mui/icons-material/Delete'; import ActionDescriptionBox from '../action/ActionDescriptionBox'; import { useThemeMode } from '../../context/theme-provider'; import { useTranslation } from 'react-i18next'; diff --git a/src/components/recorder/SaveRecording.tsx b/src/components/recorder/SaveRecording.tsx index 4a2d0ba4..c6b5687b 100644 --- a/src/components/recorder/SaveRecording.tsx +++ b/src/components/recorder/SaveRecording.tsx @@ -22,7 +22,7 @@ export const SaveRecording = ({ fileName }: SaveRecordingProps) => { const [recordingName, setRecordingName] = useState(fileName); const [waitingForSave, setWaitingForSave] = useState(false); - const { browserId, setBrowserId, notify, recordings } = useGlobalInfoStore(); + const { browserId, setBrowserId, notify, recordings, isLogin } = useGlobalInfoStore(); const { socket } = useSocketStore(); const { state, dispatch } = useContext(AuthContext); const { user } = state; @@ -59,7 +59,7 @@ export const SaveRecording = ({ fileName }: SaveRecordingProps) => { // releases resources and changes the view for main page by clearing the global browserId const saveRecording = async () => { if (user) { - const payload = { fileName: recordingName, userId: user.id }; + const payload = { fileName: recordingName, userId: user.id, isLogin: isLogin }; socket?.emit('save', payload); setWaitingForSave(true); console.log(`Saving the recording as ${recordingName} for userId ${user.id}`); diff --git a/src/components/recorder/canvas.tsx b/src/components/recorder/canvas.tsx index 83e48274..cc75a6ba 100644 --- a/src/components/recorder/canvas.tsx +++ b/src/components/recorder/canvas.tsx @@ -1,6 +1,5 @@ import React, { useCallback, useEffect, useRef } from 'react'; import { useSocketStore } from '../../context/socket'; -import { getMappedCoordinates } from "../../helpers/inputHelpers"; import { useGlobalInfoStore } from "../../context/globalInfo"; import { useActionContext } from '../../context/browserActions'; import DatePicker from '../pickers/DatePicker'; diff --git a/src/components/robot/Recordings.tsx b/src/components/robot/Recordings.tsx index 46484cb1..e63f5e1c 100644 --- a/src/components/robot/Recordings.tsx +++ b/src/components/robot/Recordings.tsx @@ -1,13 +1,15 @@ -import React, { useState } from "react"; +import React, { useEffect, 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 { IntegrationSettingsModal } from "../integration/IntegrationSettings"; +import { RobotSettingsModal } from "./RobotSettings"; import { RobotEditModal } from "./RobotEdit"; import { RobotDuplicationModal } from "./RobotDuplicate"; import { useNavigate, useLocation, useParams } from "react-router-dom"; +import { useGlobalInfoStore } from "../../context/globalInfo"; +import { useTranslation } from "react-i18next"; interface RecordingsProps { handleEditRecording: (id: string, fileName: string) => void; @@ -26,6 +28,8 @@ export const Recordings = ({ const location = useLocation(); const { selectedRecordingId } = useParams(); const [params, setParams] = useState([]); + const { notify } = useGlobalInfoStore(); + const { t } = useTranslation(); const handleNavigate = (path: string, id: string, name: string, params: string[]) => { setParams(params); @@ -39,6 +43,31 @@ export const Recordings = ({ navigate("/robots"); // Navigate back to the main robots page }; + useEffect(() => { + // Helper function to get and clear a cookie + const getAndClearCookie = (name: string) => { + const value = document.cookie + .split('; ') + .find(row => row.startsWith(`${name}=`)) + ?.split('=')[1]; + + if (value) { + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`; + } + + return value; + }; + + const authStatus = getAndClearCookie('robot_auth_status'); + const robotId = getAndClearCookie('robot_auth_robotId'); + + if (authStatus === 'success' && robotId) { + notify(authStatus, t("recordingtable.notifications.auth_success")); + + handleNavigate(`/robots/${robotId}/integrate`, robotId, "", []);'' + } + }, []); + // Determine which modal to open based on the current route const getCurrentModal = () => { const currentPath = location.pathname; diff --git a/src/components/robot/RecordingsTable.tsx b/src/components/robot/RecordingsTable.tsx index 4bbcb654..a7594b08 100644 --- a/src/components/robot/RecordingsTable.tsx +++ b/src/components/robot/RecordingsTable.tsx @@ -11,8 +11,32 @@ import TableRow from '@mui/material/TableRow'; import { useEffect } from "react"; import { WorkflowFile } from "maxun-core"; import SearchIcon from '@mui/icons-material/Search'; -import { IconButton, Button, Box, Typography, TextField, MenuItem, Menu, ListItemIcon, ListItemText } from "@mui/material"; -import { Schedule, DeleteForever, Edit, PlayCircle, Settings, Power, ContentCopy, MoreHoriz } from "@mui/icons-material"; +import { + IconButton, + Button, + Box, + Typography, + TextField, + MenuItem, + Menu, + ListItemIcon, + ListItemText, + CircularProgress, + RadioGroup, + FormControlLabel, + Radio, + Checkbox, +} from "@mui/material"; +import { + Schedule, + DeleteForever, + Edit, + PlayCircle, + Settings, + Power, + ContentCopy, + MoreHoriz +} from "@mui/icons-material"; import { useGlobalInfoStore } from "../../context/globalInfo"; import { checkRunsForRecording, deleteRecordingFromStorage, getStoredRecordings } from "../../api/storage"; import { Add } from "@mui/icons-material"; @@ -52,7 +76,14 @@ interface RecordingsTableProps { handleDuplicateRobot: (id: string, name: string, params: string[]) => void; } -export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handleScheduleRecording, handleIntegrateRecording, handleSettingsRecording, handleEditRobot, handleDuplicateRobot }: RecordingsTableProps) => { +export const RecordingsTable = ({ + handleEditRecording, + handleRunRecording, + handleScheduleRecording, + handleIntegrateRecording, + handleSettingsRecording, + handleEditRobot, + handleDuplicateRobot }: RecordingsTableProps) => { const { t } = useTranslation(); const [page, setPage] = React.useState(0); const [rowsPerPage, setRowsPerPage] = React.useState(10); @@ -85,7 +116,20 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl }, ]; - const { notify, setRecordings, browserId, setBrowserId, setInitialUrl, recordingUrl, setRecordingUrl, recordingName, setRecordingName, recordingId, setRecordingId } = useGlobalInfoStore(); + const { + notify, + setRecordings, + browserId, + setBrowserId, + setInitialUrl, + recordingUrl, + setRecordingUrl, + isLogin, + setIsLogin, + recordingName, + setRecordingName, + recordingId, + setRecordingId } = useGlobalInfoStore(); const navigate = useNavigate(); const handleChangePage = (event: unknown, newPage: number) => { @@ -200,101 +244,107 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl - - - - - {columns.map((column) => ( - - {column.label} - - ))} - - - - {filteredRows.length !== 0 ? filteredRows - .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) - .map((row) => { - return ( - - {columns.map((column) => { - // @ts-ignore - const value: any = row[column.id]; - if (value !== undefined) { - return ( - - {value} - - ); - } else { - switch (column.id) { - case 'interpret': - return ( - - handleRunRecording(row.id, row.name, row.params || [])} /> - - ); - case 'schedule': - return ( - - handleScheduleRecording(row.id, row.name, row.params || [])} /> - - ); - case 'integrate': - return ( - - handleIntegrateRecording(row.id, row.name, row.params || [])} /> - - ); - case 'options': - return ( - - handleEditRobot(row.id, row.name, row.params || [])} - handleDuplicate={() => { - handleDuplicateRobot(row.id, row.name, row.params || []); - }} - handleDelete={() => { + {rows.length === 0 ? ( + + + + ) : ( + +
+ + + {columns.map((column) => ( + + {column.label} + + ))} + + + + {filteredRows.length !== 0 ? filteredRows + .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + .map((row) => { + return ( + + {columns.map((column) => { + // @ts-ignore + const value: any = row[column.id]; + if (value !== undefined) { + return ( + + {value} + + ); + } else { + switch (column.id) { + case 'interpret': + return ( + + handleRunRecording(row.id, row.name, row.params || [])} /> + + ); + case 'schedule': + return ( + + handleScheduleRecording(row.id, row.name, row.params || [])} /> + + ); + case 'integrate': + return ( + + handleIntegrateRecording(row.id, row.name, row.params || [])} /> + + ); + case 'options': + return ( + + handleEditRobot(row.id, row.name, row.params || [])} + handleDuplicate={() => { + handleDuplicateRobot(row.id, row.name, row.params || []); + }} + handleDelete={() => { - checkRunsForRecording(row.id).then((result: boolean) => { - if (result) { - notify('warning', t('recordingtable.notifications.delete_warning')); - } - }) + checkRunsForRecording(row.id).then((result: boolean) => { + if (result) { + notify('warning', t('recordingtable.notifications.delete_warning')); + } + }) - deleteRecordingFromStorage(row.id).then((result: boolean) => { - if (result) { - setRows([]); - notify('success', t('recordingtable.notifications.delete_success')); - fetchRecordings(); - } - }) - }} - /> - - ); - case 'settings': - return ( - - handleSettingsRecording(row.id, row.name, row.params || [])} /> - - ); - default: - return null; + deleteRecordingFromStorage(row.id).then((result: boolean) => { + if (result) { + setRows([]); + notify('success', t('recordingtable.notifications.delete_success')); + fetchRecordings(); + } + }) + }} + /> + + ); + case 'settings': + return ( + + handleSettingsRecording(row.id, row.name, row.params || [])} /> + + ); + default: + return null; + } } - } - })} - - ); - }) - : null} - -
-
+ })} + + ); + }) + : null} + + + + )} setModalOpen(false)} modalStyle={modalStyle}> -
+
{t('recordingtable.modal.title')} + + setIsLogin(e.target.checked)} + color="primary" + /> + } + label={t('recordingtable.modal.login_title')} + style={{ marginBottom: '10px' }} + /> + +
- - - - ) - } + {(Object.keys(credentials).length > 0) && ( + <> + + {t('Input Texts')} + + {renderAllCredentialFields()} + + )} + + + + + + + )} ); -}; +}; \ No newline at end of file diff --git a/src/components/robot/ScheduleSettings.tsx b/src/components/robot/ScheduleSettings.tsx index 658ce594..71951646 100644 --- a/src/components/robot/ScheduleSettings.tsx +++ b/src/components/robot/ScheduleSettings.tsx @@ -190,7 +190,7 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initia sx={dropDownStyle} > {units.map((unit) => ( - {unit} + {unit.charAt(0).toUpperCase() + unit.slice(1).toLowerCase()} ))} @@ -207,7 +207,9 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initia sx={dropDownStyle} > {days.map((day) => ( - {day} + + {day.charAt(0).toUpperCase() + day.slice(1).toLowerCase()} + ))} @@ -265,7 +267,7 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initia sx={dropDownStyle} > {validMomentTimezones.map((tz) => ( - {tz} + {tz.charAt(0).toUpperCase() + tz.slice(1).toLowerCase()} ))} diff --git a/src/components/run/InterpretationButtons.tsx b/src/components/run/InterpretationButtons.tsx index 0edcf682..a6ebb1bb 100644 --- a/src/components/run/InterpretationButtons.tsx +++ b/src/components/run/InterpretationButtons.tsx @@ -1,7 +1,6 @@ import { Box, Button, Stack, Typography, CircularProgress } from "@mui/material"; -import { PlayCircle } from "@mui/icons-material"; import React, { useCallback, useEffect, useState } from "react"; -import { interpretCurrentRecording, stopCurrentInterpretation } from "../../api/recording"; +import { interpretCurrentRecording } from "../../api/recording"; import { useSocketStore } from "../../context/socket"; import { useGlobalInfoStore } from "../../context/globalInfo"; import { GenericModal } from "../ui/GenericModal"; diff --git a/src/components/run/RunsTable.tsx b/src/components/run/RunsTable.tsx index b652aef0..1264dd3c 100644 --- a/src/components/run/RunsTable.tsx +++ b/src/components/run/RunsTable.tsx @@ -9,7 +9,7 @@ import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; import TablePagination from '@mui/material/TablePagination'; import TableRow from '@mui/material/TableRow'; -import { Accordion, AccordionSummary, AccordionDetails, Typography, Box, TextField } from '@mui/material'; +import { Accordion, AccordionSummary, AccordionDetails, Typography, Box, TextField, CircularProgress } from '@mui/material'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import SearchIcon from '@mui/icons-material/Search'; import { useNavigate } from 'react-router-dom'; @@ -18,7 +18,6 @@ import { getStoredRuns } from "../../api/storage"; import { RunSettings } from "./RunSettings"; import { CollapsibleRow } from "./ColapsibleRow"; -// Export columns before the component export const columns: readonly Column[] = [ { id: 'runStatus', label: 'Status', minWidth: 80 }, { id: 'name', label: 'Name', minWidth: 80 }, @@ -70,7 +69,6 @@ export const RunsTable: React.FC = ({ const { t } = useTranslation(); const navigate = useNavigate(); - // Update column labels using translation if needed const translatedColumns = columns.map(column => ({ ...column, label: t(`runstable.${column.id}`, column.label) @@ -162,48 +160,54 @@ export const RunsTable: React.FC = ({ sx={{ width: '250px' }} /> - - {Object.entries(groupedRows).map(([id, data]) => ( - handleAccordionChange(id, isExpanded)}> - }> - {data[data.length - 1].name} - - - - - - - {translatedColumns.map((column) => ( - - {column.label} - - ))} - - - - {data - .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) - .map((row) => ( - - ))} - -
-
-
- ))} -
+ {rows.length === 0 ? ( + + + + ) : ( + + {Object.entries(groupedRows).map(([id, data]) => ( + handleAccordionChange(id, isExpanded)}> + }> + {data[data.length - 1].name} + + + + + + + {translatedColumns.map((column) => ( + + {column.label} + + ))} + + + + {data + .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + .map((row) => ( + + ))} + +
+
+
+ ))} +
+ )} void; closeNotify: () => void; + isLogin: boolean; + setIsLogin: (isLogin: boolean) => void; recordings: string[]; setRecordings: (recordings: string[]) => void; rerenderRuns: boolean; @@ -53,6 +55,7 @@ class GlobalInfoStore implements Partial { recordingName = ''; initialUrl = 'https://'; recordingUrl = 'https://'; + isLogin = false; currentWorkflowActionsState = { hasScrapeListAction: false, hasScreenshotAction: false, @@ -75,6 +78,7 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => { const [recordingLength, setRecordingLength] = useState(globalInfoStore.recordingLength); const [recordingId, setRecordingId] = useState(globalInfoStore.recordingId); const [recordingName, setRecordingName] = useState(globalInfoStore.recordingName); + const [isLogin, setIsLogin] = useState(globalInfoStore.isLogin); const [initialUrl, setInitialUrl] = useState(globalInfoStore.initialUrl); const [recordingUrl, setRecordingUrl] = useState(globalInfoStore.recordingUrl); const [currentWorkflowActionsState, setCurrentWorkflowActionsState] = useState(globalInfoStore.currentWorkflowActionsState); @@ -127,6 +131,8 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => { setInitialUrl, recordingUrl, setRecordingUrl, + isLogin, + setIsLogin, currentWorkflowActionsState, setCurrentWorkflowActionsState, shouldResetInterpretationLog, diff --git a/src/pages/PageWrappper.tsx b/src/pages/PageWrapper.tsx similarity index 96% rename from src/pages/PageWrappper.tsx rename to src/pages/PageWrapper.tsx index d778c186..1e73f26a 100644 --- a/src/pages/PageWrappper.tsx +++ b/src/pages/PageWrapper.tsx @@ -12,7 +12,7 @@ import Login from './Login'; import Register from './Register'; import UserRoute from '../routes/userRoute'; import { Routes, Route, useNavigate, Navigate } from 'react-router-dom'; -import { Runs } from '../components/run/Runs'; +import { NotFoundPage } from '../components/dashboard/NotFound'; export const PageWrapper = () => { const [open, setOpen] = useState(false); @@ -75,6 +75,7 @@ export const PageWrapper = () => { path="/register" element={} /> + } />