diff --git a/README.md b/README.md index 72d4eccb0..f56e45640 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ -

+

- +
Maxun
- Open-Source No-Code Web Data Extraction Platform
-

+ The Easiest Way To Extract Web Data With No Code
+

Maxun lets you train a robot in 2 minutes and scrape the web on auto-pilot. Web data extraction doesn't get easier than this! @@ -15,115 +15,62 @@ Maxun lets you train a robot in 2 minutes and scrape the web on auto-pilot. Web

- Go To App | - Documentation | - Website | - Discord | - Twitter | + Go To App • + Documentation • + Website • + DiscordWatch Tutorials

getmaxun%2Fmaxun | Trendshift

-![maxun_gif](https://github.com/user-attachments/assets/3e0b0cf8-9e52-44d2-a140-b26b7b481477) - - - -# Getting Started -The simplest & fastest way to get started is to use the hosted version: https://app.maxun.dev. Maxun Cloud deals with anti-bot detection, huge proxy network with automatic proxy rotation, and CAPTCHA solving. - -# Local Installation -1. Create a root folder for your project (e.g. 'maxun') -2. Create a file named `.env` in the root folder of the project -3. Example env file can be viewed [here](https://github.com/getmaxun/maxun/blob/master/ENVEXAMPLE). Copy all content of example env to your `.env` file. -4. Choose your installation method below - -### Docker Compose -1. Copy paste the [docker-compose.yml file](https://github.com/getmaxun/maxun/blob/master/docker-compose.yml) into your root folder -2. Ensure you have setup the `.env` file in that same folder -3. Run the command below from a terminal -``` -docker-compose up -d -``` -You can access the frontend at http://localhost:5173/ and backend at http://localhost:8080/ - -### Without Docker -1. Ensure you have Node.js, PostgreSQL, MinIO and Redis installed on your system. -2. Run the commands below -``` -git clone https://github.com/getmaxun/maxun - -# change directory to the project root -cd maxun - -# install dependencies -npm install - -# change directory to maxun-core to install dependencies -cd maxun-core -npm install - -# get back to the root directory -cd .. - -# install chromium and its dependencies -npx playwright install --with-deps chromium - -# get back to the root directory -cd .. - -# start frontend and backend together -npm run start -``` -You can access the frontend at http://localhost:5173/ and backend at http://localhost:8080/ - - -# Environment Variables -1. Create a file named `.env` in the root folder of the project -2. Example env file can be viewed [here](https://github.com/getmaxun/maxun/blob/master/ENVEXAMPLE). - -| Variable | Mandatory | Description | If Not Set | -|-----------------------|-----------|----------------------------------------------------------------------------------------------|--------------------------------------------------------------| -| `BACKEND_PORT` | Yes | Port to run backend on. Needed for Docker setup | Default value: 8080 | -| `FRONTEND_PORT` | Yes | Port to run frontend on. Needed for Docker setup | Default value: 5173 | -| `BACKEND_URL` | Yes | URL to run backend on. | Default value: http://localhost:8080 | -| `VITE_BACKEND_URL` | Yes | URL used by frontend to connect to backend | Default value: http://localhost:8080 | -| `PUBLIC_URL` | Yes | URL to run frontend on. | Default value: http://localhost:5173 | -| `VITE_PUBLIC_URL` | Yes | URL used by backend to connect to frontend | Default value: http://localhost:5173 | -| `JWT_SECRET` | Yes | Secret key used to sign and verify JSON Web Tokens (JWTs) for authentication. | JWT authentication will not work. | -| `DB_NAME` | Yes | Name of the Postgres database to connect to. | Database connection will fail. | -| `DB_USER` | Yes | Username for Postgres database authentication. | Database connection will fail. | -| `DB_PASSWORD` | Yes | Password for Postgres database authentication. | Database connection will fail. | -| `DB_HOST` | Yes | Host address where the Postgres database server is running. | Database connection will fail. | -| `DB_PORT` | Yes | Port number used to connect to the Postgres database server. | Database connection will fail. | -| `ENCRYPTION_KEY` | Yes | Key used for encrypting sensitive data (proxies, passwords). | Encryption functionality will not work. | -| `SESSION_SECRET` | No | A strong, random string used to sign session cookies | Uses default secret. Recommended to define your own session secret to avoid session hijacking. | -| `MINIO_ENDPOINT` | Yes | Endpoint URL for MinIO, to store Robot Run Screenshots. | Connection to MinIO storage will fail. | -| `MINIO_PORT` | Yes | Port number for MinIO service. | Connection to MinIO storage will fail. | -| `MINIO_CONSOLE_PORT` | No | Port number for MinIO WebUI service. Needed for Docker setup. | Cannot access MinIO Web UI. | -| `MINIO_ACCESS_KEY` | Yes | Access key for authenticating with MinIO. | MinIO authentication will fail. | -| `GOOGLE_CLIENT_ID` | No | Client ID for Google OAuth. Used for Google Sheet integration authentication. | Google login will not work. | -| `GOOGLE_CLIENT_SECRET`| No | Client Secret for Google OAuth. Used for Google Sheet integration authentication. | Google login will not work. | -| `GOOGLE_REDIRECT_URI` | No | Redirect URI for handling Google OAuth responses. | Google login will not work. | -| `AIRTABLE_CLIENT_ID` | No | Client ID for Airtable, used for Airtable integration authentication. | Airtable login will not work. | -| `AIRTABLE_REDIRECT_URI` | No | Redirect URI for handling Airtable OAuth responses. | Airtable login will not work. | -| `MAXUN_TELEMETRY` | No | Disables telemetry to stop sending anonymous usage data. Keeping it enabled helps us understand how the product is used and assess the impact of any new changes. Please keep it enabled. | Telemetry data will not be collected. | - -# How Do I Self-Host? -Checkout community self hosting guide: https://docs.maxun.dev/self-host - -# How Does It Work? -Maxun lets you create custom robots which emulate user actions and extract data. A robot can perform any of the actions: Capture List, Capture Text or Capture Screenshot. Once a robot is created, it will keep extracting data for you without manual intervention - -![Screenshot 2024-10-23 222138](https://github.com/user-attachments/assets/53573c98-769e-490d-829e-ada9fac0764f) - -## 1. Robot Actions -1. Capture List: Useful to extract structured and bulk items from the website. Example: Scrape products from Amazon etc. +https://github.com/user-attachments/assets/c6baa75f-b950-482c-8d26-8a8b6c5382c3 + +### Getting Started +The simplest & fastest way to get started is to use the hosted version: https://app.maxun.dev. You can self-host if you like! + +### Installation +Maxun can run locally with or without Docker +1. [Setup with Docker Compose](https://docs.maxun.dev/installation/docker) +2. [Setup without Docker](https://docs.maxun.dev/installation/local) +3. [Environment Variables](https://docs.maxun.dev/installation/environment_variables) + +### Upgrading & Self Hosting +1. [Self Host Maxun With Docker & Portainer](https://docs.maxun.dev/self-host) +2. [Upgrade Maxun With Docker Compose Setup](https://docs.maxun.dev/installation/upgrade#upgrading-with-docker-compose) +3. [Upgrade Maxun Without Docker Compose Setup](https://docs.maxun.dev/installation/upgrade#upgrading-with-local-setup) + +### How Does It Work? +Maxun lets you create custom robots which emulate user actions and extract data. A robot can perform any of the actions: Capture List, Capture Text or Capture Screenshot. Once a robot is created, it will keep extracting data for you without manual intervention. +1. Capture List: Useful to extract structured and bulk items from the website. 2. Capture Text: Useful to extract individual text content from the website. 3. Capture Screenshot: Get fullpage or visible section screenshots of the website. -# Features +### Sponsors + + + + + +
+
+ +

+ LambdaTest +
+
+ GenAI-powered Quality Engineering Platform that empowers teams to test intelligently, smarter, and ship faster. +
+ +
+ CyberYozh App +
+
+ Infrastructure for developers working with multi‑accounting & automation in one place. +
+ +### Features - ✨ Extract Data With No-Code - ✨ Handle Pagination & Scrolling - ✨ Run Robots On A Specific Schedule @@ -134,11 +81,11 @@ Maxun lets you create custom robots which emulate user actions and extract data. - ✨ Integrations - ✨ MCP -# Use Cases +### Use Cases Maxun can be used for various use-cases, including lead generation, market research, content aggregation and more. View use-cases in detail here: https://www.maxun.dev/#usecases -# Screenshots +### Screenshots ![Maxun PH Launch (1)-1-1](https://github.com/user-attachments/assets/d7c75fa2-2bbc-47bb-a5f6-0ee6c162f391) ![Maxun PH Launch (1)-2-1](https://github.com/user-attachments/assets/d85a3ec7-8ce8-4daa-89aa-52d9617e227a) ![Maxun PH Launch (1)-3-1](https://github.com/user-attachments/assets/4bd5a0b4-485d-44f4-a487-edd9afc18b11) @@ -149,18 +96,18 @@ View use-cases in detail here: https://www.maxun.dev/#usecases ![Maxun PH Launch (1)-8-1](https://github.com/user-attachments/assets/16ee4a71-772a-49ae-a0e5-cb0529519bda) ![Maxun PH Launch (1)-9-1](https://github.com/user-attachments/assets/160f46fa-0357-4c1b-ba50-b4fe64453bb7) -# Note +### Note This project is in early stages of development. Your feedback is very important for us - we're actively working on improvements. -# License +### License

This project is licensed under AGPLv3.

-# Support Us +### Support Us Star the repository, contribute if you love what we’re building, or [sponsor us](https://github.com/sponsors/amhsirak). -# Contributors +### Contributors Thank you to the combined efforts of everyone who contributes! diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 000000000..da89e75a0 --- /dev/null +++ b/SETUP.md @@ -0,0 +1,76 @@ +# Local Installation +1. Create a root folder for your project (e.g. 'maxun') +2. Create a file named `.env` in the root folder of the project +3. Example env file can be viewed [here](https://github.com/getmaxun/maxun/blob/master/ENVEXAMPLE). Copy all content of example env to your `.env` file. +4. Choose your installation method below + +### Docker Compose +1. Copy paste the [docker-compose.yml file](https://github.com/getmaxun/maxun/blob/master/docker-compose.yml) into your root folder +2. Ensure you have setup the `.env` file in that same folder +3. Run the command below from a terminal +``` +docker-compose up -d +``` +You can access the frontend at http://localhost:5173/ and backend at http://localhost:8080/ + +### Without Docker +1. Ensure you have Node.js, PostgreSQL, MinIO and Redis installed on your system. +2. Run the commands below +``` +git clone https://github.com/getmaxun/maxun + +# change directory to the project root +cd maxun + +# install dependencies +npm install + +# change directory to maxun-core to install dependencies +cd maxun-core +npm install + +# get back to the root directory +cd .. + +# install chromium and its dependencies +npx playwright install --with-deps chromium + +# get back to the root directory +cd .. + +# start frontend and backend together +npm run start +``` +You can access the frontend at http://localhost:5173/ and backend at http://localhost:8080/ + + +# Environment Variables +1. Create a file named `.env` in the root folder of the project +2. Example env file can be viewed [here](https://github.com/getmaxun/maxun/blob/master/ENVEXAMPLE). + +| Variable | Mandatory | Description | If Not Set | +|-----------------------|-----------|----------------------------------------------------------------------------------------------|--------------------------------------------------------------| +| `BACKEND_PORT` | Yes | Port to run backend on. Needed for Docker setup | Default value: 8080 | +| `FRONTEND_PORT` | Yes | Port to run frontend on. Needed for Docker setup | Default value: 5173 | +| `BACKEND_URL` | Yes | URL to run backend on. | Default value: http://localhost:8080 | +| `VITE_BACKEND_URL` | Yes | URL used by frontend to connect to backend | Default value: http://localhost:8080 | +| `PUBLIC_URL` | Yes | URL to run frontend on. | Default value: http://localhost:5173 | +| `VITE_PUBLIC_URL` | Yes | URL used by backend to connect to frontend | Default value: http://localhost:5173 | +| `JWT_SECRET` | Yes | Secret key used to sign and verify JSON Web Tokens (JWTs) for authentication. | JWT authentication will not work. | +| `DB_NAME` | Yes | Name of the Postgres database to connect to. | Database connection will fail. | +| `DB_USER` | Yes | Username for Postgres database authentication. | Database connection will fail. | +| `DB_PASSWORD` | Yes | Password for Postgres database authentication. | Database connection will fail. | +| `DB_HOST` | Yes | Host address where the Postgres database server is running. | Database connection will fail. | +| `DB_PORT` | Yes | Port number used to connect to the Postgres database server. | Database connection will fail. | +| `ENCRYPTION_KEY` | Yes | Key used for encrypting sensitive data (proxies, passwords). | Encryption functionality will not work. | +| `SESSION_SECRET` | No | A strong, random string used to sign session cookies | Uses default secret. Recommended to define your own session secret to avoid session hijacking. | +| `MINIO_ENDPOINT` | Yes | Endpoint URL for MinIO, to store Robot Run Screenshots. | Connection to MinIO storage will fail. | +| `MINIO_PORT` | Yes | Port number for MinIO service. | Connection to MinIO storage will fail. | +| `MINIO_CONSOLE_PORT` | No | Port number for MinIO WebUI service. Needed for Docker setup. | Cannot access MinIO Web UI. | +| `MINIO_ACCESS_KEY` | Yes | Access key for authenticating with MinIO. | MinIO authentication will fail. | +| `GOOGLE_CLIENT_ID` | No | Client ID for Google OAuth. Used for Google Sheet integration authentication. | Google login will not work. | +| `GOOGLE_CLIENT_SECRET`| No | Client Secret for Google OAuth. Used for Google Sheet integration authentication. | Google login will not work. | +| `GOOGLE_REDIRECT_URI` | No | Redirect URI for handling Google OAuth responses. | Google login will not work. | +| `AIRTABLE_CLIENT_ID` | No | Client ID for Airtable, used for Airtable integration authentication. | Airtable login will not work. | +| `AIRTABLE_REDIRECT_URI` | No | Redirect URI for handling Airtable OAuth responses. | Airtable login will not work. | +| `MAXUN_TELEMETRY` | No | Disables telemetry to stop sending anonymous usage data. Keeping it enabled helps us understand how the product is used and assess the impact of any new changes. Please keep it enabled. | Telemetry data will not be collected. | diff --git a/src/components/robot/RecordingsTable.tsx b/src/components/robot/RecordingsTable.tsx index 504cfa434..f06270ed3 100644 --- a/src/components/robot/RecordingsTable.tsx +++ b/src/components/robot/RecordingsTable.tsx @@ -42,6 +42,7 @@ import { Add } from "@mui/icons-material"; import { useNavigate } from 'react-router-dom'; import { canCreateBrowserInState, getActiveBrowserId, stopRecording } from "../../api/recording"; import { GenericModal } from '../ui/GenericModal'; +import { useTheme } from '@mui/material/styles'; declare global { interface Window { @@ -148,12 +149,15 @@ export const RecordingsTable = ({ handleEditRobot, handleDuplicateRobot }: RecordingsTableProps) => { const { t } = useTranslation(); + const theme = useTheme(); const [page, setPage] = React.useState(0); const [rowsPerPage, setRowsPerPage] = React.useState(10); const { data: recordingsData = [], isLoading: isFetching, error, refetch } = useCachedRecordings(); const [isModalOpen, setModalOpen] = React.useState(false); const [searchTerm, setSearchTerm] = React.useState(''); const [isWarningModalOpen, setWarningModalOpen] = React.useState(false); + const [isDeleteConfirmOpen, setDeleteConfirmOpen] = React.useState(false); + const [pendingDeleteId, setPendingDeleteId] = React.useState(null); const [activeBrowserId, setActiveBrowserId] = React.useState(''); const columns = useMemo(() => [ @@ -431,6 +435,32 @@ export const RecordingsTable = ({ return filteredRows.slice(start, start + rowsPerPage); }, [filteredRows, page, rowsPerPage]); + const openDeleteConfirm = React.useCallback((id: string) => { + setPendingDeleteId(String(id)); + setDeleteConfirmOpen(true); + }, []); + + const confirmDeleteRecording = React.useCallback(async () => { + if (!pendingDeleteId) return; + const hasRuns = await checkRunsForRecording(pendingDeleteId); + if (hasRuns) { + notify('warning', t('recordingtable.notifications.delete_warning')); + setDeleteConfirmOpen(false); + setPendingDeleteId(null); + return; + } + + const success = await deleteRecordingFromStorage(pendingDeleteId); + if (success) { + notify('success', t('recordingtable.notifications.delete_success')); + refetch(); + } + setDeleteConfirmOpen(false); + setPendingDeleteId(null); + }, [pendingDeleteId, notify, t, refetch]); + + const pendingRow = pendingDeleteId ? rows.find(r => String(r.id) === pendingDeleteId) : null; + const handlers = useMemo(() => ({ handleRunRecording, handleScheduleRecording, @@ -439,19 +469,7 @@ export const RecordingsTable = ({ handleEditRobot, handleDuplicateRobot, handleRetrainRobot, - handleDelete: async (id: string) => { - const hasRuns = await checkRunsForRecording(id); - if (hasRuns) { - notify('warning', t('recordingtable.notifications.delete_warning')); - return; - } - - const success = await deleteRecordingFromStorage(id); - if (success) { - notify('success', t('recordingtable.notifications.delete_success')); - refetch(); - } - } + handleDelete: async (id: string) => openDeleteConfirm(id) }), [handleRunRecording, handleScheduleRecording, handleIntegrateRecording, handleSettingsRecording, handleEditRobot, handleDuplicateRobot, handleRetrainRobot, notify, t, refetch]); return ( @@ -628,6 +646,33 @@ export const RecordingsTable = ({ + { setDeleteConfirmOpen(false); setPendingDeleteId(null); }} + modalStyle={{ ...modalStyle, padding: 0, backgroundColor: 'transparent', width: 'auto', maxWidth: '520px' }} + > + + + + {t('recordingtable.delete_confirm.title', { name: pendingRow?.name, defaultValue: 'Delete {{name}}?' })} + + + {t('recordingtable.delete_confirm.message', { + name: pendingRow?.name, + defaultValue: 'Are you sure you want to delete the robot "{{name}}"?' + })} + + + + + + + + ); } diff --git a/src/components/robot/pages/RobotConfigPage.tsx b/src/components/robot/pages/RobotConfigPage.tsx index 53ae90abe..d52b985e5 100644 --- a/src/components/robot/pages/RobotConfigPage.tsx +++ b/src/components/robot/pages/RobotConfigPage.tsx @@ -62,7 +62,6 @@ export const RobotConfigPage: React.FC = ({ { return ( <> - + {t("List Limits")} - {scrapeListLimits.map((limitInfo, index) => ( - { - const value = parseInt(e.target.value, 10); - if (value >= 1) { - handleLimitChange( - limitInfo.pairIndex, - limitInfo.actionIndex, - limitInfo.argIndex, - value - ); - } - }} - inputProps={{ min: 1 }} - style={{ marginBottom: "20px" }} - /> - ))} + {scrapeListLimits.map((limitInfo, index) => { + // Get the corresponding scrapeList action to extract its name + const scrapeListAction = robot?.recording?.workflow?.[limitInfo.pairIndex]?.what?.[limitInfo.actionIndex]; + const actionName = + scrapeListAction?.name || + (scrapeListAction?.args?.[0]?.__name) || + `List Limit ${index + 1}`; + + return ( + { + const value = parseInt(e.target.value, 10); + if (value >= 1) { + handleLimitChange( + limitInfo.pairIndex, + limitInfo.actionIndex, + limitInfo.argIndex, + value + ); + } + }} + inputProps={{ min: 1 }} + style={{ marginBottom: "20px" }} + /> + ); + })} ); }; @@ -539,22 +548,55 @@ export const RobotEditPage = ({ handleStart }: RobotSettingsProps) => { if (!robot || !robot.recording || !robot.recording.workflow) return null; const editableActions = new Set(['screenshot', 'scrapeList', 'scrapeSchema']); - const inputs: JSX.Element[] = []; + const textInputs: JSX.Element[] = []; + const screenshotInputs: JSX.Element[] = []; + const listInputs: JSX.Element[] = []; + + let textCount = 0; + let screenshotCount = 0; + let listCount = 0; robot.recording.workflow.forEach((pair, pairIndex) => { if (!pair.what) return; pair.what.forEach((action, actionIndex) => { - // Only show editable name inputs for meaningful action types if (!editableActions.has(String(action.action))) return; - // derive current name from possible fields - const currentName = + let currentName = action.name || (action.args && action.args[0] && typeof action.args[0] === 'object' && action.args[0].__name) || ''; - inputs.push( + if (!currentName) { + switch (action.action) { + case 'scrapeSchema': + textCount++; + currentName = `Text ${textCount}`; + break; + case 'screenshot': + screenshotCount++; + currentName = `Screenshot ${screenshotCount}`; + break; + case 'scrapeList': + listCount++; + currentName = `List ${listCount}`; + break; + } + } else { + switch (action.action) { + case 'scrapeSchema': + textCount++; + break; + case 'screenshot': + screenshotCount++; + break; + case 'scrapeList': + listCount++; + break; + } + } + + const textField = ( { fullWidth /> ); + + switch (action.action) { + case 'scrapeSchema': + textInputs.push(textField); + break; + case 'screenshot': + screenshotInputs.push(textField); + break; + case 'scrapeList': + listInputs.push(textField); + break; + } }); }); - if (inputs.length === 0) return null; + if (textInputs.length === 1 && textCount === 1) { + robot.recording.workflow.forEach((pair, pairIndex) => { + if (!pair.what) return; + + pair.what.forEach((action, actionIndex) => { + if (action.action === 'scrapeSchema') { + const existingName = + action.name || + (action.args && action.args[0] && typeof action.args[0] === 'object' && action.args[0].__name) || + ''; + + const currentName = !existingName ? 'Texts' : existingName; + + textInputs[0] = ( + handleActionNameChange(pairIndex, actionIndex, e.target.value)} + style={{ marginBottom: '12px' }} + fullWidth + /> + ); + } + }); + }); + } + + const hasAnyInputs = textInputs.length > 0 || screenshotInputs.length > 0 || listInputs.length > 0; + if (!hasAnyInputs) return null; return ( <> - + {t('Actions')} - {inputs} + + {textInputs.length > 0 && ( + <> + + Texts + + {textInputs} + + )} + + {screenshotInputs.length > 0 && ( + <> + 0 ? '16px' : '0' }}> + Screenshots + + {screenshotInputs} + + )} + + {listInputs.length > 0 && ( + <> + 0 || screenshotInputs.length > 0) ? '16px' : '0' }}> + Lists + + {listInputs} + + )} ); }; diff --git a/src/components/run/ColapsibleRow.tsx b/src/components/run/ColapsibleRow.tsx index bf6e9c405..63025a465 100644 --- a/src/components/run/ColapsibleRow.tsx +++ b/src/components/run/ColapsibleRow.tsx @@ -3,6 +3,7 @@ import * as React from "react"; import TableRow from "@mui/material/TableRow"; import TableCell from "@mui/material/TableCell"; import { Box, Collapse, IconButton, Typography, Chip, TextField } from "@mui/material"; +import { Button } from "@mui/material"; import { DeleteForever, KeyboardArrowDown, KeyboardArrowUp, Settings } from "@mui/icons-material"; import { deleteRunFromStorage } from "../../api/storage"; import { columns, Data } from "./RunsTable"; @@ -11,6 +12,7 @@ import { GenericModal } from "../ui/GenericModal"; import { modalStyle } from "../recorder/AddWhereCondModal"; import { getUserById } from "../../api/auth"; import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material/styles"; interface RunTypeChipProps { runByUserId?: string; @@ -39,6 +41,8 @@ interface CollapsibleRowProps { } export const CollapsibleRow = ({ row, handleDelete, isOpen, onToggleExpanded, currentLog, abortRunHandler, runningRecordingName, urlRunId }: CollapsibleRowProps) => { const { t } = useTranslation(); + const theme = useTheme(); + const [isDeleteOpen, setDeleteOpen] = useState(false); const [openSettingsModal, setOpenSettingsModal] = useState(false); const [userEmail, setUserEmail] = useState(null); const runByLabel = row.runByScheduleId @@ -83,6 +87,17 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, onToggleExpanded, cu fetchUserEmail(); }, [row.runByUserId]); + const handleConfirmDelete = async () => { + try { + const res = await deleteRunFromStorage(`${row.runId}`); + if (res) { + handleDelete(); + } + } finally { + setDeleteOpen(false); + } + }; + return ( *': { borderBottom: 'unset' } }} hover role="checkbox" tabIndex={-1} key={row.id}> @@ -120,13 +135,7 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, onToggleExpanded, cu case 'delete': return ( - { - deleteRunFromStorage(`${row.runId}`).then((result: boolean) => { - if (result) { - handleDelete(); - } - }) - }}> + setDeleteOpen(true)}> @@ -192,6 +201,32 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, onToggleExpanded, cu + + setDeleteOpen(false)} modalStyle={{ ...modalStyle, padding: 0, backgroundColor: 'transparent', width: 'auto', maxWidth: '520px' }}> + + + {t('runs_table.delete_confirm.title', { + name: row.name, + defaultValue: 'Delete run "{{name}}"?' + })} + + + {t('runs_table.delete_confirm.message', { + name: row.name, + defaultValue: 'Are you sure you want to delete the run "{{name}}"?' + })} + + + + + + + ); } diff --git a/src/components/run/RunContent.tsx b/src/components/run/RunContent.tsx index ee397bf42..4cc787d14 100644 --- a/src/components/run/RunContent.tsx +++ b/src/components/run/RunContent.tsx @@ -87,8 +87,8 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe const hasOldFormat = !row.serializableOutput.scrapeSchema && !row.serializableOutput.scrapeList && Object.keys(row.serializableOutput).length > 0; if (hasLegacySchema || hasLegacyList || hasOldFormat) { - setIsLegacyData(true); processLegacyData(row.serializableOutput); + setIsLegacyData(false); return; } @@ -154,11 +154,12 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe const data = legacyOutput[key]; if (Array.isArray(data)) { - const isNestedArray = data.length > 0 && Array.isArray(data[0]); + const firstNonNullElement = data.find(item => item !== null && item !== undefined); + const isNestedArray = firstNonNullElement && Array.isArray(firstNonNullElement); if (isNestedArray) { data.forEach((subArray, index) => { - if (Array.isArray(subArray) && subArray.length > 0) { + if (subArray !== null && subArray !== undefined && Array.isArray(subArray) && subArray.length > 0) { const filteredData = subArray.filter(row => row && typeof row === 'object' && Object.values(row).some(value => value !== undefined && value !== "") ); @@ -171,7 +172,7 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe }); } else { const filteredData = data.filter(row => - row && typeof row === 'object' && Object.values(row).some(value => value !== undefined && value !== "") + row && typeof row === 'object' && !Array.isArray(row) && Object.values(row).some(value => value !== undefined && value !== "") ); if (filteredData.length > 0) { @@ -208,7 +209,7 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe if (Array.isArray(schemaOutput)) { const filteredData = schemaOutput.filter(row => - row && Object.values(row).some(value => value !== undefined && value !== "") + row && typeof row === 'object' && Object.values(row).some(value => value !== undefined && value !== "") ); if (filteredData.length > 0) { @@ -231,7 +232,7 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe const data = schemaOutput[key]; if (Array.isArray(data)) { const filteredData = data.filter(row => - Object.values(row).some(value => value !== undefined && value !== "") + row && typeof row === 'object' && Object.values(row).some(value => value !== undefined && value !== "") ); dataByKey[key] = filteredData; @@ -272,7 +273,7 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe const tableData = scrapeListData[key]; if (Array.isArray(tableData) && tableData.length > 0) { const filteredData = tableData.filter(row => - Object.values(row).some(value => value !== undefined && value !== "") + row && typeof row === 'object' && Object.values(row).some(value => value !== undefined && value !== "") ); if (filteredData.length > 0) { tablesList.push(filteredData); diff --git a/src/helpers/clientSelectorGenerator.ts b/src/helpers/clientSelectorGenerator.ts index 03ad67a23..3189a4303 100644 --- a/src/helpers/clientSelectorGenerator.ts +++ b/src/helpers/clientSelectorGenerator.ts @@ -564,16 +564,17 @@ class ClientSelectorGenerator { return true; } - if (element.children.length > 0) { - return false; - } - const text = (element.textContent || "").trim(); + const hasVisibleText = text.length > 0; - if (text.length > 0) { + if (hasVisibleText || element.querySelector("svg")) { return true; } + if (element.children.length > 0) { + return false; + } + return false; }