chore: sync with develop
This commit is contained in:
@@ -15,7 +15,8 @@ Maxun lets you train a robot in 2 minutes and scrape the web on auto-pilot. Web
|
||||
|
||||
|
||||
<p align="center">
|
||||
<a href="https://maxun-website.vercel.app/?ref=ghread"><b>Website</b></a> |
|
||||
<a href="https://docs.maxun.dev/?ref=ghread"><b>Documentation</b></a> |
|
||||
<a href="https://www.maxun.dev/?ref=ghread"><b>Website</b></a> |
|
||||
<a href="https://discord.gg/5GbPjBUkws"><b>Discord</b></a> |
|
||||
<a href="https://x.com/maxun_io?ref=ghread"><b>Twitter</b></a> |
|
||||
<a href="https://docs.google.com/forms/d/e/1FAIpQLSdbD2uhqC4sbg4eLZ9qrFbyrfkXZ2XsI6dQ0USRCQNZNn5pzg/viewform"><b>Join Maxun Cloud</b></a> |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -188,69 +188,201 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
|
||||
* @param {Object.<string, {selector: string, tag: string}>} lists The named lists of HTML elements.
|
||||
* @returns {Array.<Object.<string, string>>}
|
||||
*/
|
||||
window.scrapeSchema = function (lists) {
|
||||
window.scrapeSchema = function(lists) {
|
||||
// Utility functions remain the same
|
||||
function omap(object, f, kf = (x) => x) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(object)
|
||||
.map(([k, v]) => [kf(k), f(v)]),
|
||||
.map(([k, v]) => [kf(k), f(v)]),
|
||||
);
|
||||
}
|
||||
|
||||
function ofilter(object, f) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(object)
|
||||
.filter(([k, v]) => f(k, v)),
|
||||
.filter(([k, v]) => f(k, v)),
|
||||
);
|
||||
}
|
||||
|
||||
function getSeedKey(listObj) {
|
||||
const maxLength = Math.max(...Object.values(omap(listObj, (x) => document.querySelectorAll(x.selector).length)));
|
||||
return Object.keys(ofilter(listObj, (_, v) => document.querySelectorAll(v.selector).length === maxLength))[0];
|
||||
function findAllElements(config) {
|
||||
// Regular DOM query if no special delimiters
|
||||
if (!config.selector.includes('>>') && !config.selector.includes(':>>')) {
|
||||
return Array.from(document.querySelectorAll(config.selector));
|
||||
}
|
||||
|
||||
// First handle iframe traversal if present
|
||||
if (config.selector.includes(':>>')) {
|
||||
const parts = config.selector.split(':>>').map(s => s.trim());
|
||||
let currentElements = [document];
|
||||
|
||||
// Traverse through each part of the selector
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
const nextElements = [];
|
||||
const isLast = i === parts.length - 1;
|
||||
|
||||
for (const element of currentElements) {
|
||||
try {
|
||||
// For document or iframe document
|
||||
const doc = element.contentDocument || element || element.contentWindow?.document;
|
||||
if (!doc) continue;
|
||||
|
||||
// Query elements in current context
|
||||
const found = Array.from(doc.querySelectorAll(part));
|
||||
|
||||
if (isLast) {
|
||||
// If it's the last part, keep all matching elements
|
||||
nextElements.push(...found);
|
||||
} else {
|
||||
// If not last, only keep iframes for next iteration
|
||||
const iframes = found.filter(el => el.tagName === 'IFRAME');
|
||||
nextElements.push(...iframes);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Cannot access iframe content:', error, {
|
||||
part,
|
||||
element,
|
||||
index: i
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (nextElements.length === 0) {
|
||||
console.warn('No elements found for part:', part, 'at depth:', i);
|
||||
return [];
|
||||
}
|
||||
currentElements = nextElements;
|
||||
}
|
||||
|
||||
return currentElements;
|
||||
}
|
||||
|
||||
// Handle shadow DOM traversal
|
||||
if (config.selector.includes('>>')) {
|
||||
const parts = config.selector.split('>>').map(s => s.trim());
|
||||
let currentElements = [document];
|
||||
|
||||
for (const part of parts) {
|
||||
const nextElements = [];
|
||||
for (const element of currentElements) {
|
||||
// Try regular DOM first
|
||||
const found = Array.from(element.querySelectorAll(part));
|
||||
|
||||
// Then check shadow roots
|
||||
for (const foundEl of found) {
|
||||
if (foundEl.shadowRoot) {
|
||||
nextElements.push(foundEl.shadowRoot);
|
||||
} else {
|
||||
nextElements.push(foundEl);
|
||||
}
|
||||
}
|
||||
}
|
||||
currentElements = nextElements;
|
||||
}
|
||||
return currentElements.filter(el => !(el instanceof ShadowRoot));
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// Modified to handle iframe context for URL resolution
|
||||
function getElementValue(element, attribute) {
|
||||
if (!element) return null;
|
||||
|
||||
// Get the base URL for resolving relative URLs
|
||||
const baseURL = element.ownerDocument?.location?.href || window.location.origin;
|
||||
|
||||
switch (attribute) {
|
||||
case 'href': {
|
||||
const relativeHref = element.getAttribute('href');
|
||||
return relativeHref ? new URL(relativeHref, baseURL).href : null;
|
||||
}
|
||||
case 'src': {
|
||||
const relativeSrc = element.getAttribute('src');
|
||||
return relativeSrc ? new URL(relativeSrc, baseURL).href : null;
|
||||
}
|
||||
case 'innerText':
|
||||
return element.innerText?.trim();
|
||||
case 'textContent':
|
||||
return element.textContent?.trim();
|
||||
default:
|
||||
return element.getAttribute(attribute) || element.innerText?.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Rest of the functions remain largely the same
|
||||
function getSeedKey(listObj) {
|
||||
const maxLength = Math.max(...Object.values(
|
||||
omap(listObj, (x) => findAllElements(x).length)
|
||||
));
|
||||
return Object.keys(
|
||||
ofilter(listObj, (_, v) => findAllElements(v).length === maxLength)
|
||||
)[0];
|
||||
}
|
||||
|
||||
// Find minimal bounding elements
|
||||
function getMBEs(elements) {
|
||||
return elements.map((element) => {
|
||||
let candidate = element;
|
||||
const isUniqueChild = (e) => elements
|
||||
.filter((elem) => e.parentNode?.contains(elem))
|
||||
.filter((elem) => {
|
||||
// Handle both iframe and shadow DOM boundaries
|
||||
const sameContext = elem.getRootNode() === e.getRootNode() &&
|
||||
elem.ownerDocument === e.ownerDocument;
|
||||
return sameContext && e.parentNode?.contains(elem);
|
||||
})
|
||||
.length === 1;
|
||||
|
||||
|
||||
while (candidate && isUniqueChild(candidate)) {
|
||||
candidate = candidate.parentNode;
|
||||
}
|
||||
|
||||
|
||||
return candidate;
|
||||
});
|
||||
}
|
||||
|
||||
const seedName = getSeedKey(lists);
|
||||
const seedElements = Array.from(document.querySelectorAll(lists[seedName].selector));
|
||||
const seedElements = findAllElements(lists[seedName]);
|
||||
const MBEs = getMBEs(seedElements);
|
||||
|
||||
return MBEs.map((mbe) => omap(
|
||||
lists,
|
||||
({ selector, attribute }, key) => {
|
||||
const elem = Array.from(document.querySelectorAll(selector)).find((elem) => mbe.contains(elem));
|
||||
if (!elem) return undefined;
|
||||
|
||||
switch (attribute) {
|
||||
case 'href':
|
||||
const relativeHref = elem.getAttribute('href');
|
||||
return relativeHref ? new URL(relativeHref, window.location.origin).href : null;
|
||||
case 'src':
|
||||
const relativeSrc = elem.getAttribute('src');
|
||||
return relativeSrc ? new URL(relativeSrc, window.location.origin).href : null;
|
||||
case 'innerText':
|
||||
return elem.innerText;
|
||||
case 'textContent':
|
||||
return elem.textContent;
|
||||
default:
|
||||
return elem.innerText;
|
||||
}
|
||||
},
|
||||
(key) => key // Use the original key in the output
|
||||
|
||||
const mbeResults = MBEs.map((mbe) => omap(
|
||||
lists,
|
||||
(config) => {
|
||||
const elem = findAllElements(config)
|
||||
.find((elem) => mbe.contains(elem));
|
||||
|
||||
return elem ? getElementValue(elem, config.attribute) : undefined;
|
||||
},
|
||||
(key) => key
|
||||
)) || [];
|
||||
}
|
||||
|
||||
// If MBE approach didn't find all elements, try independent scraping
|
||||
if (mbeResults.some(result => Object.values(result).some(v => v === undefined))) {
|
||||
// Fall back to independent scraping
|
||||
const results = [];
|
||||
const foundElements = new Map();
|
||||
|
||||
// Find all elements for each selector
|
||||
Object.entries(lists).forEach(([key, config]) => {
|
||||
const elements = findAllElements(config);
|
||||
foundElements.set(key, elements);
|
||||
});
|
||||
|
||||
// Create result objects for each found element
|
||||
foundElements.forEach((elements, key) => {
|
||||
elements.forEach((element, index) => {
|
||||
if (!results[index]) {
|
||||
results[index] = {};
|
||||
}
|
||||
results[index][key] = getElementValue(element, lists[key].attribute);
|
||||
});
|
||||
});
|
||||
|
||||
return results.filter(result => Object.keys(result).length > 0);
|
||||
}
|
||||
|
||||
return mbeResults;
|
||||
};
|
||||
|
||||
/**
|
||||
* Scrapes multiple lists of similar items based on a template item.
|
||||
@@ -262,108 +394,275 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
|
||||
* @returns {Array.<Array.<Object>>} Array of arrays of scraped items, one sub-array per list
|
||||
*/
|
||||
window.scrapeList = async function ({ listSelector, fields, limit = 10 }) {
|
||||
// Helper function to extract values from elements
|
||||
// Enhanced query function to handle both iframe and shadow DOM
|
||||
const queryElement = (rootElement, selector) => {
|
||||
if (!selector.includes('>>') && !selector.includes(':>>')) {
|
||||
return rootElement.querySelector(selector);
|
||||
}
|
||||
|
||||
const parts = selector.split(/(?:>>|:>>)/).map(part => part.trim());
|
||||
let currentElement = rootElement;
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (!currentElement) return null;
|
||||
|
||||
// Handle iframe traversal
|
||||
if (currentElement.tagName === 'IFRAME') {
|
||||
try {
|
||||
const iframeDoc = currentElement.contentDocument || currentElement.contentWindow.document;
|
||||
currentElement = iframeDoc.querySelector(parts[i]);
|
||||
continue;
|
||||
} catch (e) {
|
||||
console.warn('Cannot access iframe content:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Try regular DOM first
|
||||
let nextElement = currentElement.querySelector(parts[i]);
|
||||
|
||||
// Try shadow DOM if not found
|
||||
if (!nextElement && currentElement.shadowRoot) {
|
||||
nextElement = currentElement.shadowRoot.querySelector(parts[i]);
|
||||
}
|
||||
|
||||
// Check children's shadow roots if still not found
|
||||
if (!nextElement) {
|
||||
const children = Array.from(currentElement.children || []);
|
||||
for (const child of children) {
|
||||
if (child.shadowRoot) {
|
||||
nextElement = child.shadowRoot.querySelector(parts[i]);
|
||||
if (nextElement) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentElement = nextElement;
|
||||
}
|
||||
|
||||
return currentElement;
|
||||
};
|
||||
|
||||
// Enhanced query all function for both contexts
|
||||
const queryElementAll = (rootElement, selector) => {
|
||||
if (!selector.includes('>>') && !selector.includes(':>>')) {
|
||||
return rootElement.querySelectorAll(selector);
|
||||
}
|
||||
|
||||
const parts = selector.split(/(?:>>|:>>)/).map(part => part.trim());
|
||||
let currentElements = [rootElement];
|
||||
|
||||
for (const part of parts) {
|
||||
const nextElements = [];
|
||||
|
||||
for (const element of currentElements) {
|
||||
// Handle iframe traversal
|
||||
if (element.tagName === 'IFRAME') {
|
||||
try {
|
||||
const iframeDoc = element.contentDocument || element.contentWindow.document;
|
||||
nextElements.push(...iframeDoc.querySelectorAll(part));
|
||||
} catch (e) {
|
||||
console.warn('Cannot access iframe content:', e);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Regular DOM elements
|
||||
if (element.querySelectorAll) {
|
||||
nextElements.push(...element.querySelectorAll(part));
|
||||
}
|
||||
|
||||
// Shadow DOM elements
|
||||
if (element.shadowRoot) {
|
||||
nextElements.push(...element.shadowRoot.querySelectorAll(part));
|
||||
}
|
||||
|
||||
// Check children's shadow roots
|
||||
const children = Array.from(element.children || []);
|
||||
for (const child of children) {
|
||||
if (child.shadowRoot) {
|
||||
nextElements.push(...child.shadowRoot.querySelectorAll(part));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentElements = nextElements;
|
||||
}
|
||||
|
||||
return currentElements;
|
||||
};
|
||||
|
||||
// Enhanced value extraction with context awareness
|
||||
function extractValue(element, attribute) {
|
||||
if (!element) return null;
|
||||
|
||||
if (attribute === 'innerText') {
|
||||
return element.innerText.trim();
|
||||
} else if (attribute === 'innerHTML') {
|
||||
return element.innerHTML.trim();
|
||||
} else if (attribute === 'src' || attribute === 'href') {
|
||||
const attrValue = element.getAttribute(attribute);
|
||||
return attrValue ? new URL(attrValue, window.location.origin).href : null;
|
||||
}
|
||||
return element.getAttribute(attribute);
|
||||
if (!element) return null;
|
||||
|
||||
// Get context-aware base URL
|
||||
const baseURL = element.ownerDocument?.location?.href || window.location.origin;
|
||||
|
||||
// Check shadow root first
|
||||
if (element.shadowRoot) {
|
||||
const shadowContent = element.shadowRoot.textContent;
|
||||
if (shadowContent?.trim()) {
|
||||
return shadowContent.trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (attribute === 'innerText') {
|
||||
return element.innerText.trim();
|
||||
} else if (attribute === 'innerHTML') {
|
||||
return element.innerHTML.trim();
|
||||
} else if (attribute === 'src' || attribute === 'href') {
|
||||
const attrValue = element.getAttribute(attribute);
|
||||
return attrValue ? new URL(attrValue, baseURL).href : null;
|
||||
}
|
||||
return element.getAttribute(attribute);
|
||||
}
|
||||
|
||||
// Helper function to find table ancestors
|
||||
// Enhanced table ancestor finding with context support
|
||||
function findTableAncestor(element) {
|
||||
let currentElement = element;
|
||||
const MAX_DEPTH = 5;
|
||||
let depth = 0;
|
||||
|
||||
while (currentElement && depth < MAX_DEPTH) {
|
||||
if (currentElement.tagName === 'TD') {
|
||||
return { type: 'TD', element: currentElement };
|
||||
} else if (currentElement.tagName === 'TR') {
|
||||
return { type: 'TR', element: currentElement };
|
||||
}
|
||||
currentElement = currentElement.parentElement;
|
||||
depth++;
|
||||
}
|
||||
return null;
|
||||
let currentElement = element;
|
||||
const MAX_DEPTH = 5;
|
||||
let depth = 0;
|
||||
|
||||
while (currentElement && depth < MAX_DEPTH) {
|
||||
// Handle shadow DOM
|
||||
if (currentElement.getRootNode() instanceof ShadowRoot) {
|
||||
currentElement = currentElement.getRootNode().host;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentElement.tagName === 'TD') {
|
||||
return { type: 'TD', element: currentElement };
|
||||
} else if (currentElement.tagName === 'TR') {
|
||||
return { type: 'TR', element: currentElement };
|
||||
}
|
||||
|
||||
// Handle iframe crossing
|
||||
if (currentElement.tagName === 'IFRAME') {
|
||||
try {
|
||||
currentElement = currentElement.contentDocument.body;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
currentElement = currentElement.parentElement;
|
||||
}
|
||||
depth++;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Helper function to get cell index
|
||||
function getCellIndex(td) {
|
||||
let index = 0;
|
||||
let sibling = td;
|
||||
while (sibling = sibling.previousElementSibling) {
|
||||
index++;
|
||||
}
|
||||
return index;
|
||||
if (td.getRootNode() instanceof ShadowRoot) {
|
||||
const shadowRoot = td.getRootNode();
|
||||
const allCells = Array.from(shadowRoot.querySelectorAll('td'));
|
||||
return allCells.indexOf(td);
|
||||
}
|
||||
|
||||
let index = 0;
|
||||
let sibling = td;
|
||||
while (sibling = sibling.previousElementSibling) {
|
||||
index++;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
// Helper function to check for TH elements
|
||||
function hasThElement(row, tableFields) {
|
||||
for (const [label, { selector }] of Object.entries(tableFields)) {
|
||||
const element = row.querySelector(selector);
|
||||
if (element) {
|
||||
let current = element;
|
||||
while (current && current !== row) {
|
||||
if (current.tagName === 'TH') {
|
||||
return true;
|
||||
}
|
||||
current = current.parentElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
for (const [_, { selector }] of Object.entries(tableFields)) {
|
||||
const element = queryElement(row, selector);
|
||||
if (element) {
|
||||
let current = element;
|
||||
while (current && current !== row) {
|
||||
if (current.getRootNode() instanceof ShadowRoot) {
|
||||
current = current.getRootNode().host;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current.tagName === 'TH') return true;
|
||||
|
||||
if (current.tagName === 'IFRAME') {
|
||||
try {
|
||||
current = current.contentDocument.body;
|
||||
} catch (e) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
current = current.parentElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Helper function to filter rows
|
||||
function filterRowsBasedOnTag(rows, tableFields) {
|
||||
for (const row of rows) {
|
||||
if (hasThElement(row, tableFields)) {
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
return rows.filter(row => row.getElementsByTagName('TH').length === 0);
|
||||
// Include shadow DOM in TH search
|
||||
return rows.filter(row => {
|
||||
const directTH = row.getElementsByTagName('TH').length === 0;
|
||||
const shadowTH = row.shadowRoot ?
|
||||
row.shadowRoot.querySelector('th') === null : true;
|
||||
return directTH && shadowTH;
|
||||
});
|
||||
}
|
||||
|
||||
// Class similarity comparison functions
|
||||
function calculateClassSimilarity(classList1, classList2) {
|
||||
const set1 = new Set(classList1);
|
||||
const set2 = new Set(classList2);
|
||||
|
||||
// Calculate intersection
|
||||
const intersection = new Set([...set1].filter(x => set2.has(x)));
|
||||
|
||||
// Calculate union
|
||||
const union = new Set([...set1, ...set2]);
|
||||
|
||||
// Return Jaccard similarity coefficient
|
||||
return intersection.size / union.size;
|
||||
}
|
||||
const set1 = new Set(classList1);
|
||||
const set2 = new Set(classList2);
|
||||
const intersection = new Set([...set1].filter(x => set2.has(x)));
|
||||
const union = new Set([...set1, ...set2]);
|
||||
return intersection.size / union.size;
|
||||
}
|
||||
|
||||
// New helper function to find elements with similar classes
|
||||
// Enhanced similar elements finding with context support
|
||||
function findSimilarElements(baseElement, similarityThreshold = 0.7) {
|
||||
const baseClasses = Array.from(baseElement.classList);
|
||||
|
||||
if (baseClasses.length === 0) return [];
|
||||
|
||||
const allElements = [];
|
||||
|
||||
const potentialElements = document.getElementsByTagName(baseElement.tagName);
|
||||
// Get elements from main document
|
||||
allElements.push(...document.getElementsByTagName(baseElement.tagName));
|
||||
|
||||
return Array.from(potentialElements).filter(element => {
|
||||
if (element === baseElement) return false;
|
||||
|
||||
const similarity = calculateClassSimilarity(
|
||||
baseClasses,
|
||||
Array.from(element.classList)
|
||||
);
|
||||
|
||||
return similarity >= similarityThreshold;
|
||||
// Get elements from shadow DOM
|
||||
if (baseElement.getRootNode() instanceof ShadowRoot) {
|
||||
const shadowHost = baseElement.getRootNode().host;
|
||||
allElements.push(...shadowHost.getElementsByTagName(baseElement.tagName));
|
||||
}
|
||||
|
||||
// Get elements from iframes
|
||||
const iframes = document.getElementsByTagName('iframe');
|
||||
for (const iframe of iframes) {
|
||||
try {
|
||||
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
||||
allElements.push(...iframeDoc.getElementsByTagName(baseElement.tagName));
|
||||
} catch (e) {
|
||||
console.warn('Cannot access iframe content:', e);
|
||||
}
|
||||
}
|
||||
|
||||
return allElements.filter(element => {
|
||||
if (element === baseElement) return false;
|
||||
const similarity = calculateClassSimilarity(
|
||||
baseClasses,
|
||||
Array.from(element.classList)
|
||||
);
|
||||
return similarity >= similarityThreshold;
|
||||
});
|
||||
}
|
||||
|
||||
let containers = Array.from(document.querySelectorAll(listSelector));
|
||||
// Main scraping logic with context support
|
||||
let containers = queryElementAll(document, listSelector);
|
||||
containers = Array.from(containers);
|
||||
|
||||
if (containers.length === 0) return [];
|
||||
|
||||
if (limit > 1 && containers.length === 1) {
|
||||
@@ -374,115 +673,157 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
|
||||
const newContainers = similarContainers.filter(container =>
|
||||
!container.matches(listSelector)
|
||||
);
|
||||
|
||||
containers = [...containers, ...newContainers];
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize arrays to store field classifications for each container
|
||||
const containerFields = containers.map(() => ({
|
||||
tableFields: {},
|
||||
nonTableFields: {}
|
||||
tableFields: {},
|
||||
nonTableFields: {}
|
||||
}));
|
||||
|
||||
// Analyze field types for each container
|
||||
// Classify fields
|
||||
containers.forEach((container, containerIndex) => {
|
||||
for (const [label, field] of Object.entries(fields)) {
|
||||
const sampleElement = container.querySelector(field.selector);
|
||||
|
||||
if (sampleElement) {
|
||||
const ancestor = findTableAncestor(sampleElement);
|
||||
if (ancestor) {
|
||||
containerFields[containerIndex].tableFields[label] = {
|
||||
...field,
|
||||
tableContext: ancestor.type,
|
||||
cellIndex: ancestor.type === 'TD' ? getCellIndex(ancestor.element) : -1
|
||||
};
|
||||
} else {
|
||||
containerFields[containerIndex].nonTableFields[label] = field;
|
||||
}
|
||||
for (const [label, field] of Object.entries(fields)) {
|
||||
const sampleElement = queryElement(container, field.selector);
|
||||
|
||||
if (sampleElement) {
|
||||
const ancestor = findTableAncestor(sampleElement);
|
||||
if (ancestor) {
|
||||
containerFields[containerIndex].tableFields[label] = {
|
||||
...field,
|
||||
tableContext: ancestor.type,
|
||||
cellIndex: ancestor.type === 'TD' ? getCellIndex(ancestor.element) : -1
|
||||
};
|
||||
} else {
|
||||
containerFields[containerIndex].nonTableFields[label] = field;
|
||||
}
|
||||
} else {
|
||||
containerFields[containerIndex].nonTableFields[label] = field;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const tableData = [];
|
||||
const nonTableData = [];
|
||||
|
||||
// Process table fields across all containers
|
||||
|
||||
// Process table data with both iframe and shadow DOM support
|
||||
for (let containerIndex = 0; containerIndex < containers.length; containerIndex++) {
|
||||
const container = containers[containerIndex];
|
||||
const { tableFields } = containerFields[containerIndex];
|
||||
|
||||
if (Object.keys(tableFields).length > 0) {
|
||||
const firstField = Object.values(tableFields)[0];
|
||||
const firstElement = container.querySelector(firstField.selector);
|
||||
let tableContext = firstElement;
|
||||
|
||||
while (tableContext && tableContext.tagName !== 'TABLE' && tableContext !== container) {
|
||||
tableContext = tableContext.parentElement;
|
||||
}
|
||||
|
||||
if (tableContext) {
|
||||
const rows = Array.from(tableContext.getElementsByTagName('TR'));
|
||||
const processedRows = filterRowsBasedOnTag(rows, tableFields);
|
||||
|
||||
for (let rowIndex = 0; rowIndex < Math.min(processedRows.length, limit); rowIndex++) {
|
||||
const record = {};
|
||||
const currentRow = processedRows[rowIndex];
|
||||
|
||||
for (const [label, { selector, attribute, cellIndex }] of Object.entries(tableFields)) {
|
||||
let element = null;
|
||||
|
||||
if (cellIndex >= 0) {
|
||||
const td = currentRow.children[cellIndex];
|
||||
if (td) {
|
||||
element = td.querySelector(selector);
|
||||
|
||||
if (!element && selector.split(">").pop().includes('td:nth-child')) {
|
||||
element = td;
|
||||
const firstField = Object.values(tableFields)[0];
|
||||
const firstElement = queryElement(container, firstField.selector);
|
||||
let tableContext = firstElement;
|
||||
|
||||
// Find table context including both iframe and shadow DOM
|
||||
while (tableContext && tableContext.tagName !== 'TABLE' && tableContext !== container) {
|
||||
if (tableContext.getRootNode() instanceof ShadowRoot) {
|
||||
tableContext = tableContext.getRootNode().host;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tableContext.tagName === 'IFRAME') {
|
||||
try {
|
||||
tableContext = tableContext.contentDocument.body;
|
||||
} catch (e) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
tableContext = tableContext.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
if (!element) {
|
||||
const tagOnlySelector = selector.split('.')[0];
|
||||
element = td.querySelector(tagOnlySelector);
|
||||
if (tableContext) {
|
||||
// Get rows from all contexts
|
||||
const rows = [];
|
||||
|
||||
// Get rows from regular DOM
|
||||
rows.push(...tableContext.getElementsByTagName('TR'));
|
||||
|
||||
// Get rows from shadow DOM
|
||||
if (tableContext.shadowRoot) {
|
||||
rows.push(...tableContext.shadowRoot.getElementsByTagName('TR'));
|
||||
}
|
||||
|
||||
// Get rows from iframes
|
||||
if (tableContext.tagName === 'IFRAME') {
|
||||
try {
|
||||
const iframeDoc = tableContext.contentDocument || tableContext.contentWindow.document;
|
||||
rows.push(...iframeDoc.getElementsByTagName('TR'));
|
||||
} catch (e) {
|
||||
console.warn('Cannot access iframe rows:', e);
|
||||
}
|
||||
}
|
||||
|
||||
const processedRows = filterRowsBasedOnTag(rows, tableFields);
|
||||
|
||||
for (let rowIndex = 0; rowIndex < Math.min(processedRows.length, limit); rowIndex++) {
|
||||
const record = {};
|
||||
const currentRow = processedRows[rowIndex];
|
||||
|
||||
if (!element) {
|
||||
let currentElement = td;
|
||||
while (currentElement && currentElement.children.length > 0) {
|
||||
let foundContentChild = false;
|
||||
for (const child of currentElement.children) {
|
||||
if (extractValue(child, attribute)) {
|
||||
currentElement = child;
|
||||
foundContentChild = true;
|
||||
break;
|
||||
for (const [label, { selector, attribute, cellIndex }] of Object.entries(tableFields)) {
|
||||
let element = null;
|
||||
|
||||
if (cellIndex >= 0) {
|
||||
// Get TD element considering both contexts
|
||||
let td = currentRow.children[cellIndex];
|
||||
|
||||
// Check shadow DOM for td
|
||||
if (!td && currentRow.shadowRoot) {
|
||||
const shadowCells = currentRow.shadowRoot.children;
|
||||
if (shadowCells && shadowCells.length > cellIndex) {
|
||||
td = shadowCells[cellIndex];
|
||||
}
|
||||
}
|
||||
if (!foundContentChild) break;
|
||||
}
|
||||
element = currentElement;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
element = currentRow.querySelector(selector);
|
||||
}
|
||||
|
||||
if (element) {
|
||||
record[label] = extractValue(element, attribute);
|
||||
}
|
||||
}
|
||||
|
||||
if (td) {
|
||||
element = queryElement(td, selector);
|
||||
|
||||
if (!element && selector.split(/(?:>>|:>>)/).pop().includes('td:nth-child')) {
|
||||
element = td;
|
||||
}
|
||||
|
||||
if (Object.keys(record).length > 0) {
|
||||
tableData.push(record);
|
||||
}
|
||||
if (!element) {
|
||||
const tagOnlySelector = selector.split('.')[0];
|
||||
element = queryElement(td, tagOnlySelector);
|
||||
}
|
||||
|
||||
if (!element) {
|
||||
let currentElement = td;
|
||||
while (currentElement && currentElement.children.length > 0) {
|
||||
let foundContentChild = false;
|
||||
for (const child of currentElement.children) {
|
||||
if (extractValue(child, attribute)) {
|
||||
currentElement = child;
|
||||
foundContentChild = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!foundContentChild) break;
|
||||
}
|
||||
element = currentElement;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
element = queryElement(currentRow, selector);
|
||||
}
|
||||
|
||||
if (element) {
|
||||
record[label] = extractValue(element, attribute);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(record).length > 0) {
|
||||
tableData.push(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process non-table fields across all containers
|
||||
|
||||
// Process non-table data with both contexts support
|
||||
for (let containerIndex = 0; containerIndex < containers.length; containerIndex++) {
|
||||
if (nonTableData.length >= limit) break;
|
||||
|
||||
@@ -490,26 +831,28 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
|
||||
const { nonTableFields } = containerFields[containerIndex];
|
||||
|
||||
if (Object.keys(nonTableFields).length > 0) {
|
||||
const record = {};
|
||||
const record = {};
|
||||
|
||||
for (const [label, { selector, attribute }] of Object.entries(nonTableFields)) {
|
||||
const element = container.querySelector(selector);
|
||||
|
||||
if (element) {
|
||||
record[label] = extractValue(element, attribute);
|
||||
}
|
||||
}
|
||||
for (const [label, { selector, attribute }] of Object.entries(nonTableFields)) {
|
||||
// Get the last part of the selector after any context delimiter
|
||||
const relativeSelector = selector.split(/(?:>>|:>>)/).slice(-1)[0];
|
||||
const element = queryElement(container, relativeSelector);
|
||||
|
||||
if (Object.keys(record).length > 0) {
|
||||
nonTableData.push(record);
|
||||
}
|
||||
}
|
||||
if (element) {
|
||||
record[label] = extractValue(element, attribute);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(record).length > 0) {
|
||||
nonTableData.push(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge and limit the results
|
||||
const scrapedData = [...tableData, ...nonTableData];
|
||||
return scrapedData;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets all children of the elements matching the listSelector,
|
||||
|
||||
@@ -403,7 +403,7 @@ export default class Interpreter extends EventEmitter {
|
||||
await this.options.serializableCallback(scrapeResults);
|
||||
},
|
||||
|
||||
scrapeSchema: async (schema: Record<string, { selector: string; tag: string, attribute: string; }>) => {
|
||||
scrapeSchema: async (schema: Record<string, { selector: string; tag: string, attribute: string; shadow: string}>) => {
|
||||
await this.ensureScriptsLoaded(page);
|
||||
|
||||
const scrapeResult = await page.evaluate((schemaObj) => window.scrapeSchema(schemaObj), schema);
|
||||
@@ -663,11 +663,42 @@ export default class Interpreter extends EventEmitter {
|
||||
if (isApplicable) {
|
||||
return actionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private removeShadowSelectors(workflow: Workflow) {
|
||||
for (let actionId = workflow.length - 1; actionId >= 0; actionId--) {
|
||||
const step = workflow[actionId];
|
||||
|
||||
// Check if step has where and selectors
|
||||
if (step.where && Array.isArray(step.where.selectors)) {
|
||||
// Filter out selectors that contain ">>"
|
||||
step.where.selectors = step.where.selectors.filter(selector => !selector.includes('>>'));
|
||||
}
|
||||
}
|
||||
|
||||
return workflow;
|
||||
}
|
||||
|
||||
private removeSpecialSelectors(workflow: Workflow) {
|
||||
for (let actionId = workflow.length - 1; actionId >= 0; actionId--) {
|
||||
const step = workflow[actionId];
|
||||
|
||||
if (step.where && Array.isArray(step.where.selectors)) {
|
||||
// Filter out if selector has EITHER ":>>" OR ">>"
|
||||
step.where.selectors = step.where.selectors.filter(selector =>
|
||||
!(selector.includes(':>>') || selector.includes('>>'))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return workflow;
|
||||
}
|
||||
|
||||
private async runLoop(p: Page, workflow: Workflow) {
|
||||
const workflowCopy: Workflow = JSON.parse(JSON.stringify(workflow));
|
||||
let workflowCopy: Workflow = JSON.parse(JSON.stringify(workflow));
|
||||
|
||||
workflowCopy = this.removeSpecialSelectors(workflowCopy);
|
||||
|
||||
// apply ad-blocker to the current page
|
||||
try {
|
||||
|
||||
@@ -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",
|
||||
|
||||
181
perf/performance.ts
Normal file
181
perf/performance.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
// Frontend Performance Monitoring
|
||||
export class FrontendPerformanceMonitor {
|
||||
private metrics: {
|
||||
fps: number[];
|
||||
memoryUsage: MemoryInfo[];
|
||||
renderTime: number[];
|
||||
eventLatency: number[];
|
||||
};
|
||||
private lastFrameTime: number;
|
||||
private frameCount: number;
|
||||
|
||||
constructor() {
|
||||
this.metrics = {
|
||||
fps: [],
|
||||
memoryUsage: [],
|
||||
renderTime: [],
|
||||
eventLatency: [],
|
||||
};
|
||||
this.lastFrameTime = performance.now();
|
||||
this.frameCount = 0;
|
||||
|
||||
// Start monitoring
|
||||
this.startMonitoring();
|
||||
}
|
||||
|
||||
private startMonitoring(): void {
|
||||
// Monitor FPS
|
||||
const measureFPS = () => {
|
||||
const currentTime = performance.now();
|
||||
const elapsed = currentTime - this.lastFrameTime;
|
||||
this.frameCount++;
|
||||
|
||||
if (elapsed >= 1000) { // Calculate FPS every second
|
||||
const fps = Math.round((this.frameCount * 1000) / elapsed);
|
||||
this.metrics.fps.push(fps);
|
||||
this.frameCount = 0;
|
||||
this.lastFrameTime = currentTime;
|
||||
}
|
||||
requestAnimationFrame(measureFPS);
|
||||
};
|
||||
requestAnimationFrame(measureFPS);
|
||||
|
||||
// Monitor Memory Usage
|
||||
if (window.performance && (performance as any).memory) {
|
||||
setInterval(() => {
|
||||
const memory = (performance as any).memory;
|
||||
this.metrics.memoryUsage.push({
|
||||
usedJSHeapSize: memory.usedJSHeapSize,
|
||||
totalJSHeapSize: memory.totalJSHeapSize,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// Monitor Canvas Render Time
|
||||
public measureRenderTime(renderFunction: () => void): void {
|
||||
const startTime = performance.now();
|
||||
renderFunction();
|
||||
const endTime = performance.now();
|
||||
this.metrics.renderTime.push(endTime - startTime);
|
||||
}
|
||||
|
||||
// Monitor Event Latency
|
||||
public measureEventLatency(event: MouseEvent | KeyboardEvent): void {
|
||||
const latency = performance.now() - event.timeStamp;
|
||||
this.metrics.eventLatency.push(latency);
|
||||
}
|
||||
|
||||
// Get Performance Report
|
||||
public getPerformanceReport(): PerformanceReport {
|
||||
return {
|
||||
averageFPS: this.calculateAverage(this.metrics.fps),
|
||||
averageRenderTime: this.calculateAverage(this.metrics.renderTime),
|
||||
averageEventLatency: this.calculateAverage(this.metrics.eventLatency),
|
||||
memoryTrend: this.getMemoryTrend(),
|
||||
lastMemoryUsage: this.metrics.memoryUsage[this.metrics.memoryUsage.length - 1]
|
||||
};
|
||||
}
|
||||
|
||||
private calculateAverage(array: number[]): number {
|
||||
return array.length ? array.reduce((a, b) => a + b) / array.length : 0;
|
||||
}
|
||||
|
||||
private getMemoryTrend(): MemoryTrend {
|
||||
if (this.metrics.memoryUsage.length < 2) return 'stable';
|
||||
const latest = this.metrics.memoryUsage[this.metrics.memoryUsage.length - 1];
|
||||
const previous = this.metrics.memoryUsage[this.metrics.memoryUsage.length - 2];
|
||||
const change = latest.usedJSHeapSize - previous.usedJSHeapSize;
|
||||
if (change > 1000000) return 'increasing'; // 1MB threshold
|
||||
if (change < -1000000) return 'decreasing';
|
||||
return 'stable';
|
||||
}
|
||||
}
|
||||
|
||||
// Backend Performance Monitoring
|
||||
export class BackendPerformanceMonitor {
|
||||
private metrics: {
|
||||
screenshotTimes: number[];
|
||||
emitTimes: number[];
|
||||
memoryUsage: NodeJS.MemoryUsage[];
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.metrics = {
|
||||
screenshotTimes: [],
|
||||
emitTimes: [],
|
||||
memoryUsage: []
|
||||
};
|
||||
this.startMonitoring();
|
||||
}
|
||||
|
||||
private startMonitoring(): void {
|
||||
// Monitor Memory Usage
|
||||
setInterval(() => {
|
||||
this.metrics.memoryUsage.push(process.memoryUsage());
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
public async measureScreenshotPerformance(
|
||||
makeScreenshot: () => Promise<void>
|
||||
): Promise<void> {
|
||||
const startTime = process.hrtime();
|
||||
await makeScreenshot();
|
||||
const [seconds, nanoseconds] = process.hrtime(startTime);
|
||||
this.metrics.screenshotTimes.push(seconds * 1000 + nanoseconds / 1000000);
|
||||
}
|
||||
|
||||
public measureEmitPerformance(emitFunction: () => void): void {
|
||||
const startTime = process.hrtime();
|
||||
emitFunction();
|
||||
const [seconds, nanoseconds] = process.hrtime(startTime);
|
||||
this.metrics.emitTimes.push(seconds * 1000 + nanoseconds / 1000000);
|
||||
}
|
||||
|
||||
public getPerformanceReport(): BackendPerformanceReport {
|
||||
return {
|
||||
averageScreenshotTime: this.calculateAverage(this.metrics.screenshotTimes),
|
||||
averageEmitTime: this.calculateAverage(this.metrics.emitTimes),
|
||||
currentMemoryUsage: this.metrics.memoryUsage[this.metrics.memoryUsage.length - 1],
|
||||
memoryTrend: this.getMemoryTrend()
|
||||
};
|
||||
}
|
||||
|
||||
private calculateAverage(array: number[]): number {
|
||||
return array.length ? array.reduce((a, b) => a + b) / array.length : 0;
|
||||
}
|
||||
|
||||
private getMemoryTrend(): MemoryTrend {
|
||||
if (this.metrics.memoryUsage.length < 2) return 'stable';
|
||||
const latest = this.metrics.memoryUsage[this.metrics.memoryUsage.length - 1];
|
||||
const previous = this.metrics.memoryUsage[this.metrics.memoryUsage.length - 2];
|
||||
const change = latest.heapUsed - previous.heapUsed;
|
||||
if (change > 1000000) return 'increasing';
|
||||
if (change < -1000000) return 'decreasing';
|
||||
return 'stable';
|
||||
}
|
||||
}
|
||||
|
||||
interface MemoryInfo {
|
||||
usedJSHeapSize: number;
|
||||
totalJSHeapSize: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
type MemoryTrend = 'increasing' | 'decreasing' | 'stable';
|
||||
|
||||
interface PerformanceReport {
|
||||
averageFPS: number;
|
||||
averageRenderTime: number;
|
||||
averageEventLatency: number;
|
||||
memoryTrend: MemoryTrend;
|
||||
lastMemoryUsage: MemoryInfo;
|
||||
}
|
||||
|
||||
interface BackendPerformanceReport {
|
||||
averageScreenshotTime: number;
|
||||
averageEmitTime: number;
|
||||
currentMemoryUsage: NodeJS.MemoryUsage;
|
||||
memoryTrend: MemoryTrend;
|
||||
}
|
||||
@@ -158,11 +158,13 @@
|
||||
"confirm": "Bestätigen",
|
||||
"discard": "Verwerfen",
|
||||
"confirm_capture": "Erfassung bestätigen",
|
||||
"confirm_pagination": "Paginierung bestätigen",
|
||||
"confirm_limit": "Limit bestätigen",
|
||||
"confirm_pagination": "Bestätigen",
|
||||
"confirm_limit": "Bestätigen",
|
||||
"finish_capture": "Erfassung abschließen",
|
||||
"back": "Zurück",
|
||||
"finish": "Fertig",
|
||||
"cancel": "Abbrechen"
|
||||
"cancel": "Abbrechen",
|
||||
"delete": "Löschen"
|
||||
},
|
||||
"screenshot": {
|
||||
"capture_fullpage": "Vollständige Seite erfassen",
|
||||
|
||||
@@ -159,11 +159,13 @@
|
||||
"confirm": "Confirm",
|
||||
"discard": "Discard",
|
||||
"confirm_capture": "Confirm Capture",
|
||||
"confirm_pagination": "Confirm Pagination",
|
||||
"confirm_limit": "Confirm Limit",
|
||||
"confirm_pagination": "Confirm",
|
||||
"confirm_limit": "Confirm",
|
||||
"finish_capture": "Finish Capture",
|
||||
"back": "Back",
|
||||
"finish": "Finish",
|
||||
"cancel": "Cancel"
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"screenshot": {
|
||||
"capture_fullpage": "Capture Fullpage",
|
||||
|
||||
@@ -159,11 +159,13 @@
|
||||
"confirm": "Confirmar",
|
||||
"discard": "Descartar",
|
||||
"confirm_capture": "Confirmar Captura",
|
||||
"confirm_pagination": "Confirmar Paginación",
|
||||
"confirm_limit": "Confirmar Límite",
|
||||
"confirm_pagination": "Confirmar",
|
||||
"confirm_limit": "Confirmar",
|
||||
"finish_capture": "Finalizar Captura",
|
||||
"back": "Atrás",
|
||||
"finish": "Finalizar",
|
||||
"cancel": "Cancelar"
|
||||
"cancel": "Cancelar",
|
||||
"delete": "Eliminar"
|
||||
},
|
||||
"screenshot": {
|
||||
"capture_fullpage": "Capturar Página Completa",
|
||||
|
||||
@@ -159,11 +159,13 @@
|
||||
"confirm": "確認",
|
||||
"discard": "破棄",
|
||||
"confirm_capture": "取得を確認",
|
||||
"confirm_pagination": "ページネーションを確認",
|
||||
"confirm_limit": "制限を確認",
|
||||
"confirm_pagination": "確認",
|
||||
"confirm_limit": "確認",
|
||||
"finish_capture": "取得を完了",
|
||||
"back": "戻る",
|
||||
"finish": "完了",
|
||||
"cancel": "キャンセル"
|
||||
"cancel": "キャンセル",
|
||||
"delete": "削除"
|
||||
},
|
||||
"screenshot": {
|
||||
"capture_fullpage": "フルページを取得",
|
||||
|
||||
@@ -159,11 +159,13 @@
|
||||
"confirm": "确认",
|
||||
"discard": "放弃",
|
||||
"confirm_capture": "确认捕获",
|
||||
"confirm_pagination": "确认分页",
|
||||
"confirm_limit": "确认限制",
|
||||
"confirm_pagination": "确认",
|
||||
"confirm_limit": "确认",
|
||||
"finish_capture": "完成捕获",
|
||||
"back": "返回",
|
||||
"finish": "完成",
|
||||
"cancel": "取消"
|
||||
"cancel": "取消",
|
||||
"delete": "删除"
|
||||
},
|
||||
"screenshot": {
|
||||
"capture_fullpage": "捕获整页",
|
||||
|
||||
@@ -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<void> {
|
||||
this.screenshotQueue = [];
|
||||
this.isProcessingScreenshot = false;
|
||||
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
|
||||
// Reset CDP session if needed
|
||||
if (this.client) {
|
||||
try {
|
||||
await this.stopScreencast();
|
||||
this.client = null;
|
||||
if (this.currentPage) {
|
||||
this.client = await this.currentPage.context().newCDPSession(this.currentPage);
|
||||
await this.startScreencast();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error resetting CDP session:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes URLs to prevent navigation loops while maintaining consistent format
|
||||
*/
|
||||
@@ -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<void>}
|
||||
*/
|
||||
public switchOff = async (): Promise<void> => {
|
||||
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<void> {
|
||||
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<Buffer> {
|
||||
try {
|
||||
return await sharp(screenshot)
|
||||
.jpeg({
|
||||
quality: Math.round(SCREENCAST_CONFIG.compressionQuality * 100),
|
||||
progressive: true
|
||||
})
|
||||
.resize({
|
||||
width: SCREENCAST_CONFIG.maxWidth,
|
||||
height: SCREENCAST_CONFIG.maxHeight,
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true
|
||||
})
|
||||
.toBuffer();
|
||||
} catch (error) {
|
||||
logger.error('Screenshot optimization failed:', error);
|
||||
return screenshot;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes and emits a single screenshot to the client side.
|
||||
@@ -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<void>}
|
||||
*/
|
||||
private startScreencast = async (): Promise<void> => {
|
||||
private async startScreencast(): Promise<void> {
|
||||
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<void>}
|
||||
*/
|
||||
private stopScreencast = async (): Promise<void> => {
|
||||
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<void> {
|
||||
if (!this.client) {
|
||||
logger.error('Client is not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.client.send('Page.stopScreencast');
|
||||
this.screenshotQueue = [];
|
||||
this.isProcessingScreenshot = false;
|
||||
logger.info('Screencast stopped successfully');
|
||||
} catch (error) {
|
||||
logger.error('Failed to stop screencast:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper for emitting the screenshot of browser's active page through websocket.
|
||||
* @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<void> => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -129,6 +129,17 @@ export interface BaseActionInfo {
|
||||
hasOnlyText: boolean;
|
||||
}
|
||||
|
||||
|
||||
interface IframeSelector {
|
||||
full: string;
|
||||
isIframe: boolean;
|
||||
}
|
||||
|
||||
interface ShadowSelector {
|
||||
full: string;
|
||||
mode: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds all the possible css selectors that has been found for an element.
|
||||
* @category Types
|
||||
@@ -143,6 +154,8 @@ export interface Selectors {
|
||||
hrefSelector: string|null;
|
||||
accessibilitySelector: string|null;
|
||||
formSelector: string|null;
|
||||
iframeSelector: IframeSelector|null;
|
||||
shadowSelector: ShadowSelector|null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,7 +169,7 @@ export interface BaseAction extends BaseActionInfo{
|
||||
associatedActions: ActionType[];
|
||||
inputType: string | undefined;
|
||||
value: string | undefined;
|
||||
selectors: { [key: string]: string | null };
|
||||
selectors: Selectors;
|
||||
timestamp: number;
|
||||
isPassword: boolean;
|
||||
/**
|
||||
|
||||
@@ -730,15 +730,26 @@ export class WorkflowGenerator {
|
||||
const displaySelector = await this.generateSelector(page, coordinates, ActionType.Click);
|
||||
const elementInfo = await getElementInformation(page, coordinates, this.listSelector, this.getList);
|
||||
if (rect) {
|
||||
const highlighterData = {
|
||||
rect,
|
||||
selector: displaySelector,
|
||||
elementInfo,
|
||||
// Include shadow DOM specific information
|
||||
shadowInfo: elementInfo?.isShadowRoot ? {
|
||||
mode: elementInfo.shadowRootMode,
|
||||
content: elementInfo.shadowRootContent
|
||||
} : null
|
||||
};
|
||||
|
||||
if (this.getList === true) {
|
||||
if (this.listSelector !== '') {
|
||||
const childSelectors = await getChildSelectors(page, this.listSelector || '');
|
||||
this.socket.emit('highlighter', { rect, selector: displaySelector, elementInfo, childSelectors })
|
||||
this.socket.emit('highlighter', { ...highlighterData, childSelectors })
|
||||
} else {
|
||||
this.socket.emit('highlighter', { rect, selector: displaySelector, elementInfo });
|
||||
this.socket.emit('highlighter', { ...highlighterData });
|
||||
}
|
||||
} else {
|
||||
this.socket.emit('highlighter', { rect, selector: displaySelector, elementInfo });
|
||||
this.socket.emit('highlighter', { ...highlighterData });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,16 @@ export const getBestSelectorForAction = (action: Action) => {
|
||||
case ActionType.Hover:
|
||||
case ActionType.DragAndDrop: {
|
||||
const selectors = action.selectors;
|
||||
|
||||
|
||||
if (selectors?.iframeSelector?.full) {
|
||||
return selectors.iframeSelector.full;
|
||||
}
|
||||
|
||||
if (selectors?.shadowSelector?.full) {
|
||||
return selectors.shadowSelector.full;
|
||||
}
|
||||
|
||||
// less than 25 characters, and element only has text inside
|
||||
const textSelector =
|
||||
selectors?.text?.length != null &&
|
||||
@@ -75,6 +85,11 @@ export const getBestSelectorForAction = (action: Action) => {
|
||||
case ActionType.Input:
|
||||
case ActionType.Keydown: {
|
||||
const selectors = action.selectors;
|
||||
|
||||
if (selectors?.shadowSelector?.full) {
|
||||
return selectors.shadowSelector.full;
|
||||
}
|
||||
|
||||
return (
|
||||
selectors.testIdSelector ??
|
||||
selectors?.id ??
|
||||
|
||||
23
src/App.tsx
23
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 (
|
||||
<ThemeProvider theme={theme}>
|
||||
<ThemeModeProvider>
|
||||
<GlobalInfoProvider>
|
||||
<Routes>
|
||||
<Route path="/*" element={<PageWrapper />} />
|
||||
</Routes>
|
||||
</GlobalInfoProvider>
|
||||
</ThemeModeProvider>
|
||||
|
||||
// <ThemeProvider theme={theme}>
|
||||
|
||||
<GlobalInfoProvider>
|
||||
<Routes>
|
||||
<Route path="/*" element={<PageWrapper />} />
|
||||
</Routes>
|
||||
</GlobalInfoProvider>
|
||||
// <GlobalInfoProvider>
|
||||
// <Routes>
|
||||
// <Route path="/*" element={<PageWrapper />} />
|
||||
// </Routes>
|
||||
// </GlobalInfoProvider>
|
||||
|
||||
</ThemeProvider>
|
||||
// </ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
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";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const getStoredRecordings = async (): Promise<string[] | null> => {
|
||||
try {
|
||||
const response = await axios.get(`${apiUrl}/storage/recordings`);
|
||||
@@ -82,11 +77,7 @@ export const getStoredRecording = async (id: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const checkRunsForRecording = async (id: string): Promise<boolean> => {
|
||||
|
||||
|
||||
try {
|
||||
const response = await axios.get(`${apiUrl}/storage/recordings/${id}/runs`);
|
||||
|
||||
@@ -99,32 +90,26 @@ export const checkRunsForRecording = async (id: string): Promise<boolean> => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const deleteRecordingFromStorage = async (id: string): Promise<boolean> => {
|
||||
|
||||
const hasRuns = await checkRunsForRecording(id);
|
||||
|
||||
|
||||
if (hasRuns) {
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const response = await axios.delete(`${apiUrl}/storage/recordings/${id}`);
|
||||
if (response.status === 200) {
|
||||
|
||||
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(`Couldn't delete stored recording ${id}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log(error);
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
export const deleteRunFromStorage = async (id: string): Promise<boolean> => {
|
||||
@@ -159,7 +144,7 @@ export const createRunForStoredRecording = async (id: string, settings: RunSetti
|
||||
try {
|
||||
const response = await axios.put(
|
||||
`${apiUrl}/storage/runs/${id}`,
|
||||
{ ...settings });
|
||||
{ ...settings });
|
||||
if (response.status === 200) {
|
||||
return response.data;
|
||||
} else {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { emptyWorkflow } from "../shared/constants";
|
||||
import { default as axios, AxiosResponse } from "axios";
|
||||
import { apiUrl } from "../apiConfig";
|
||||
|
||||
export const getActiveWorkflow = async(id: string) : Promise<WorkflowFile> => {
|
||||
export const getActiveWorkflow = async (id: string): Promise<WorkflowFile> => {
|
||||
try {
|
||||
const response = await axios.get(`${apiUrl}/workflow/${id}`)
|
||||
if (response.status === 200) {
|
||||
@@ -11,13 +11,13 @@ export const getActiveWorkflow = async(id: string) : Promise<WorkflowFile> => {
|
||||
} else {
|
||||
throw new Error('Something went wrong when fetching a recorded workflow');
|
||||
}
|
||||
} catch(error: any) {
|
||||
} catch (error: any) {
|
||||
console.log(error);
|
||||
return emptyWorkflow;
|
||||
}
|
||||
};
|
||||
|
||||
export const getParamsOfActiveWorkflow = async(id: string) : Promise<string[]|null> => {
|
||||
export const getParamsOfActiveWorkflow = async (id: string): Promise<string[] | null> => {
|
||||
try {
|
||||
const response = await axios.get(`${apiUrl}/workflow/params/${id}`)
|
||||
if (response.status === 200) {
|
||||
@@ -25,15 +25,15 @@ export const getParamsOfActiveWorkflow = async(id: string) : Promise<string[]|nu
|
||||
} else {
|
||||
throw new Error('Something went wrong when fetching the parameters of the recorded workflow');
|
||||
}
|
||||
} catch(error: any) {
|
||||
} catch (error: any) {
|
||||
console.log(error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const deletePair = async(index: number): Promise<WorkflowFile> => {
|
||||
export const deletePair = async (index: number): Promise<WorkflowFile> => {
|
||||
try {
|
||||
const response = await axios.delete(`${apiUrl}/workflow/pair/${index}`);
|
||||
const response = await axios.delete(`${apiUrl}/workflow/pair/${index}`);
|
||||
if (response.status === 200) {
|
||||
return response.data;
|
||||
} else {
|
||||
@@ -45,11 +45,11 @@ export const deletePair = async(index: number): Promise<WorkflowFile> => {
|
||||
}
|
||||
};
|
||||
|
||||
export const AddPair = async(index: number, pair: WhereWhatPair): Promise<WorkflowFile> => {
|
||||
export const AddPair = async (index: number, pair: WhereWhatPair): Promise<WorkflowFile> => {
|
||||
try {
|
||||
const response = await axios.post(`${apiUrl}/workflow/pair/${index}`, {
|
||||
pair,
|
||||
}, {headers: {'Content-Type': 'application/json'}});
|
||||
}, { headers: { 'Content-Type': 'application/json' } });
|
||||
if (response.status === 200) {
|
||||
return response.data;
|
||||
} else {
|
||||
@@ -61,11 +61,11 @@ export const AddPair = async(index: number, pair: WhereWhatPair): Promise<Workfl
|
||||
}
|
||||
};
|
||||
|
||||
export const UpdatePair = async(index: number, pair: WhereWhatPair): Promise<WorkflowFile> => {
|
||||
export const UpdatePair = async (index: number, pair: WhereWhatPair): Promise<WorkflowFile> => {
|
||||
try {
|
||||
const response = await axios.put(`${apiUrl}/workflow/pair/${index}`, {
|
||||
pair,
|
||||
}, {headers: {'Content-Type': 'application/json'}});
|
||||
}, { headers: { 'Content-Type': 'application/json' } });
|
||||
if (response.status === 200) {
|
||||
return response.data;
|
||||
} else {
|
||||
|
||||
@@ -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<CustomBoxContainerProps>`
|
||||
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<CustomBoxContainerProps>`
|
||||
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 = () => {
|
||||
<Checkbox
|
||||
checked={index < currentStageIndex}
|
||||
disabled
|
||||
sx={{
|
||||
color: isDarkMode ? 'white' : 'default',
|
||||
'&.Mui-checked': {
|
||||
color: isDarkMode ? '#90caf9' : '#1976d2',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={<Typography variant="body2" gutterBottom>{text}</Typography>}
|
||||
label={
|
||||
<Typography variant="body2" gutterBottom color={isDarkMode ? 'white' : 'textPrimary'}>
|
||||
{text}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
@@ -112,9 +128,9 @@ const ActionDescriptionBox = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<CustomBoxContainer>
|
||||
<Logo src={MaxunLogo} alt={t('common.maxun_logo')} />
|
||||
<Triangle />
|
||||
<CustomBoxContainer isDarkMode={isDarkMode}>
|
||||
<Logo src={MaxunLogo} alt="Maxun Logo" />
|
||||
<Triangle isDarkMode={isDarkMode} />
|
||||
<Content>
|
||||
{renderActionDescription()}
|
||||
</Content>
|
||||
@@ -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 <Settings.ScreenshotSettings ref={settingsRef} />;
|
||||
case 'scroll':
|
||||
return <Settings.ScrollSettings ref={settingsRef} />;
|
||||
case 'scrape':
|
||||
return <Settings.ScrapeSettings ref={settingsRef} />;
|
||||
case 'scrape':
|
||||
return <Settings.ScrapeSettings ref={settingsRef} />;
|
||||
case 'scrapeSchema':
|
||||
return <Settings.ScrapeSchemaSettings ref={settingsRef} />;
|
||||
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 (
|
||||
<div>
|
||||
{/* <ActionDescription>Action settings:</ActionDescription> */}
|
||||
<ActionSettingsWrapper action={action}>
|
||||
<ActionSettingsWrapper action={action} darkMode={darkMode}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<DisplaySettings />
|
||||
<Button
|
||||
@@ -64,10 +60,13 @@ export const ActionSettings = ({ action }: ActionSettingsProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const ActionSettingsWrapper = styled.div<{ action: string }>`
|
||||
// Ensure that the Wrapper accepts the darkMode prop for styling adjustments.
|
||||
const ActionSettingsWrapper = styled.div<{ action: string; darkMode: boolean }>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: ${({ action }) => action === 'script' ? 'stretch' : 'center'};;
|
||||
align-items: ${({ action }) => (action === 'script' ? 'stretch' : 'center')};
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
background-color: ${({ darkMode }) => (darkMode ? '#1E1E1E' : 'white')};
|
||||
color: ${({ darkMode }) => (darkMode ? 'white' : 'black')};
|
||||
`;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { forwardRef, useImperativeHandle } from 'react';
|
||||
import { Stack, TextField } from "@mui/material";
|
||||
import { WarningText } from '../../atoms/texts';
|
||||
import { WarningText } from '../../ui/texts';
|
||||
import InfoIcon from "@mui/icons-material/Info";
|
||||
|
||||
export const ScrapeSettings = forwardRef((props, ref) => {
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { forwardRef, useCallback, useImperativeHandle, useRef } from 'react';
|
||||
import { WarningText } from "../../atoms/texts";
|
||||
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
|
||||
import { WarningText } from "../../ui/texts";
|
||||
import InfoIcon from "@mui/icons-material/Info";
|
||||
import { KeyValueForm } from "../KeyValueForm";
|
||||
import { KeyValueForm } from "../../recorder/KeyValueForm";
|
||||
|
||||
export const ScrapeSchemaSettings = forwardRef((props, ref) => {
|
||||
const keyValueFormRef = useRef<{ getObject: () => object }>(null);
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { forwardRef, useImperativeHandle } from 'react';
|
||||
import { InputLabel, MenuItem, TextField, Select, FormControl } from "@mui/material";
|
||||
import { MenuItem, TextField } from "@mui/material";
|
||||
import { ScreenshotSettings as Settings } from "../../../shared/types";
|
||||
import styled from "styled-components";
|
||||
import { SelectChangeEvent } from "@mui/material/Select/Select";
|
||||
import { Dropdown } from "../../atoms/DropdownMui";
|
||||
import { Dropdown } from "../../ui/DropdownMui";
|
||||
|
||||
export const ScreenshotSettings = forwardRef((props, ref) => {
|
||||
const [settings, setSettings] = React.useState<Settings>({});
|
||||
@@ -124,7 +124,11 @@ const ApiKeyManager = () => {
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>{apiKeyName}</TableCell>
|
||||
<TableCell>{showKey ? `${apiKey?.substring(0, 10)}...` : '***************'}</TableCell>
|
||||
<TableCell>
|
||||
<Box sx={{ fontFamily: 'monospace', width: '10ch' }}>
|
||||
{showKey ? `${apiKey?.substring(0, 10)}...` : '**********'}
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip title={t('apikey.actions.copy')}>
|
||||
<IconButton onClick={copyToClipboard}>
|
||||
@@ -1,17 +1,13 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
import BrowserNavBar from "../molecules/BrowserNavBar";
|
||||
import BrowserNavBar from "./BrowserNavBar";
|
||||
import { BrowserWindow } from "./BrowserWindow";
|
||||
import { useBrowserDimensionsStore } from "../../context/browserDimensions";
|
||||
import { BrowserTabs } from "../molecules/BrowserTabs";
|
||||
import { BrowserTabs } from "./BrowserTabs";
|
||||
import { useSocketStore } from "../../context/socket";
|
||||
import {
|
||||
getCurrentTabs,
|
||||
getCurrentUrl,
|
||||
interpretCurrentRecording,
|
||||
} from "../../api/recording";
|
||||
import { Box } from "@mui/material";
|
||||
import { InterpretationLog } from "../molecules/InterpretationLog";
|
||||
|
||||
// TODO: Tab !show currentUrl after recordingUrl global state
|
||||
export const BrowserContent = () => {
|
||||
@@ -152,6 +148,7 @@ export const BrowserContent = () => {
|
||||
// todo: use width from browser dimension once fixed
|
||||
browserWidth={900}
|
||||
handleUrlChanged={handleUrlChanged}
|
||||
|
||||
/>
|
||||
<BrowserWindow />
|
||||
</div>
|
||||
@@ -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<NavBarProps> = ({
|
||||
browserWidth,
|
||||
handleUrlChanged,
|
||||
}) => {
|
||||
const isDarkMode = useThemeMode().darkMode;
|
||||
|
||||
const { socket } = useSocketStore();
|
||||
const { recordingUrl, setRecordingUrl } = useGlobalInfoStore();
|
||||
@@ -67,7 +75,7 @@ const BrowserNavBar: FC<NavBarProps> = ({
|
||||
socket.off('urlChanged', handleCurrentUrlChange);
|
||||
}
|
||||
}
|
||||
}, [socket, handleCurrentUrlChange])
|
||||
}, [socket, handleCurrentUrlChange]);
|
||||
|
||||
const addAddress = (address: string) => {
|
||||
if (socket) {
|
||||
@@ -78,38 +86,41 @@ const BrowserNavBar: FC<NavBarProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledNavBar browserWidth={900}>
|
||||
<NavBarButton
|
||||
<StyledNavBar browserWidth={browserWidth} isDarkMode={isDarkMode}>
|
||||
<IconButton
|
||||
type="button"
|
||||
onClick={() => {
|
||||
socket?.emit('input:back');
|
||||
}}
|
||||
disabled={false}
|
||||
mode={isDarkMode ? 'dark' : 'light'}
|
||||
>
|
||||
<ArrowBackIcon />
|
||||
</NavBarButton>
|
||||
</IconButton>
|
||||
|
||||
<NavBarButton
|
||||
<IconButton
|
||||
type="button"
|
||||
onClick={() => {
|
||||
socket?.emit('input:forward');
|
||||
}}
|
||||
disabled={false}
|
||||
mode={isDarkMode ? 'dark' : 'light'}
|
||||
>
|
||||
<ArrowForwardIcon />
|
||||
</NavBarButton>
|
||||
</IconButton>
|
||||
|
||||
<NavBarButton
|
||||
<IconButton
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (socket) {
|
||||
handleRefresh()
|
||||
handleRefresh();
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
mode={isDarkMode ? 'dark' : 'light'}
|
||||
>
|
||||
<ReplayIcon />
|
||||
</NavBarButton>
|
||||
</IconButton>
|
||||
|
||||
<UrlForm
|
||||
currentAddress={recordingUrl}
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Grid, Button, Box, Typography } from '@mui/material';
|
||||
import { SaveRecording } from "./SaveRecording";
|
||||
import { SaveRecording } from "../recorder/SaveRecording";
|
||||
import { useGlobalInfoStore } from '../../context/globalInfo';
|
||||
import { stopRecording } from "../../api/recording";
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { GenericModal } from "../atoms/GenericModal";
|
||||
import { GenericModal } from "../ui/GenericModal";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const BrowserRecordingSave = () => {
|
||||
@@ -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"
|
||||
}}>
|
||||
<Button onClick={() => setOpenModal(true)} variant="outlined" style={{ marginLeft: "25px" }} size="small" color="error">
|
||||
<Button
|
||||
onClick={() => setOpenModal(true)}
|
||||
variant="outlined"
|
||||
color="error"
|
||||
sx={{
|
||||
marginLeft: '25px',
|
||||
color: 'red !important',
|
||||
borderColor: 'red !important',
|
||||
backgroundColor: 'whitesmoke !important',
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
{t('right_panel.buttons.discard')}
|
||||
</Button>
|
||||
<GenericModal isOpen={openModal} onClose={() => setOpenModal(false)} modalStyle={modalStyle}>
|
||||
@@ -48,7 +60,14 @@ const BrowserRecordingSave = () => {
|
||||
<Button onClick={goToMainMenu} variant="contained" color="error">
|
||||
{t('right_panel.buttons.discard')}
|
||||
</Button>
|
||||
<Button onClick={() => setOpenModal(false)} variant="outlined">
|
||||
<Button
|
||||
onClick={() => setOpenModal(false)}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
color: '#ff00c3 !important',
|
||||
borderColor: '#ff00c3 !important',
|
||||
backgroundColor: 'whitesmoke !important',
|
||||
}} >
|
||||
{t('right_panel.buttons.cancel')}
|
||||
</Button>
|
||||
</Box>
|
||||
@@ -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 (
|
||||
<Box sx={{
|
||||
width: 800,
|
||||
width: 800, // Fixed width
|
||||
display: 'flex',
|
||||
overflow: 'auto',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}> {/* Synced border color */}
|
||||
<Tabs
|
||||
value={tabIndex}
|
||||
onChange={handleChange}
|
||||
@@ -48,7 +49,11 @@ export const BrowserTabs = (
|
||||
id={`tab-${index}`}
|
||||
sx={{
|
||||
background: 'white',
|
||||
borderRadius: '5px 5px 0px 0px'
|
||||
borderRadius: '5px 5px 0px 0px',
|
||||
'&.Mui-selected': {
|
||||
backgroundColor: ` ${isDarkMode ? "#2a2a2a" : "#f5f5f5"}`, // Synced selected tab color
|
||||
color: '#ff00c3', // Slightly lighter text when selected
|
||||
},
|
||||
}}
|
||||
icon={<CloseButton closeTab={() => {
|
||||
tabWasClosed = true;
|
||||
@@ -60,8 +65,7 @@ export const BrowserTabs = (
|
||||
if (!tabWasClosed) {
|
||||
handleTabChange(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
label={tab}
|
||||
/>
|
||||
);
|
||||
@@ -1,18 +1,19 @@
|
||||
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';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
interface ElementInfo {
|
||||
tagName: string;
|
||||
hasOnlyText?: boolean;
|
||||
isIframeContent?: boolean;
|
||||
isShadowRoot?: boolean;
|
||||
innerText?: string;
|
||||
url?: string;
|
||||
imageUrl?: string;
|
||||
@@ -68,7 +69,7 @@ export const BrowserWindow = () => {
|
||||
|
||||
const { socket } = useSocketStore();
|
||||
const { notify } = useGlobalInfoStore();
|
||||
const { getText, getList, paginationMode, paginationType, limitMode } = useActionContext();
|
||||
const { getText, getList, paginationMode, paginationType, limitMode, captureStage } = useActionContext();
|
||||
const { addTextStep, addListStep } = useBrowserSteps();
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
@@ -117,34 +118,81 @@ export const BrowserWindow = () => {
|
||||
}, [screenShot, canvasRef, socket, screencastHandler]);
|
||||
|
||||
const highlighterHandler = useCallback((data: { rect: DOMRect, selector: string, elementInfo: ElementInfo | null, childSelectors?: string[] }) => {
|
||||
console.log("LIST SELECTOR", listSelector);
|
||||
console.log("DATA SELECTOR", data.selector);
|
||||
console.log("CHILD SELECTORS", data.childSelectors);
|
||||
if (getList === true) {
|
||||
if (listSelector) {
|
||||
socket?.emit('listSelector', { selector: listSelector });
|
||||
const hasValidChildSelectors = Array.isArray(data.childSelectors) && data.childSelectors.length > 0;
|
||||
|
||||
if (limitMode) {
|
||||
setHighlighterData(null);
|
||||
} else if (paginationMode) {
|
||||
// only set highlighterData if type is not empty, 'none', 'scrollDown', or 'scrollUp'
|
||||
// Only set highlighterData if type is not empty, 'none', 'scrollDown', or 'scrollUp'
|
||||
if (paginationType !== '' && !['none', 'scrollDown', 'scrollUp'].includes(paginationType)) {
|
||||
setHighlighterData(data);
|
||||
} else {
|
||||
setHighlighterData(null);
|
||||
}
|
||||
} else if (data.childSelectors && data.childSelectors.includes(data.selector)) {
|
||||
// highlight only valid child elements within the listSelector
|
||||
// Highlight only valid child elements within the listSelector
|
||||
setHighlighterData(data);
|
||||
} 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 =>
|
||||
data.selector.includes(':>>') && // Iframe uses :>> for traversal
|
||||
childSelector.split(':>>').some(part =>
|
||||
data.selector.includes(part.trim())
|
||||
)
|
||||
);
|
||||
setHighlighterData(isIframeChild ? data : null);
|
||||
} else if (data.selector.includes(':>>') && hasValidChildSelectors) {
|
||||
// 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 =>
|
||||
// We know data.childSelectors is defined due to hasValidChildSelectors check
|
||||
data.childSelectors!.some(childSelector =>
|
||||
childSelector.includes(part)
|
||||
)
|
||||
);
|
||||
setHighlighterData(isValidMixedSelector ? data : null);
|
||||
} 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 =>
|
||||
data.selector.includes('>>') && // Shadow DOM uses >> for piercing
|
||||
childSelector.split('>>').some(part =>
|
||||
data.selector.includes(part.trim())
|
||||
)
|
||||
);
|
||||
setHighlighterData(isShadowChild ? data : null);
|
||||
} 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 =>
|
||||
// Now we know data.childSelectors is defined
|
||||
data.childSelectors!.some(childSelector =>
|
||||
childSelector.includes(part)
|
||||
)
|
||||
);
|
||||
setHighlighterData(isValidMixedSelector ? data : null);
|
||||
} else {
|
||||
// if !valid child in normal mode, clear the highlighter
|
||||
setHighlighterData(null);
|
||||
}
|
||||
} else {
|
||||
// set highlighterData for the initial listSelector selection
|
||||
// Set highlighterData for the initial listSelector selection
|
||||
setHighlighterData(data);
|
||||
}
|
||||
} else {
|
||||
// for non-list steps
|
||||
// For non-list steps
|
||||
setHighlighterData(data);
|
||||
}
|
||||
}, [highlighterData, getList, socket, listSelector, paginationMode, paginationType]);
|
||||
}, [highlighterData, getList, socket, listSelector, paginationMode, paginationType, captureStage]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
@@ -158,6 +206,13 @@ export const BrowserWindow = () => {
|
||||
};
|
||||
}, [socket, onMouseMove]);
|
||||
|
||||
useEffect(() => {
|
||||
if (captureStage === 'initial' && listSelector) {
|
||||
socket?.emit('setGetList', { getList: true });
|
||||
socket?.emit('listSelector', { selector: listSelector });
|
||||
}
|
||||
}, [captureStage, listSelector, socket]);
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (highlighterData && canvasRef?.current) {
|
||||
const canvasRect = canvasRef.current.getBoundingClientRect();
|
||||
@@ -185,6 +240,7 @@ export const BrowserWindow = () => {
|
||||
addTextStep('', data, {
|
||||
selector: highlighterData.selector,
|
||||
tag: highlighterData.elementInfo?.tagName,
|
||||
shadow: highlighterData.elementInfo?.isShadowRoot,
|
||||
attribute
|
||||
});
|
||||
} else {
|
||||
@@ -192,7 +248,7 @@ export const BrowserWindow = () => {
|
||||
setAttributeOptions(options);
|
||||
setSelectedElement({
|
||||
selector: highlighterData.selector,
|
||||
info: highlighterData.elementInfo
|
||||
info: highlighterData.elementInfo,
|
||||
});
|
||||
setShowAttributeModal(true);
|
||||
}
|
||||
@@ -229,6 +285,7 @@ export const BrowserWindow = () => {
|
||||
selectorObj: {
|
||||
selector: highlighterData.selector,
|
||||
tag: highlighterData.elementInfo?.tagName,
|
||||
shadow: highlighterData.elementInfo?.isShadowRoot,
|
||||
attribute
|
||||
}
|
||||
};
|
||||
@@ -276,6 +333,7 @@ export const BrowserWindow = () => {
|
||||
addTextStep('', data, {
|
||||
selector: selectedElement.selector,
|
||||
tag: selectedElement.info?.tagName,
|
||||
shadow: selectedElement.info?.isShadowRoot,
|
||||
attribute: attribute
|
||||
});
|
||||
}
|
||||
@@ -288,6 +346,7 @@ export const BrowserWindow = () => {
|
||||
selectorObj: {
|
||||
selector: selectedElement.selector,
|
||||
tag: selectedElement.info?.tagName,
|
||||
shadow: selectedElement.info?.isShadowRoot,
|
||||
attribute: attribute
|
||||
}
|
||||
};
|
||||
@@ -319,7 +378,6 @@ export const BrowserWindow = () => {
|
||||
}
|
||||
}, [paginationMode, resetPaginationSelector]);
|
||||
|
||||
|
||||
return (
|
||||
<div onClick={handleClick} style={{ width: '900px' }} id="browser-window">
|
||||
{
|
||||
@@ -345,6 +403,11 @@ export const BrowserWindow = () => {
|
||||
overflow: 'hidden',
|
||||
padding: '5px 10px',
|
||||
}}
|
||||
sx={{
|
||||
color: '#ff00c3 !important',
|
||||
borderColor: '#ff00c3 !important',
|
||||
backgroundColor: 'whitesmoke !important',
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
display: 'block',
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import type { SyntheticEvent } from 'react';
|
||||
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
|
||||
import { NavBarForm, NavBarInput } from "../atoms/form";
|
||||
import { UrlFormButton } from "../atoms/buttons/buttons";
|
||||
import { NavBarForm, NavBarInput } from "../ui/form";
|
||||
import { UrlFormButton } from "../ui/buttons/buttons";
|
||||
import { useSocketStore } from '../../context/socket';
|
||||
import { Socket } from "socket.io-client";
|
||||
|
||||
@@ -1,41 +1,56 @@
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import Box from '@mui/material/Box';
|
||||
import { Paper, Button } from "@mui/material";
|
||||
import { AutoAwesome, FormatListBulleted, VpnKey, Usb, CloudQueue, Code } from "@mui/icons-material";
|
||||
import { Paper, Button, useTheme } from "@mui/material";
|
||||
import { AutoAwesome, FormatListBulleted, VpnKey, Usb, Article, CloudQueue, Code, } from "@mui/icons-material";
|
||||
import { apiUrl } from "../../apiConfig";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '../../i18n';
|
||||
|
||||
|
||||
interface MainMenuProps {
|
||||
value: string;
|
||||
handleChangeContent: (newValue: string) => void;
|
||||
}
|
||||
|
||||
export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenuProps) => {
|
||||
const {t} = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleChange = (event: React.SyntheticEvent, newValue: string) => {
|
||||
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 (
|
||||
<Paper
|
||||
sx={{
|
||||
height: 'auto',
|
||||
width: '250px',
|
||||
backgroundColor: 'white',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
paddingTop: '0.5rem',
|
||||
color: defaultcolor,
|
||||
}}
|
||||
variant="outlined"
|
||||
square
|
||||
>
|
||||
<Box sx={{
|
||||
width: '100%',
|
||||
paddingBottom: '1rem',
|
||||
}}>
|
||||
<Box sx={{ width: '100%', paddingBottom: '1rem' }}>
|
||||
<Tabs
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
@@ -101,17 +116,4 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
const buttonStyles = {
|
||||
justifyContent: 'flex-start',
|
||||
textAlign: 'left',
|
||||
fontSize: 'medium',
|
||||
padding: '6px 16px 6px 22px',
|
||||
minHeight: '48px',
|
||||
minWidth: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
textTransform: 'none',
|
||||
color: '#6C6C6C !important',
|
||||
};
|
||||
@@ -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<NavBarProps> = ({
|
||||
const { state, dispatch } = useContext(AuthContext);
|
||||
const { user } = state;
|
||||
const navigate = useNavigate();
|
||||
const { darkMode, toggleTheme } = useThemeMode();
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
@@ -102,6 +103,22 @@ export const NavBar: React.FC<NavBarProps> = ({
|
||||
localStorage.setItem("language", lang);
|
||||
};
|
||||
|
||||
const renderThemeToggle = () => (
|
||||
<Tooltip title="Toggle Mode">
|
||||
<IconButton
|
||||
onClick={toggleTheme}
|
||||
sx={{
|
||||
color: darkMode ? '#ffffff' : '#0000008A',
|
||||
'&:hover': {
|
||||
color: '#ff00c3'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{darkMode ? <Brightness7 /> : <Brightness4 />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const checkForUpdates = async () => {
|
||||
const latestVersion = await fetchLatestVersion();
|
||||
@@ -158,13 +175,13 @@ export const NavBar: React.FC<NavBarProps> = ({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<NavBarWrapper>
|
||||
<NavBarWrapper mode={darkMode ? 'dark' : 'light'}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
}}>
|
||||
<img src={MaxunLogo} width={45} height={40} style={{ borderRadius: '5px', margin: '5px 0px 5px 15px' }} />
|
||||
<div style={{ padding: '11px' }}><ProjectName>{t('navbar.project_name')}</ProjectName></div>
|
||||
<div style={{ padding: '11px' }}><ProjectName mode={darkMode ? 'dark' : 'light'}>{t('navbar.project_name')}</ProjectName></div>
|
||||
<Chip
|
||||
label={`${currentVersion}`}
|
||||
color="primary"
|
||||
@@ -261,6 +278,11 @@ export const NavBar: React.FC<NavBarProps> = ({
|
||||
docker-compose down
|
||||
<br />
|
||||
<br />
|
||||
# replace existing docker-compose file with new one by copy pasting the code from
|
||||
<br />
|
||||
<a href="https://github.com/getmaxun/maxun/blob/develop/docker-compose.yml">Latest Docker Compose</a>
|
||||
<br />
|
||||
<br />
|
||||
# pull latest docker images
|
||||
<br />
|
||||
docker-compose pull
|
||||
@@ -283,7 +305,6 @@ export const NavBar: React.FC<NavBarProps> = ({
|
||||
borderRadius: '5px',
|
||||
padding: '8px',
|
||||
marginRight: '10px',
|
||||
'&:hover': { backgroundColor: 'white', color: '#ff00c3' }
|
||||
}}>
|
||||
<AccountCircle sx={{ marginRight: '5px' }} />
|
||||
<Typography variant="body1">{user.email}</Typography>
|
||||
@@ -305,6 +326,11 @@ export const NavBar: React.FC<NavBarProps> = ({
|
||||
<MenuItem onClick={() => { handleMenuClose(); logout(); }}>
|
||||
<Logout sx={{ marginRight: '5px' }} /> {t('navbar.menu_items.logout')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => {
|
||||
window.open('https://docs.maxun.dev', '_blank');
|
||||
}}>
|
||||
<Description sx={{ marginRight: '5px' }} /> Docs
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => {
|
||||
window.open('https://discord.gg/5GbPjBUkws', '_blank');
|
||||
}}>
|
||||
@@ -318,7 +344,7 @@ export const NavBar: React.FC<NavBarProps> = ({
|
||||
<MenuItem onClick={() => {
|
||||
window.open('https://x.com/maxun_io?ref=app', '_blank');
|
||||
}}>
|
||||
<X sx={{ marginRight: '5px' }} /> Twiiter (X)
|
||||
<X sx={{ marginRight: '5px' }} /> Twitter (X)
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleLangMenuOpen}>
|
||||
<Language sx={{ marginRight: '5px' }} /> {t('navbar.menu_items.language')}
|
||||
@@ -376,17 +402,9 @@ export const NavBar: React.FC<NavBarProps> = ({
|
||||
>
|
||||
Deutsch
|
||||
</MenuItem>
|
||||
{/* WIP: Replace change language with i18n docs link */}
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
changeLanguage("de");
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
Add Language
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Menu>
|
||||
{renderThemeToggle()}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -406,18 +424,19 @@ export const NavBar: React.FC<NavBarProps> = ({
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<><IconButton
|
||||
onClick={handleLangMenuOpen}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
borderRadius: "5px",
|
||||
padding: "8px",
|
||||
marginRight: "10px",
|
||||
}}
|
||||
>
|
||||
<Language sx={{ marginRight: '5px' }} /><Typography variant="body1">{t("Language")}</Typography>
|
||||
</IconButton>
|
||||
<NavBarRight>
|
||||
<IconButton
|
||||
onClick={handleLangMenuOpen}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
borderRadius: "5px",
|
||||
padding: "8px",
|
||||
marginRight: "8px",
|
||||
}}
|
||||
>
|
||||
<Language sx={{ marginRight: '5px' }} /><Typography variant="body1">{t("Language")}</Typography>
|
||||
</IconButton>
|
||||
<Menu
|
||||
anchorEl={langAnchorEl}
|
||||
open={Boolean(langAnchorEl)}
|
||||
@@ -471,32 +490,33 @@ export const NavBar: React.FC<NavBarProps> = ({
|
||||
>
|
||||
Deutsch
|
||||
</MenuItem>
|
||||
{/* WIP: Replace change language with i18n docs link */}
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
changeLanguage("de");
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
Add Language
|
||||
</MenuItem>
|
||||
</Menu></>
|
||||
</Menu>
|
||||
{renderThemeToggle()}
|
||||
</NavBarRight>
|
||||
)}
|
||||
</NavBarWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const NavBarWrapper = styled.div`
|
||||
// Styled Components
|
||||
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;
|
||||
`;
|
||||
@@ -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" />
|
||||
</g>
|
||||
</g>
|
||||
@@ -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 (
|
||||
<GenericModal isOpen={isOpen} onClose={handleClose} modalStyle={modalStyle}>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
marginLeft: "65px",
|
||||
}}>
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
marginLeft: "65px",
|
||||
}}>
|
||||
<Typography variant="h6">
|
||||
{t('integration_settings.title')}
|
||||
</Typography>
|
||||
@@ -220,8 +235,8 @@ export const IntegrationSettingsModal = ({
|
||||
<>
|
||||
{recording.google_sheet_email && (
|
||||
<Typography sx={{ margin: "20px 0px 30px 0px" }}>
|
||||
{t('integration_settings.descriptions.authenticated_as', {
|
||||
email: recording.google_sheet_email
|
||||
{t('integration_settings.descriptions.authenticated_as', {
|
||||
email: recording.google_sheet_email
|
||||
})}
|
||||
</Typography>
|
||||
)}
|
||||
@@ -309,4 +324,4 @@ export const modalStyle = {
|
||||
height: "fit-content",
|
||||
display: "block",
|
||||
padding: "20px",
|
||||
};
|
||||
};
|
||||
@@ -1,133 +0,0 @@
|
||||
import { WhereWhatPair } from "maxun-core";
|
||||
import { GenericModal } from "../atoms/GenericModal";
|
||||
import { modalStyle } from "./AddWhereCondModal";
|
||||
import { Button, MenuItem, TextField, Typography } from "@mui/material";
|
||||
import React, { useRef } from "react";
|
||||
import { Dropdown as MuiDropdown } from "../atoms/DropdownMui";
|
||||
import { KeyValueForm } from "./KeyValueForm";
|
||||
import { ClearButton } from "../atoms/buttons/ClearButton";
|
||||
import { useSocketStore } from "../../context/socket";
|
||||
|
||||
interface AddWhatCondModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
pair: WhereWhatPair;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export const AddWhatCondModal = ({isOpen, onClose, pair, index}: AddWhatCondModalProps) => {
|
||||
const [action, setAction] = React.useState<string>('');
|
||||
const [objectIndex, setObjectIndex] = React.useState<number>(0);
|
||||
const [args, setArgs] = React.useState<({type: string, value: (string|number|object|unknown)})[]>([]);
|
||||
|
||||
const objectRefs = useRef<({getObject: () => object}|unknown)[]>([]);
|
||||
|
||||
const {socket} = useSocketStore();
|
||||
|
||||
const handleSubmit = () => {
|
||||
const argsArray: (string|number|object|unknown)[] = [];
|
||||
args.map((arg, index) => {
|
||||
switch (arg.type) {
|
||||
case 'string':
|
||||
case 'number':
|
||||
argsArray[index] = arg.value;
|
||||
break;
|
||||
case 'object':
|
||||
// @ts-ignore
|
||||
argsArray[index] = objectRefs.current[arg.value].getObject();
|
||||
}
|
||||
})
|
||||
setArgs([]);
|
||||
onClose();
|
||||
pair.what.push({
|
||||
// @ts-ignore
|
||||
action,
|
||||
args: argsArray,
|
||||
})
|
||||
socket?.emit('updatePair', {index: index-1, pair: pair});
|
||||
}
|
||||
|
||||
return (
|
||||
<GenericModal isOpen={isOpen} onClose={() => {
|
||||
setArgs([]);
|
||||
onClose();
|
||||
}} modalStyle={modalStyle}>
|
||||
<div>
|
||||
<Typography sx={{margin: '20px 0px'}}>Add what condition:</Typography>
|
||||
<div style={{margin:'8px'}}>
|
||||
<Typography>Action:</Typography>
|
||||
<TextField
|
||||
size='small'
|
||||
type="string"
|
||||
onChange={(e) => setAction(e.target.value)}
|
||||
value={action}
|
||||
label='action'
|
||||
/>
|
||||
<div>
|
||||
<Typography>Add new argument of type:</Typography>
|
||||
<Button onClick={() => setArgs([...args,{type: 'string', value: null}]) }>string</Button>
|
||||
<Button onClick={() => setArgs([...args,{type: 'number', value: null}]) }>number</Button>
|
||||
<Button onClick={() => {
|
||||
setArgs([...args,{type: 'object', value: objectIndex}])
|
||||
setObjectIndex(objectIndex+1);
|
||||
} }>object</Button>
|
||||
</div>
|
||||
<Typography>args:</Typography>
|
||||
{args.map((arg, index) => {
|
||||
// @ts-ignore
|
||||
return (
|
||||
<div style={{border:'solid 1px gray', padding: '10px', display:'flex', flexDirection:'row', alignItems:'center' }}
|
||||
key={`wrapper-for-${arg.type}-${index}`}>
|
||||
<ClearButton handleClick={() => {
|
||||
args.splice(index,1);
|
||||
setArgs([...args]);
|
||||
}}/>
|
||||
<Typography sx={{margin: '5px'}} key={`number-argument-${arg.type}-${index}`}>{index}: </Typography>
|
||||
{arg.type === 'string' ?
|
||||
<TextField
|
||||
size='small'
|
||||
type="string"
|
||||
onChange={(e) => setArgs([
|
||||
...args.slice(0, index),
|
||||
{type: arg.type, value: e.target.value},
|
||||
...args.slice(index + 1)
|
||||
])}
|
||||
value={args[index].value || ''}
|
||||
label="string"
|
||||
key={`arg-${arg.type}-${index}`}
|
||||
/> : arg.type === 'number' ?
|
||||
<TextField
|
||||
key={`arg-${arg.type}-${index}`}
|
||||
size='small'
|
||||
type="number"
|
||||
onChange={(e) => setArgs([
|
||||
...args.slice(0, index),
|
||||
{type: arg.type, value: Number(e.target.value)},
|
||||
...args.slice(index + 1)
|
||||
])}
|
||||
value={args[index].value || ''}
|
||||
label="number"
|
||||
/> :
|
||||
<KeyValueForm ref={el =>
|
||||
//@ts-ignore
|
||||
objectRefs.current[arg.value] = el} key={`arg-${arg.type}-${index}`}/>
|
||||
}
|
||||
</div>
|
||||
)})}
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
display: "table-cell",
|
||||
float: "right",
|
||||
marginRight: "15px",
|
||||
marginTop: "20px",
|
||||
}}
|
||||
>
|
||||
{"Add Condition"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</GenericModal>
|
||||
)
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
import { Dropdown as MuiDropdown } from "../atoms/DropdownMui";
|
||||
import {
|
||||
Button,
|
||||
MenuItem,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
import React, { useRef } from "react";
|
||||
import { GenericModal } from "../atoms/GenericModal";
|
||||
import { WhereWhatPair } from "maxun-core";
|
||||
import { SelectChangeEvent } from "@mui/material/Select/Select";
|
||||
import { DisplayConditionSettings } from "./DisplayWhereConditionSettings";
|
||||
import { useSocketStore } from "../../context/socket";
|
||||
|
||||
interface AddWhereCondModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
pair: WhereWhatPair;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export const AddWhereCondModal = ({isOpen, onClose, pair, index}: AddWhereCondModalProps) => {
|
||||
const [whereProp, setWhereProp] = React.useState<string>('');
|
||||
const [additionalSettings, setAdditionalSettings] = React.useState<string>('');
|
||||
const [newValue, setNewValue] = React.useState<any>('');
|
||||
const [checked, setChecked] = React.useState<boolean[]>(new Array(Object.keys(pair.where).length).fill(false));
|
||||
|
||||
const keyValueFormRef = useRef<{getObject: () => object}>(null);
|
||||
|
||||
const {socket} = useSocketStore();
|
||||
|
||||
const handlePropSelect = (event: SelectChangeEvent<string>) => {
|
||||
setWhereProp(event.target.value);
|
||||
switch (event.target.value) {
|
||||
case 'url': setNewValue(''); break;
|
||||
case 'selectors': setNewValue(['']); break;
|
||||
case 'default': return;
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
switch (whereProp) {
|
||||
case 'url':
|
||||
if (additionalSettings === 'string'){
|
||||
pair.where.url = newValue;
|
||||
} else {
|
||||
pair.where.url = { $regex: newValue };
|
||||
}
|
||||
break;
|
||||
case 'selectors':
|
||||
pair.where.selectors = newValue;
|
||||
break;
|
||||
case 'cookies':
|
||||
pair.where.cookies = keyValueFormRef.current?.getObject() as Record<string,string>
|
||||
break;
|
||||
case 'before':
|
||||
pair.where.$before = newValue;
|
||||
break;
|
||||
case 'after':
|
||||
pair.where.$after = newValue;
|
||||
break;
|
||||
case 'boolean':
|
||||
const booleanArr = [];
|
||||
const deleteKeys: string[] = [];
|
||||
for (let i = 0; i < checked.length; i++) {
|
||||
if (checked[i]) {
|
||||
if (Object.keys(pair.where)[i]) {
|
||||
//@ts-ignore
|
||||
if (pair.where[Object.keys(pair.where)[i]]) {
|
||||
booleanArr.push({
|
||||
//@ts-ignore
|
||||
[Object.keys(pair.where)[i]]: pair.where[Object.keys(pair.where)[i]]});
|
||||
}
|
||||
deleteKeys.push(Object.keys(pair.where)[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
deleteKeys.forEach((key: string) => delete pair.where[key]);
|
||||
//@ts-ignore
|
||||
pair.where[`$${additionalSettings}`] = booleanArr;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
onClose();
|
||||
setWhereProp('');
|
||||
setAdditionalSettings('');
|
||||
setNewValue('');
|
||||
socket?.emit('updatePair', {index: index-1, pair: pair});
|
||||
}
|
||||
|
||||
return (
|
||||
<GenericModal isOpen={isOpen} onClose={() => {
|
||||
setWhereProp('');
|
||||
setAdditionalSettings('');
|
||||
setNewValue('');
|
||||
onClose();
|
||||
}} modalStyle={modalStyle}>
|
||||
<div>
|
||||
<Typography sx={{margin: '20px 0px'}}>Add where condition:</Typography>
|
||||
<div style={{margin:'8px'}}>
|
||||
<MuiDropdown
|
||||
id="whereProp"
|
||||
label="Condition"
|
||||
value={whereProp}
|
||||
handleSelect={handlePropSelect}>
|
||||
<MenuItem value="url">url</MenuItem>
|
||||
<MenuItem value="selectors">selectors</MenuItem>
|
||||
<MenuItem value="cookies">cookies</MenuItem>
|
||||
<MenuItem value="before">before</MenuItem>
|
||||
<MenuItem value="after">after</MenuItem>
|
||||
<MenuItem value="boolean">boolean logic</MenuItem>
|
||||
</MuiDropdown>
|
||||
</div>
|
||||
{whereProp ?
|
||||
<div style={{margin: '8px'}}>
|
||||
<DisplayConditionSettings
|
||||
whereProp={whereProp} additionalSettings={additionalSettings} setAdditionalSettings={setAdditionalSettings}
|
||||
newValue={newValue} setNewValue={setNewValue} checked={checked} setChecked={setChecked}
|
||||
keyValueFormRef={keyValueFormRef} whereKeys={Object.keys(pair.where)}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
display: "table-cell",
|
||||
float: "right",
|
||||
marginRight: "15px",
|
||||
marginTop: "20px",
|
||||
}}
|
||||
>
|
||||
{"Add Condition"}
|
||||
</Button>
|
||||
</div>
|
||||
: null}
|
||||
</div>
|
||||
</GenericModal>
|
||||
)
|
||||
}
|
||||
|
||||
export const modalStyle = {
|
||||
top: '40%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: '30%',
|
||||
backgroundColor: 'background.paper',
|
||||
p: 4,
|
||||
height:'fit-content',
|
||||
display:'block',
|
||||
padding: '20px',
|
||||
};
|
||||
@@ -1,86 +0,0 @@
|
||||
import React from "react";
|
||||
import { Button, MenuItem, TextField, Typography } from "@mui/material";
|
||||
import { Dropdown } from "../atoms/DropdownMui";
|
||||
import { RunSettings } from "./RunSettings";
|
||||
import { useSocketStore } from "../../context/socket";
|
||||
|
||||
interface LeftSidePanelSettingsProps {
|
||||
params: any[]
|
||||
settings: RunSettings,
|
||||
setSettings: (setting: RunSettings) => void
|
||||
}
|
||||
|
||||
export const LeftSidePanelSettings = ({params, settings, setSettings}: LeftSidePanelSettingsProps) => {
|
||||
const { socket } = useSocketStore();
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection:'column', alignItems: 'flex-start'}}>
|
||||
{ params.length !== 0 && (
|
||||
<React.Fragment>
|
||||
<Typography>Parameters:</Typography>
|
||||
{ params?.map((item: string, index: number) => {
|
||||
return <TextField
|
||||
sx={{margin: '15px 0px'}}
|
||||
value={settings.params ? settings.params[item] : ''}
|
||||
key={`param-${index}`}
|
||||
type="string"
|
||||
label={item}
|
||||
required
|
||||
onChange={(e) => setSettings(
|
||||
{
|
||||
...settings,
|
||||
params: settings.params
|
||||
? {
|
||||
...settings.params,
|
||||
[item]: e.target.value,
|
||||
}
|
||||
: {
|
||||
[item]: e.target.value,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
}) }
|
||||
</React.Fragment>
|
||||
)}
|
||||
<Typography sx={{margin: '15px 0px'}}>Interpreter:</Typography>
|
||||
<TextField
|
||||
type="number"
|
||||
label="maxConcurrency"
|
||||
required
|
||||
onChange={(e) => setSettings(
|
||||
{
|
||||
...settings,
|
||||
maxConcurrency: parseInt(e.target.value),
|
||||
})}
|
||||
defaultValue={settings.maxConcurrency}
|
||||
/>
|
||||
<TextField
|
||||
sx={{margin: '15px 0px'}}
|
||||
type="number"
|
||||
label="maxRepeats"
|
||||
required
|
||||
onChange={(e) => setSettings(
|
||||
{
|
||||
...settings,
|
||||
maxRepeats: parseInt(e.target.value),
|
||||
})}
|
||||
defaultValue={settings.maxRepeats}
|
||||
/>
|
||||
<Dropdown
|
||||
id="debug"
|
||||
label="debug"
|
||||
value={settings.debug?.toString()}
|
||||
handleSelect={(e) => setSettings(
|
||||
{
|
||||
...settings,
|
||||
debug: e.target.value === "true",
|
||||
})}
|
||||
>
|
||||
<MenuItem value="true">true</MenuItem>
|
||||
<MenuItem value="false">false</MenuItem>
|
||||
</Dropdown>
|
||||
<Button sx={{margin: '15px 0px'}} variant='contained'
|
||||
onClick={() => socket?.emit('settings', settings)}>change</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,310 +0,0 @@
|
||||
import React, { useLayoutEffect, useRef, useState } from 'react';
|
||||
import { WhereWhatPair } from "maxun-core";
|
||||
import { Box, Button, IconButton, MenuItem, Stack, TextField, Tooltip, Typography } from "@mui/material";
|
||||
import { Close, KeyboardArrowDown, KeyboardArrowUp } from "@mui/icons-material";
|
||||
import TreeView from '@mui/lab/TreeView';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||
import TreeItem from '@mui/lab/TreeItem';
|
||||
import { AddButton } from "../atoms/buttons/AddButton";
|
||||
import { WarningText } from "../atoms/texts";
|
||||
import NotificationImportantIcon from '@mui/icons-material/NotificationImportant';
|
||||
import { RemoveButton } from "../atoms/buttons/RemoveButton";
|
||||
import { AddWhereCondModal } from "./AddWhereCondModal";
|
||||
import { UpdatePair } from "../../api/workflow";
|
||||
import { useSocketStore } from "../../context/socket";
|
||||
import { AddWhatCondModal } from "./AddWhatCondModal";
|
||||
|
||||
interface PairDetailProps {
|
||||
pair: WhereWhatPair | null;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export const PairDetail = ({ pair, index }: PairDetailProps) => {
|
||||
const [pairIsSelected, setPairIsSelected] = useState(false);
|
||||
const [collapseWhere, setCollapseWhere] = useState(true);
|
||||
const [collapseWhat, setCollapseWhat] = useState(true);
|
||||
const [rerender, setRerender] = useState(false);
|
||||
const [expanded, setExpanded] = React.useState<string[]>(
|
||||
pair ? Object.keys(pair.where).map((key, index) => `${key}-${index}`) : []
|
||||
);
|
||||
const [addWhereCondOpen, setAddWhereCondOpen] = useState(false);
|
||||
const [addWhatCondOpen, setAddWhatCondOpen] = useState(false);
|
||||
|
||||
const { socket } = useSocketStore();
|
||||
|
||||
const handleCollapseWhere = () => {
|
||||
setCollapseWhere(!collapseWhere);
|
||||
}
|
||||
|
||||
const handleCollapseWhat = () => {
|
||||
setCollapseWhat(!collapseWhat);
|
||||
}
|
||||
|
||||
const handleToggle = (event: React.SyntheticEvent, nodeIds: string[]) => {
|
||||
setExpanded(nodeIds);
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (pair) {
|
||||
setPairIsSelected(true);
|
||||
}
|
||||
}, [pair])
|
||||
|
||||
const handleChangeValue = (value: any, where: boolean, keys: (string|number)[]) => {
|
||||
// a moving reference to internal objects within pair.where or pair.what
|
||||
let schema: any = where ? pair?.where : pair?.what;
|
||||
const length = keys.length;
|
||||
for(let i = 0; i < length-1; i++) {
|
||||
const elem = keys[i];
|
||||
if( !schema[elem] ) schema[elem] = {}
|
||||
schema = schema[elem];
|
||||
}
|
||||
|
||||
schema[keys[length-1]] = value;
|
||||
if (pair && socket) {
|
||||
socket.emit('updatePair', {index: index-1, pair: pair});
|
||||
}
|
||||
setRerender(!rerender);
|
||||
}
|
||||
|
||||
|
||||
const DisplayValueContent = (value: any, keys: (string|number)[], where: boolean = true) => {
|
||||
switch (typeof(value)) {
|
||||
case 'string':
|
||||
return <TextField
|
||||
size='small'
|
||||
type="string"
|
||||
onChange={(e) => {
|
||||
try {
|
||||
const obj = JSON.parse(e.target.value);
|
||||
handleChangeValue(obj, where, keys);
|
||||
} catch (error) {
|
||||
const num = Number(e.target.value);
|
||||
if (!isNaN(num)) {
|
||||
handleChangeValue(num, where, keys);
|
||||
}
|
||||
handleChangeValue(e.target.value, where, keys)
|
||||
}
|
||||
}}
|
||||
defaultValue={value}
|
||||
key={`text-field-${keys.join('-')}-${where}`}
|
||||
/>
|
||||
case 'number':
|
||||
return <TextField
|
||||
size='small'
|
||||
type="number"
|
||||
onChange={(e) => handleChangeValue(Number(e.target.value), where, keys)}
|
||||
defaultValue={value}
|
||||
key={`text-field-${keys.join('-')}-${where}`}
|
||||
/>
|
||||
case 'object':
|
||||
if (value) {
|
||||
if (Array.isArray(value)) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{
|
||||
value.map((element, index) => {
|
||||
return DisplayValueContent(element, [...keys, index], where);
|
||||
})
|
||||
}
|
||||
<AddButton handleClick={()=> {
|
||||
let prevValue:any = where ? pair?.where : pair?.what;
|
||||
for (const key of keys) {
|
||||
prevValue = prevValue[key];
|
||||
}
|
||||
handleChangeValue([...prevValue, ''], where, keys);
|
||||
setRerender(!rerender);
|
||||
}} hoverEffect={false}/>
|
||||
<RemoveButton handleClick={()=> {
|
||||
let prevValue:any = where ? pair?.where : pair?.what;
|
||||
for (const key of keys) {
|
||||
prevValue = prevValue[key];
|
||||
}
|
||||
prevValue.splice(-1);
|
||||
handleChangeValue(prevValue, where, keys);
|
||||
setRerender(!rerender);
|
||||
}}/>
|
||||
</React.Fragment>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<TreeView
|
||||
defaultCollapseIcon={<ExpandMoreIcon />}
|
||||
defaultExpandIcon={<ChevronRightIcon />}
|
||||
sx={{ flexGrow: 1, overflowY: 'auto' }}
|
||||
key={`tree-view-nested-${keys.join('-')}-${where}`}
|
||||
>
|
||||
{
|
||||
Object.keys(value).map((key2, index) =>
|
||||
{
|
||||
return (
|
||||
<TreeItem nodeId={`${key2}-${index}`} label={`${key2}:`} key={`${key2}-${index}`}>
|
||||
{ DisplayValueContent(value[key2], [...keys, key2], where) }
|
||||
</TreeItem>
|
||||
)
|
||||
})
|
||||
}
|
||||
</TreeView>
|
||||
)
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{ pair &&
|
||||
<React.Fragment>
|
||||
<AddWhatCondModal isOpen={addWhatCondOpen} onClose={() => setAddWhatCondOpen(false)}
|
||||
pair={pair} index={index}/>
|
||||
<AddWhereCondModal isOpen={addWhereCondOpen} onClose={() => setAddWhereCondOpen(false)}
|
||||
pair={pair} index={index}/>
|
||||
</React.Fragment>
|
||||
}
|
||||
{
|
||||
pairIsSelected
|
||||
? (
|
||||
<div style={{padding: '10px', overflow: 'hidden'}}>
|
||||
<Typography>Pair number: {index}</Typography>
|
||||
<TextField
|
||||
size='small'
|
||||
label='id'
|
||||
onChange={(e) => {
|
||||
if (pair && socket) {
|
||||
socket.emit('updatePair', {index: index-1, pair: pair});
|
||||
pair.id = e.target.value;
|
||||
}
|
||||
}}
|
||||
value={pair ? pair.id ? pair.id : '' : ''}
|
||||
/>
|
||||
<Stack spacing={0} direction='row' sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
background: 'lightGray',
|
||||
}}>
|
||||
<CollapseButton
|
||||
handleClick={handleCollapseWhere}
|
||||
isCollapsed={collapseWhere}
|
||||
/>
|
||||
<Typography>Where</Typography>
|
||||
<Tooltip title='Add where condition' placement='right'>
|
||||
<div>
|
||||
<AddButton handleClick={()=> {
|
||||
setAddWhereCondOpen(true);
|
||||
}} style={{color:'rgba(0, 0, 0, 0.54)', background:'transparent'}}/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
{(collapseWhere && pair && pair.where)
|
||||
?
|
||||
<React.Fragment>
|
||||
{ Object.keys(pair.where).map((key, index) => {
|
||||
return (
|
||||
<TreeView
|
||||
expanded={expanded}
|
||||
defaultCollapseIcon={<ExpandMoreIcon />}
|
||||
defaultExpandIcon={<ChevronRightIcon />}
|
||||
sx={{ flexGrow: 1, overflowY: 'auto' }}
|
||||
onNodeToggle={handleToggle}
|
||||
key={`tree-view-${key}-${index}`}
|
||||
>
|
||||
<TreeItem nodeId={`${key}-${index}`} label={`${key}:`} key={`${key}-${index}`}>
|
||||
{
|
||||
// @ts-ignore
|
||||
DisplayValueContent(pair.where[key], [key])
|
||||
}
|
||||
</TreeItem>
|
||||
</TreeView>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
: null
|
||||
}
|
||||
<Stack spacing={0} direction='row' sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
background: 'lightGray',
|
||||
}}>
|
||||
<CollapseButton
|
||||
handleClick={handleCollapseWhat}
|
||||
isCollapsed={collapseWhat}
|
||||
/>
|
||||
<Typography>What</Typography>
|
||||
|
||||
<Tooltip title='Add what condition' placement='right'>
|
||||
<div>
|
||||
<AddButton handleClick={()=> {
|
||||
setAddWhatCondOpen(true);
|
||||
}} style={{color:'rgba(0, 0, 0, 0.54)', background:'transparent'}}/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
{(collapseWhat && pair && pair.what)
|
||||
?(
|
||||
<React.Fragment>
|
||||
{ Object.keys(pair.what).map((key, index) => {
|
||||
return (
|
||||
<TreeView
|
||||
defaultCollapseIcon={<ExpandMoreIcon />}
|
||||
defaultExpandIcon={<ChevronRightIcon />}
|
||||
sx={{ flexGrow: 1, overflowY: 'auto' }}
|
||||
key={`tree-view-2-${key}-${index}`}
|
||||
>
|
||||
<TreeItem nodeId={`${key}-${index}`} label={`${pair.what[index].action}`}>
|
||||
{
|
||||
// @ts-ignore
|
||||
DisplayValueContent(pair.what[key], [key], false)
|
||||
}
|
||||
<Tooltip title='remove action' placement='left'>
|
||||
<div style={{float:'right'}}>
|
||||
<CloseButton handleClick={() => {
|
||||
//@ts-ignore
|
||||
pair.what.splice(key, 1);
|
||||
setRerender(!rerender);
|
||||
}}/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</TreeItem>
|
||||
</TreeView>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
)
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
)
|
||||
: <WarningText>
|
||||
<NotificationImportantIcon color="warning"/>
|
||||
No pair from the left side panel was selected.
|
||||
</WarningText>
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
interface CollapseButtonProps {
|
||||
handleClick: () => void;
|
||||
isCollapsed?: boolean;
|
||||
}
|
||||
|
||||
const CollapseButton = ({handleClick, isCollapsed } : CollapseButtonProps) => {
|
||||
return (
|
||||
<IconButton aria-label="add" size={"small"} onClick={handleClick}>
|
||||
{ isCollapsed ? <KeyboardArrowDown/> : <KeyboardArrowUp/>}
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
|
||||
const CloseButton = ({handleClick } : CollapseButtonProps) => {
|
||||
return (
|
||||
<IconButton aria-label="add" size={"small"} onClick={handleClick}
|
||||
sx={{'&:hover': { color: '#1976d2', backgroundColor: 'white' }}}>
|
||||
<Close/>
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
@@ -27,7 +27,7 @@ const DatePicker: React.FC<DatePickerProps> = ({ coordinates, selector, onClose
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${coordinates.x}px`,
|
||||
@@ -48,20 +48,19 @@ const DatePicker: React.FC<DatePickerProps> = ({ coordinates, selector, onClose
|
||||
autoFocus
|
||||
/>
|
||||
<div className="flex justify-end space-x-2">
|
||||
<button
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-3 py-1 text-sm text-gray-600 hover:text-gray-800 border rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
onClick={handleConfirm}
|
||||
disabled={!selectedDate}
|
||||
className={`px-3 py-1 text-sm rounded ${
|
||||
selectedDate
|
||||
? 'bg-blue-500 text-white hover:bg-blue-600'
|
||||
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||
}`}
|
||||
className={`px-3 py-1 text-sm rounded ${selectedDate
|
||||
? 'bg-blue-500 text-white hover:bg-blue-600'
|
||||
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
@@ -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<DateTimeLocalPickerProps> = ({ coordinates,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${coordinates.x}px`,
|
||||
@@ -48,20 +48,19 @@ const DateTimeLocalPicker: React.FC<DateTimeLocalPickerProps> = ({ coordinates,
|
||||
autoFocus
|
||||
/>
|
||||
<div className="flex justify-end space-x-2">
|
||||
<button
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-3 py-1 text-sm text-gray-600 hover:text-gray-800 border rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
onClick={handleConfirm}
|
||||
disabled={!selectedDateTime}
|
||||
className={`px-3 py-1 text-sm rounded ${
|
||||
selectedDateTime
|
||||
? 'bg-blue-500 text-white hover:bg-blue-600'
|
||||
className={`px-3 py-1 text-sm rounded ${selectedDateTime
|
||||
? 'bg-blue-500 text-white hover:bg-blue-600'
|
||||
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
@@ -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 (
|
||||
<div
|
||||
className="fixed inset-0"
|
||||
<div
|
||||
className="fixed inset-0"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
<div
|
||||
style={containerStyle}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
@@ -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 (
|
||||
<div
|
||||
className="fixed inset-0"
|
||||
<div
|
||||
className="fixed inset-0"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
<div
|
||||
style={containerStyle}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
@@ -109,7 +109,7 @@ const TimePicker = ({ coordinates, selector, onClose }: TimePickerProps) => {
|
||||
</div>
|
||||
|
||||
{/* Minutes column */}
|
||||
<div style={{...columnStyle, borderRight: 'none'}}>
|
||||
<div style={{ ...columnStyle, borderRight: 'none' }}>
|
||||
{minutes.map((minute) => (
|
||||
<div
|
||||
key={minute}
|
||||
@@ -1,8 +1,27 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { styled } from '@mui/system';
|
||||
import { Alert, AlertTitle, TextField, Button, Switch, FormControlLabel, Box, Typography, Tabs, Tab, Table, TableContainer, TableHead, TableRow, TableBody, TableCell, Paper } from '@mui/material';
|
||||
import {
|
||||
Alert,
|
||||
AlertTitle,
|
||||
TextField,
|
||||
Button,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
Box,
|
||||
Typography,
|
||||
Tabs,
|
||||
Tab,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableBody,
|
||||
TableCell,
|
||||
Paper
|
||||
} from '@mui/material';
|
||||
import { sendProxyConfig, getProxyConfig, testProxyConfig, deleteProxyConfig } from '../../api/proxy';
|
||||
import { useGlobalInfoStore } from '../../context/globalInfo';
|
||||
import { useThemeMode } from '../../context/theme-provider';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const FormContainer = styled(Box)({
|
||||
@@ -134,6 +153,9 @@ const ProxyForm: React.FC = () => {
|
||||
fetchProxyConfig();
|
||||
}, []);
|
||||
|
||||
const theme = useThemeMode();
|
||||
const isDarkMode = theme.darkMode;
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormContainer>
|
||||
@@ -144,6 +166,7 @@ const ProxyForm: React.FC = () => {
|
||||
<Tab label={t('proxy.tab_standard')} />
|
||||
<Tab label={t('proxy.tab_rotation')} />
|
||||
</Tabs>
|
||||
|
||||
{tabIndex === 0 && (
|
||||
isProxyConfigured ? (
|
||||
<Box sx={{ maxWidth: 600, width: '100%', marginTop: '5px' }}>
|
||||
@@ -236,14 +259,20 @@ const ProxyForm: React.FC = () => {
|
||||
<Typography variant="body1" gutterBottom component="div">
|
||||
{t('proxy.coming_soon')}
|
||||
</Typography>
|
||||
|
||||
{/* <Button variant="contained" color="primary" sx={{ marginTop: '20px',backgroundColor: '#ff00c3' }}>
|
||||
<a style={{ color: 'white', textDecoration: 'none' }} href="https://forms.gle/hXjgqDvkEhPcaBW76">Join Maxun Cloud Waitlist</a> */}
|
||||
|
||||
<Button variant="contained" color="primary" sx={{ marginTop: '20px' }}>
|
||||
<a style={{ color: 'white', textDecoration: 'none' }} href="https://forms.gle/hXjgqDvkEhPcaBW76">{t('proxy.join_waitlist')}</a>
|
||||
|
||||
</Button>
|
||||
</>
|
||||
</Box>
|
||||
)}
|
||||
</FormContainer>
|
||||
<Alert severity="info" sx={{ marginTop: '80px', marginLeft: '50px', height: '230px', width: '450px', border: '1px solid #ff00c3' }}>
|
||||
|
||||
<Alert severity="info" sx={{ marginTop: '80px', marginLeft: '50px', height: '250px', width: '600px', border: '1px solid #ff00c3' }}>
|
||||
<AlertTitle>{t('proxy.alert.title')}</AlertTitle>
|
||||
<br />
|
||||
<b>{t('proxy.alert.right_way')}</b>
|
||||
@@ -257,6 +286,7 @@ const ProxyForm: React.FC = () => {
|
||||
<br />
|
||||
<b>{t('proxy.alert.wrong_way')}</b>
|
||||
<br />
|
||||
|
||||
{t('proxy.alert.proxy_url')} http://myusername:mypassword@proxy.com:1337
|
||||
</Alert>
|
||||
</>
|
||||
134
src/components/recorder/AddWhatCondModal.tsx
Normal file
134
src/components/recorder/AddWhatCondModal.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import { WhereWhatPair } from "maxun-core";
|
||||
import { GenericModal } from "../ui/GenericModal";
|
||||
import { modalStyle } from "./AddWhereCondModal";
|
||||
import { Button, MenuItem, TextField, Typography } from "@mui/material";
|
||||
import React, { useRef } from "react";
|
||||
import { Dropdown as MuiDropdown } from "../ui/DropdownMui";
|
||||
import { KeyValueForm } from "./KeyValueForm";
|
||||
import { ClearButton } from "../ui/buttons/ClearButton";
|
||||
import { useSocketStore } from "../../context/socket";
|
||||
|
||||
interface AddWhatCondModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
pair: WhereWhatPair;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export const AddWhatCondModal = ({ isOpen, onClose, pair, index }: AddWhatCondModalProps) => {
|
||||
const [action, setAction] = React.useState<string>('');
|
||||
const [objectIndex, setObjectIndex] = React.useState<number>(0);
|
||||
const [args, setArgs] = React.useState<({ type: string, value: (string | number | object | unknown) })[]>([]);
|
||||
|
||||
const objectRefs = useRef<({ getObject: () => object } | unknown)[]>([]);
|
||||
|
||||
const { socket } = useSocketStore();
|
||||
|
||||
const handleSubmit = () => {
|
||||
const argsArray: (string | number | object | unknown)[] = [];
|
||||
args.map((arg, index) => {
|
||||
switch (arg.type) {
|
||||
case 'string':
|
||||
case 'number':
|
||||
argsArray[index] = arg.value;
|
||||
break;
|
||||
case 'object':
|
||||
// @ts-ignore
|
||||
argsArray[index] = objectRefs.current[arg.value].getObject();
|
||||
}
|
||||
})
|
||||
setArgs([]);
|
||||
onClose();
|
||||
pair.what.push({
|
||||
// @ts-ignore
|
||||
action,
|
||||
args: argsArray,
|
||||
})
|
||||
socket?.emit('updatePair', { index: index - 1, pair: pair });
|
||||
}
|
||||
|
||||
return (
|
||||
<GenericModal isOpen={isOpen} onClose={() => {
|
||||
setArgs([]);
|
||||
onClose();
|
||||
}} modalStyle={modalStyle}>
|
||||
<div>
|
||||
<Typography sx={{ margin: '20px 0px' }}>Add what condition:</Typography>
|
||||
<div style={{ margin: '8px' }}>
|
||||
<Typography>Action:</Typography>
|
||||
<TextField
|
||||
size='small'
|
||||
type="string"
|
||||
onChange={(e) => setAction(e.target.value)}
|
||||
value={action}
|
||||
label='action'
|
||||
/>
|
||||
<div>
|
||||
<Typography>Add new argument of type:</Typography>
|
||||
<Button onClick={() => setArgs([...args, { type: 'string', value: null }])}>string</Button>
|
||||
<Button onClick={() => setArgs([...args, { type: 'number', value: null }])}>number</Button>
|
||||
<Button onClick={() => {
|
||||
setArgs([...args, { type: 'object', value: objectIndex }])
|
||||
setObjectIndex(objectIndex + 1);
|
||||
}}>object</Button>
|
||||
</div>
|
||||
<Typography>args:</Typography>
|
||||
{args.map((arg, index) => {
|
||||
// @ts-ignore
|
||||
return (
|
||||
<div style={{ border: 'solid 1px gray', padding: '10px', display: 'flex', flexDirection: 'row', alignItems: 'center' }}
|
||||
key={`wrapper-for-${arg.type}-${index}`}>
|
||||
<ClearButton handleClick={() => {
|
||||
args.splice(index, 1);
|
||||
setArgs([...args]);
|
||||
}} />
|
||||
<Typography sx={{ margin: '5px' }} key={`number-argument-${arg.type}-${index}`}>{index}: </Typography>
|
||||
{arg.type === 'string' ?
|
||||
<TextField
|
||||
size='small'
|
||||
type="string"
|
||||
onChange={(e) => setArgs([
|
||||
...args.slice(0, index),
|
||||
{ type: arg.type, value: e.target.value },
|
||||
...args.slice(index + 1)
|
||||
])}
|
||||
value={args[index].value || ''}
|
||||
label="string"
|
||||
key={`arg-${arg.type}-${index}`}
|
||||
/> : arg.type === 'number' ?
|
||||
<TextField
|
||||
key={`arg-${arg.type}-${index}`}
|
||||
size='small'
|
||||
type="number"
|
||||
onChange={(e) => setArgs([
|
||||
...args.slice(0, index),
|
||||
{ type: arg.type, value: Number(e.target.value) },
|
||||
...args.slice(index + 1)
|
||||
])}
|
||||
value={args[index].value || ''}
|
||||
label="number"
|
||||
/> :
|
||||
<KeyValueForm ref={el =>
|
||||
//@ts-ignore
|
||||
objectRefs.current[arg.value] = el} key={`arg-${arg.type}-${index}`} />
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
display: "table-cell",
|
||||
float: "right",
|
||||
marginRight: "15px",
|
||||
marginTop: "20px",
|
||||
}}
|
||||
>
|
||||
{"Add Condition"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</GenericModal>
|
||||
)
|
||||
}
|
||||
152
src/components/recorder/AddWhereCondModal.tsx
Normal file
152
src/components/recorder/AddWhereCondModal.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import { Dropdown as MuiDropdown } from "../ui/DropdownMui";
|
||||
import {
|
||||
Button,
|
||||
MenuItem,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
import React, { useRef } from "react";
|
||||
import { GenericModal } from "../ui/GenericModal";
|
||||
import { WhereWhatPair } from "maxun-core";
|
||||
import { SelectChangeEvent } from "@mui/material/Select/Select";
|
||||
import { DisplayConditionSettings } from "./DisplayWhereConditionSettings";
|
||||
import { useSocketStore } from "../../context/socket";
|
||||
|
||||
interface AddWhereCondModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
pair: WhereWhatPair;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export const AddWhereCondModal = ({ isOpen, onClose, pair, index }: AddWhereCondModalProps) => {
|
||||
const [whereProp, setWhereProp] = React.useState<string>('');
|
||||
const [additionalSettings, setAdditionalSettings] = React.useState<string>('');
|
||||
const [newValue, setNewValue] = React.useState<any>('');
|
||||
const [checked, setChecked] = React.useState<boolean[]>(new Array(Object.keys(pair.where).length).fill(false));
|
||||
|
||||
const keyValueFormRef = useRef<{ getObject: () => object }>(null);
|
||||
|
||||
const { socket } = useSocketStore();
|
||||
|
||||
const handlePropSelect = (event: SelectChangeEvent<string>) => {
|
||||
setWhereProp(event.target.value);
|
||||
switch (event.target.value) {
|
||||
case 'url': setNewValue(''); break;
|
||||
case 'selectors': setNewValue(['']); break;
|
||||
case 'default': return;
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
switch (whereProp) {
|
||||
case 'url':
|
||||
if (additionalSettings === 'string') {
|
||||
pair.where.url = newValue;
|
||||
} else {
|
||||
pair.where.url = { $regex: newValue };
|
||||
}
|
||||
break;
|
||||
case 'selectors':
|
||||
pair.where.selectors = newValue;
|
||||
break;
|
||||
case 'cookies':
|
||||
pair.where.cookies = keyValueFormRef.current?.getObject() as Record<string, string>
|
||||
break;
|
||||
case 'before':
|
||||
pair.where.$before = newValue;
|
||||
break;
|
||||
case 'after':
|
||||
pair.where.$after = newValue;
|
||||
break;
|
||||
case 'boolean':
|
||||
const booleanArr = [];
|
||||
const deleteKeys: string[] = [];
|
||||
for (let i = 0; i < checked.length; i++) {
|
||||
if (checked[i]) {
|
||||
if (Object.keys(pair.where)[i]) {
|
||||
//@ts-ignore
|
||||
if (pair.where[Object.keys(pair.where)[i]]) {
|
||||
booleanArr.push({
|
||||
//@ts-ignore
|
||||
[Object.keys(pair.where)[i]]: pair.where[Object.keys(pair.where)[i]]
|
||||
});
|
||||
}
|
||||
deleteKeys.push(Object.keys(pair.where)[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
deleteKeys.forEach((key: string) => delete pair.where[key]);
|
||||
//@ts-ignore
|
||||
pair.where[`$${additionalSettings}`] = booleanArr;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
onClose();
|
||||
setWhereProp('');
|
||||
setAdditionalSettings('');
|
||||
setNewValue('');
|
||||
socket?.emit('updatePair', { index: index - 1, pair: pair });
|
||||
}
|
||||
|
||||
return (
|
||||
<GenericModal isOpen={isOpen} onClose={() => {
|
||||
setWhereProp('');
|
||||
setAdditionalSettings('');
|
||||
setNewValue('');
|
||||
onClose();
|
||||
}} modalStyle={modalStyle}>
|
||||
<div>
|
||||
<Typography sx={{ margin: '20px 0px' }}>Add where condition:</Typography>
|
||||
<div style={{ margin: '8px' }}>
|
||||
<MuiDropdown
|
||||
id="whereProp"
|
||||
label="Condition"
|
||||
value={whereProp}
|
||||
handleSelect={handlePropSelect}>
|
||||
<MenuItem value="url">url</MenuItem>
|
||||
<MenuItem value="selectors">selectors</MenuItem>
|
||||
<MenuItem value="cookies">cookies</MenuItem>
|
||||
<MenuItem value="before">before</MenuItem>
|
||||
<MenuItem value="after">after</MenuItem>
|
||||
<MenuItem value="boolean">boolean logic</MenuItem>
|
||||
</MuiDropdown>
|
||||
</div>
|
||||
{whereProp ?
|
||||
<div style={{ margin: '8px' }}>
|
||||
<DisplayConditionSettings
|
||||
whereProp={whereProp} additionalSettings={additionalSettings} setAdditionalSettings={setAdditionalSettings}
|
||||
newValue={newValue} setNewValue={setNewValue} checked={checked} setChecked={setChecked}
|
||||
keyValueFormRef={keyValueFormRef} whereKeys={Object.keys(pair.where)}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
display: "table-cell",
|
||||
float: "right",
|
||||
marginRight: "15px",
|
||||
marginTop: "20px",
|
||||
}}
|
||||
>
|
||||
{"Add Condition"}
|
||||
</Button>
|
||||
</div>
|
||||
: null}
|
||||
</div>
|
||||
</GenericModal>
|
||||
)
|
||||
}
|
||||
|
||||
export const modalStyle = {
|
||||
top: '40%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: '30%',
|
||||
backgroundColor: 'background.paper',
|
||||
p: 4,
|
||||
height: 'fit-content',
|
||||
display: 'block',
|
||||
padding: '20px',
|
||||
};
|
||||
@@ -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 = (
|
||||
<MenuItem value="string">string</MenuItem>
|
||||
<MenuItem value="regex">regex</MenuItem>
|
||||
</MuiDropdown>
|
||||
{ additionalSettings ? <TextField
|
||||
{additionalSettings ? <TextField
|
||||
size='small'
|
||||
type="string"
|
||||
onChange={(e) => setNewValue(e.target.value)}
|
||||
@@ -56,20 +56,20 @@ export const DisplayConditionSettings = (
|
||||
...newValue.slice(0, index),
|
||||
e.target.value,
|
||||
...newValue.slice(index + 1)
|
||||
])}/>
|
||||
])} />
|
||||
})
|
||||
}
|
||||
</Stack>
|
||||
<AddButton handleClick={() => setNewValue([...newValue, ''])}/>
|
||||
<RemoveButton handleClick={()=> {
|
||||
<AddButton handleClick={() => setNewValue([...newValue, ''])} />
|
||||
<RemoveButton handleClick={() => {
|
||||
const arr = newValue;
|
||||
arr.splice(-1);
|
||||
setNewValue([...arr]);
|
||||
}}/>
|
||||
}} />
|
||||
</React.Fragment>
|
||||
)
|
||||
case 'cookies':
|
||||
return <KeyValueForm ref={keyValueFormRef}/>
|
||||
return <KeyValueForm ref={keyValueFormRef} />
|
||||
case 'before':
|
||||
return <TextField
|
||||
label='pair id'
|
||||
@@ -96,23 +96,23 @@ export const DisplayConditionSettings = (
|
||||
<MenuItem value="or">or</MenuItem>
|
||||
</MuiDropdown>
|
||||
<FormGroup>
|
||||
{
|
||||
whereKeys.map((key: string, index: number) => {
|
||||
return (
|
||||
<FormControlLabel control={
|
||||
<Checkbox
|
||||
checked={checked[index]}
|
||||
onChange={() => 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 (
|
||||
<FormControlLabel control={
|
||||
<Checkbox
|
||||
checked={checked[index]}
|
||||
onChange={() => setChecked([
|
||||
...checked.slice(0, index),
|
||||
!checked[index],
|
||||
...checked.slice(index + 1)
|
||||
])}
|
||||
key={`checkbox-${key}-${index}`}
|
||||
/>
|
||||
} label={key} key={`control-label-form-${key}-${index}`} />
|
||||
)
|
||||
})
|
||||
}
|
||||
</FormGroup>
|
||||
<WarningText>
|
||||
Choose at least 2 where conditions. Nesting of boolean operators
|
||||
@@ -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<number>(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 <KeyValuePair keyLabel={`key ${index + 1}`} valueLabel={`value ${index + 1}`} key={`keyValuePair-${index}`}
|
||||
//@ts-ignore
|
||||
ref={el => keyValuePairRefs.current[index] = el}/>
|
||||
//@ts-ignore
|
||||
ref={el => keyValuePairRefs.current[index] = el} />
|
||||
})
|
||||
}
|
||||
<AddButton handleClick={() => setNumberOfPairs(numberOfPairs + 1)} hoverEffect={false}/>
|
||||
<RemoveButton handleClick={() => setNumberOfPairs(numberOfPairs - 1)}/>
|
||||
<AddButton handleClick={() => setNumberOfPairs(numberOfPairs + 1)} hoverEffect={false} />
|
||||
<RemoveButton handleClick={() => setNumberOfPairs(numberOfPairs - 1)} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -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(
|
||||
@@ -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<number>(0);
|
||||
const [breakpoints, setBreakpoints] = React.useState<boolean[]>([]);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
@@ -67,12 +67,12 @@ export const LeftSidePanelContent = ({ workflow, updateWorkflow, recordingName,
|
||||
return (
|
||||
<div>
|
||||
<Tooltip title='Add pair' placement='left' arrow>
|
||||
<div style={{ float: 'right'}}>
|
||||
<div style={{ float: 'right' }}>
|
||||
<AddButton
|
||||
handleClick={handleAddPair}
|
||||
title=''
|
||||
hoverEffect={false}
|
||||
style={{color: 'white', background: '#1976d2'}}
|
||||
style={{ color: 'white', background: '#1976d2' }}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
@@ -86,20 +86,20 @@ export const LeftSidePanelContent = ({ workflow, updateWorkflow, recordingName,
|
||||
/>
|
||||
</GenericModal>
|
||||
<div>
|
||||
{
|
||||
workflow.workflow.map((pair, i, workflow, ) =>
|
||||
<Pair
|
||||
handleBreakpoint={() => handleBreakpointClick(i)}
|
||||
isActive={ activeId === i + 1}
|
||||
key={workflow.length - i}
|
||||
index={workflow.length - i}
|
||||
pair={pair}
|
||||
updateWorkflow={updateWorkflow}
|
||||
numberOfPairs={workflow.length}
|
||||
handleSelectPairForEdit={handleSelectPairForEdit}
|
||||
/>)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
workflow.workflow.map((pair, i, workflow,) =>
|
||||
<Pair
|
||||
handleBreakpoint={() => handleBreakpointClick(i)}
|
||||
isActive={activeId === i + 1}
|
||||
key={workflow.length - i}
|
||||
index={workflow.length - i}
|
||||
pair={pair}
|
||||
updateWorkflow={updateWorkflow}
|
||||
numberOfPairs={workflow.length}
|
||||
handleSelectPairForEdit={handleSelectPairForEdit}
|
||||
/>)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
86
src/components/recorder/LeftSidePanelSettings.tsx
Normal file
86
src/components/recorder/LeftSidePanelSettings.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import React from "react";
|
||||
import { Button, MenuItem, TextField, Typography } from "@mui/material";
|
||||
import { Dropdown } from "../ui/DropdownMui";
|
||||
import { RunSettings } from "../run/RunSettings";
|
||||
import { useSocketStore } from "../../context/socket";
|
||||
|
||||
interface LeftSidePanelSettingsProps {
|
||||
params: any[]
|
||||
settings: RunSettings,
|
||||
setSettings: (setting: RunSettings) => void
|
||||
}
|
||||
|
||||
export const LeftSidePanelSettings = ({ params, settings, setSettings }: LeftSidePanelSettingsProps) => {
|
||||
const { socket } = useSocketStore();
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
{params.length !== 0 && (
|
||||
<React.Fragment>
|
||||
<Typography>Parameters:</Typography>
|
||||
{params?.map((item: string, index: number) => {
|
||||
return <TextField
|
||||
sx={{ margin: '15px 0px' }}
|
||||
value={settings.params ? settings.params[item] : ''}
|
||||
key={`param-${index}`}
|
||||
type="string"
|
||||
label={item}
|
||||
required
|
||||
onChange={(e) => setSettings(
|
||||
{
|
||||
...settings,
|
||||
params: settings.params
|
||||
? {
|
||||
...settings.params,
|
||||
[item]: e.target.value,
|
||||
}
|
||||
: {
|
||||
[item]: e.target.value,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
})}
|
||||
</React.Fragment>
|
||||
)}
|
||||
<Typography sx={{ margin: '15px 0px' }}>Interpreter:</Typography>
|
||||
<TextField
|
||||
type="number"
|
||||
label="maxConcurrency"
|
||||
required
|
||||
onChange={(e) => setSettings(
|
||||
{
|
||||
...settings,
|
||||
maxConcurrency: parseInt(e.target.value),
|
||||
})}
|
||||
defaultValue={settings.maxConcurrency}
|
||||
/>
|
||||
<TextField
|
||||
sx={{ margin: '15px 0px' }}
|
||||
type="number"
|
||||
label="maxRepeats"
|
||||
required
|
||||
onChange={(e) => setSettings(
|
||||
{
|
||||
...settings,
|
||||
maxRepeats: parseInt(e.target.value),
|
||||
})}
|
||||
defaultValue={settings.maxRepeats}
|
||||
/>
|
||||
<Dropdown
|
||||
id="debug"
|
||||
label="debug"
|
||||
value={settings.debug?.toString()}
|
||||
handleSelect={(e) => setSettings(
|
||||
{
|
||||
...settings,
|
||||
debug: e.target.value === "true",
|
||||
})}
|
||||
>
|
||||
<MenuItem value="true">true</MenuItem>
|
||||
<MenuItem value="false">false</MenuItem>
|
||||
</Dropdown>
|
||||
<Button sx={{ margin: '15px 0px' }} variant='contained'
|
||||
onClick={() => socket?.emit('settings', settings)}>change</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<PairProps> = (
|
||||
};
|
||||
|
||||
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<PairProps> = (
|
||||
return (
|
||||
<PairWrapper isActive={isActive}>
|
||||
<Stack direction="row">
|
||||
<div style={{display: 'flex', maxWidth:'20px', alignItems:'center', justifyContent: 'center', }}>
|
||||
{isActive ? <LoadingButton loading variant="text"/>
|
||||
: breakpoint ? <BreakpointButton changeColor={true} handleClick={handleBreakpointClick}/>
|
||||
: <BreakpointButton handleClick={handleBreakpointClick}/>
|
||||
<div style={{ display: 'flex', maxWidth: '20px', alignItems: 'center', justifyContent: 'center', }}>
|
||||
{isActive ? <LoadingButton loading variant="text" />
|
||||
: breakpoint ? <BreakpointButton changeColor={true} handleClick={handleBreakpointClick} />
|
||||
: <BreakpointButton handleClick={handleBreakpointClick} />
|
||||
}
|
||||
</div>
|
||||
<Badge badgeContent={pair.what.length} color="primary">
|
||||
@@ -92,53 +92,53 @@ export const Pair: FC<PairProps> = (
|
||||
fontSize: '1rem',
|
||||
textTransform: 'none',
|
||||
}} variant='text' key={`pair-${index}`}
|
||||
onClick={() => handleSelectPairForEdit(pair, index)}>
|
||||
onClick={() => handleSelectPairForEdit(pair, index)}>
|
||||
index: {index}
|
||||
</Button>
|
||||
</Badge>
|
||||
<Stack direction="row" spacing={0}
|
||||
sx={{
|
||||
color: 'inherit',
|
||||
"&:hover": {
|
||||
sx={{
|
||||
color: 'inherit',
|
||||
}
|
||||
}}>
|
||||
"&:hover": {
|
||||
color: 'inherit',
|
||||
}
|
||||
}}>
|
||||
<Tooltip title="View" placement='right' arrow>
|
||||
<div>
|
||||
<ViewButton
|
||||
handleClick={handleOpen}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ViewButton
|
||||
handleClick={handleOpen}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Raw edit" placement='right' arrow>
|
||||
<div>
|
||||
<EditButton
|
||||
handleClick={() => {
|
||||
enableEdit();
|
||||
handleOpen();
|
||||
}}
|
||||
/>
|
||||
<EditButton
|
||||
handleClick={() => {
|
||||
enableEdit();
|
||||
handleOpen();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete" placement='right' arrow>
|
||||
<div>
|
||||
<ClearButton handleClick={handleDelete}/>
|
||||
<ClearButton handleClick={handleDelete} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<GenericModal isOpen={open} onClose={handleClose}>
|
||||
{ edit
|
||||
{edit
|
||||
?
|
||||
<PairEditForm
|
||||
onSubmitOfPair={handleEdit}
|
||||
numberOfPairs={numberOfPairs}
|
||||
index={index.toString()}
|
||||
where={pair.where ? JSON.stringify(pair.where) : undefined}
|
||||
what={pair.what ? JSON.stringify(pair.what) : undefined}
|
||||
id={pair.id}
|
||||
/>
|
||||
<PairEditForm
|
||||
onSubmitOfPair={handleEdit}
|
||||
numberOfPairs={numberOfPairs}
|
||||
index={index.toString()}
|
||||
where={pair.where ? JSON.stringify(pair.where) : undefined}
|
||||
what={pair.what ? JSON.stringify(pair.what) : undefined}
|
||||
id={pair.id}
|
||||
/>
|
||||
:
|
||||
<div>
|
||||
<PairDisplayDiv
|
||||
@@ -149,26 +149,26 @@ export const Pair: FC<PairProps> = (
|
||||
}
|
||||
</GenericModal>
|
||||
</PairWrapper>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
interface ViewButtonProps {
|
||||
handleClick: () => void;
|
||||
}
|
||||
|
||||
const ViewButton = ({handleClick}: ViewButtonProps) => {
|
||||
const ViewButton = ({ handleClick }: ViewButtonProps) => {
|
||||
return (
|
||||
<IconButton aria-label="add" size={"small"} onClick={handleClick}
|
||||
sx={{color: 'inherit', '&:hover': { color: '#1976d2', backgroundColor: 'transparent' }}}>
|
||||
<VisibilityIcon/>
|
||||
sx={{ color: 'inherit', '&:hover': { color: '#1976d2', backgroundColor: 'transparent' } }}>
|
||||
<VisibilityIcon />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
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'};
|
||||
}
|
||||
`;
|
||||
309
src/components/recorder/PairDetail.tsx
Normal file
309
src/components/recorder/PairDetail.tsx
Normal file
@@ -0,0 +1,309 @@
|
||||
import React, { useLayoutEffect, useRef, useState } from 'react';
|
||||
import { WhereWhatPair } from "maxun-core";
|
||||
import { Box, Button, IconButton, MenuItem, Stack, TextField, Tooltip, Typography } from "@mui/material";
|
||||
import { Close, KeyboardArrowDown, KeyboardArrowUp } from "@mui/icons-material";
|
||||
import TreeView from '@mui/lab/TreeView';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||
import TreeItem from '@mui/lab/TreeItem';
|
||||
import { AddButton } from "../ui/buttons/AddButton";
|
||||
import { WarningText } from "../ui/texts";
|
||||
import NotificationImportantIcon from '@mui/icons-material/NotificationImportant';
|
||||
import { RemoveButton } from "../ui/buttons/RemoveButton";
|
||||
import { AddWhereCondModal } from "./AddWhereCondModal";
|
||||
import { UpdatePair } from "../../api/workflow";
|
||||
import { useSocketStore } from "../../context/socket";
|
||||
import { AddWhatCondModal } from "./AddWhatCondModal";
|
||||
|
||||
interface PairDetailProps {
|
||||
pair: WhereWhatPair | null;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export const PairDetail = ({ pair, index }: PairDetailProps) => {
|
||||
const [pairIsSelected, setPairIsSelected] = useState(false);
|
||||
const [collapseWhere, setCollapseWhere] = useState(true);
|
||||
const [collapseWhat, setCollapseWhat] = useState(true);
|
||||
const [rerender, setRerender] = useState(false);
|
||||
const [expanded, setExpanded] = React.useState<string[]>(
|
||||
pair ? Object.keys(pair.where).map((key, index) => `${key}-${index}`) : []
|
||||
);
|
||||
const [addWhereCondOpen, setAddWhereCondOpen] = useState(false);
|
||||
const [addWhatCondOpen, setAddWhatCondOpen] = useState(false);
|
||||
|
||||
const { socket } = useSocketStore();
|
||||
|
||||
const handleCollapseWhere = () => {
|
||||
setCollapseWhere(!collapseWhere);
|
||||
}
|
||||
|
||||
const handleCollapseWhat = () => {
|
||||
setCollapseWhat(!collapseWhat);
|
||||
}
|
||||
|
||||
const handleToggle = (event: React.SyntheticEvent, nodeIds: string[]) => {
|
||||
setExpanded(nodeIds);
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (pair) {
|
||||
setPairIsSelected(true);
|
||||
}
|
||||
}, [pair])
|
||||
|
||||
const handleChangeValue = (value: any, where: boolean, keys: (string | number)[]) => {
|
||||
// a moving reference to internal objects within pair.where or pair.what
|
||||
let schema: any = where ? pair?.where : pair?.what;
|
||||
const length = keys.length;
|
||||
for (let i = 0; i < length - 1; i++) {
|
||||
const elem = keys[i];
|
||||
if (!schema[elem]) schema[elem] = {}
|
||||
schema = schema[elem];
|
||||
}
|
||||
|
||||
schema[keys[length - 1]] = value;
|
||||
if (pair && socket) {
|
||||
socket.emit('updatePair', { index: index - 1, pair: pair });
|
||||
}
|
||||
setRerender(!rerender);
|
||||
}
|
||||
|
||||
|
||||
const DisplayValueContent = (value: any, keys: (string | number)[], where: boolean = true) => {
|
||||
switch (typeof (value)) {
|
||||
case 'string':
|
||||
return <TextField
|
||||
size='small'
|
||||
type="string"
|
||||
onChange={(e) => {
|
||||
try {
|
||||
const obj = JSON.parse(e.target.value);
|
||||
handleChangeValue(obj, where, keys);
|
||||
} catch (error) {
|
||||
const num = Number(e.target.value);
|
||||
if (!isNaN(num)) {
|
||||
handleChangeValue(num, where, keys);
|
||||
}
|
||||
handleChangeValue(e.target.value, where, keys)
|
||||
}
|
||||
}}
|
||||
defaultValue={value}
|
||||
key={`text-field-${keys.join('-')}-${where}`}
|
||||
/>
|
||||
case 'number':
|
||||
return <TextField
|
||||
size='small'
|
||||
type="number"
|
||||
onChange={(e) => handleChangeValue(Number(e.target.value), where, keys)}
|
||||
defaultValue={value}
|
||||
key={`text-field-${keys.join('-')}-${where}`}
|
||||
/>
|
||||
case 'object':
|
||||
if (value) {
|
||||
if (Array.isArray(value)) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{
|
||||
value.map((element, index) => {
|
||||
return DisplayValueContent(element, [...keys, index], where);
|
||||
})
|
||||
}
|
||||
<AddButton handleClick={() => {
|
||||
let prevValue: any = where ? pair?.where : pair?.what;
|
||||
for (const key of keys) {
|
||||
prevValue = prevValue[key];
|
||||
}
|
||||
handleChangeValue([...prevValue, ''], where, keys);
|
||||
setRerender(!rerender);
|
||||
}} hoverEffect={false} />
|
||||
<RemoveButton handleClick={() => {
|
||||
let prevValue: any = where ? pair?.where : pair?.what;
|
||||
for (const key of keys) {
|
||||
prevValue = prevValue[key];
|
||||
}
|
||||
prevValue.splice(-1);
|
||||
handleChangeValue(prevValue, where, keys);
|
||||
setRerender(!rerender);
|
||||
}} />
|
||||
</React.Fragment>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<TreeView
|
||||
defaultCollapseIcon={<ExpandMoreIcon />}
|
||||
defaultExpandIcon={<ChevronRightIcon />}
|
||||
sx={{ flexGrow: 1, overflowY: 'auto' }}
|
||||
key={`tree-view-nested-${keys.join('-')}-${where}`}
|
||||
>
|
||||
{
|
||||
Object.keys(value).map((key2, index) => {
|
||||
return (
|
||||
<TreeItem nodeId={`${key2}-${index}`} label={`${key2}:`} key={`${key2}-${index}`}>
|
||||
{DisplayValueContent(value[key2], [...keys, key2], where)}
|
||||
</TreeItem>
|
||||
)
|
||||
})
|
||||
}
|
||||
</TreeView>
|
||||
)
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{pair &&
|
||||
<React.Fragment>
|
||||
<AddWhatCondModal isOpen={addWhatCondOpen} onClose={() => setAddWhatCondOpen(false)}
|
||||
pair={pair} index={index} />
|
||||
<AddWhereCondModal isOpen={addWhereCondOpen} onClose={() => setAddWhereCondOpen(false)}
|
||||
pair={pair} index={index} />
|
||||
</React.Fragment>
|
||||
}
|
||||
{
|
||||
pairIsSelected
|
||||
? (
|
||||
<div style={{ padding: '10px', overflow: 'hidden' }}>
|
||||
<Typography>Pair number: {index}</Typography>
|
||||
<TextField
|
||||
size='small'
|
||||
label='id'
|
||||
onChange={(e) => {
|
||||
if (pair && socket) {
|
||||
socket.emit('updatePair', { index: index - 1, pair: pair });
|
||||
pair.id = e.target.value;
|
||||
}
|
||||
}}
|
||||
value={pair ? pair.id ? pair.id : '' : ''}
|
||||
/>
|
||||
<Stack spacing={0} direction='row' sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
background: 'lightGray',
|
||||
}}>
|
||||
<CollapseButton
|
||||
handleClick={handleCollapseWhere}
|
||||
isCollapsed={collapseWhere}
|
||||
/>
|
||||
<Typography>Where</Typography>
|
||||
<Tooltip title='Add where condition' placement='right'>
|
||||
<div>
|
||||
<AddButton handleClick={() => {
|
||||
setAddWhereCondOpen(true);
|
||||
}} style={{ color: 'rgba(0, 0, 0, 0.54)', background: 'transparent' }} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
{(collapseWhere && pair && pair.where)
|
||||
?
|
||||
<React.Fragment>
|
||||
{Object.keys(pair.where).map((key, index) => {
|
||||
return (
|
||||
<TreeView
|
||||
expanded={expanded}
|
||||
defaultCollapseIcon={<ExpandMoreIcon />}
|
||||
defaultExpandIcon={<ChevronRightIcon />}
|
||||
sx={{ flexGrow: 1, overflowY: 'auto' }}
|
||||
onNodeToggle={handleToggle}
|
||||
key={`tree-view-${key}-${index}`}
|
||||
>
|
||||
<TreeItem nodeId={`${key}-${index}`} label={`${key}:`} key={`${key}-${index}`}>
|
||||
{
|
||||
// @ts-ignore
|
||||
DisplayValueContent(pair.where[key], [key])
|
||||
}
|
||||
</TreeItem>
|
||||
</TreeView>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
: null
|
||||
}
|
||||
<Stack spacing={0} direction='row' sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
background: 'lightGray',
|
||||
}}>
|
||||
<CollapseButton
|
||||
handleClick={handleCollapseWhat}
|
||||
isCollapsed={collapseWhat}
|
||||
/>
|
||||
<Typography>What</Typography>
|
||||
|
||||
<Tooltip title='Add what condition' placement='right'>
|
||||
<div>
|
||||
<AddButton handleClick={() => {
|
||||
setAddWhatCondOpen(true);
|
||||
}} style={{ color: 'rgba(0, 0, 0, 0.54)', background: 'transparent' }} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
{(collapseWhat && pair && pair.what)
|
||||
? (
|
||||
<React.Fragment>
|
||||
{Object.keys(pair.what).map((key, index) => {
|
||||
return (
|
||||
<TreeView
|
||||
defaultCollapseIcon={<ExpandMoreIcon />}
|
||||
defaultExpandIcon={<ChevronRightIcon />}
|
||||
sx={{ flexGrow: 1, overflowY: 'auto' }}
|
||||
key={`tree-view-2-${key}-${index}`}
|
||||
>
|
||||
<TreeItem nodeId={`${key}-${index}`} label={`${pair.what[index].action}`}>
|
||||
{
|
||||
// @ts-ignore
|
||||
DisplayValueContent(pair.what[key], [key], false)
|
||||
}
|
||||
<Tooltip title='remove action' placement='left'>
|
||||
<div style={{ float: 'right' }}>
|
||||
<CloseButton handleClick={() => {
|
||||
//@ts-ignore
|
||||
pair.what.splice(key, 1);
|
||||
setRerender(!rerender);
|
||||
}} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</TreeItem>
|
||||
</TreeView>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
)
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
)
|
||||
: <WarningText>
|
||||
<NotificationImportantIcon color="warning" />
|
||||
No pair from the left side panel was selected.
|
||||
</WarningText>
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
interface CollapseButtonProps {
|
||||
handleClick: () => void;
|
||||
isCollapsed?: boolean;
|
||||
}
|
||||
|
||||
const CollapseButton = ({ handleClick, isCollapsed }: CollapseButtonProps) => {
|
||||
return (
|
||||
<IconButton aria-label="add" size={"small"} onClick={handleClick}>
|
||||
{isCollapsed ? <KeyboardArrowDown /> : <KeyboardArrowUp />}
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
|
||||
const CloseButton = ({ handleClick }: CollapseButtonProps) => {
|
||||
return (
|
||||
<IconButton aria-label="add" size={"small"} onClick={handleClick}
|
||||
sx={{ '&:hover': { color: '#1976d2', backgroundColor: 'white' } }}>
|
||||
<Close />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
@@ -56,30 +56,30 @@ export const PairEditForm: FC<PairEditFormProps> = (
|
||||
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<PairEditFormProps> = (
|
||||
} 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<PairEditFormProps> = (
|
||||
marginTop: "36px",
|
||||
}}
|
||||
>
|
||||
<Typography sx={{marginBottom:'30px'}} variant='h5'>Raw pair edit form:</Typography>
|
||||
<Typography sx={{ marginBottom: '30px' }} variant='h5'>Raw pair edit form:</Typography>
|
||||
<TextField sx={{
|
||||
display:"block",
|
||||
display: "block",
|
||||
marginBottom: "20px"
|
||||
}} id="index" label="Index" type="number"
|
||||
InputProps={{ inputProps: { min: 1 } }}
|
||||
InputLabelProps={{
|
||||
InputProps={{ inputProps: { min: 1 } }}
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
}} defaultValue={pairProps.index}
|
||||
onChange={handleInputChange}
|
||||
error={!!errors.index} helperText={errors.index}
|
||||
required
|
||||
onChange={handleInputChange}
|
||||
error={!!errors.index} helperText={errors.index}
|
||||
required
|
||||
/>
|
||||
<TextField sx={{
|
||||
marginBottom: "20px"
|
||||
}} id="id" label="Id" type="string"
|
||||
defaultValue={pairProps.id}
|
||||
onChange={handleInputChange}
|
||||
defaultValue={pairProps.id}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<TextField multiline sx={{marginBottom: "20px"}}
|
||||
id="where" label="Where" variant="outlined" onChange={handleInputChange}
|
||||
defaultValue={ where || '{"url":"","selectors":[""]}' }
|
||||
error={!!errors.where} helperText={errors.where}/>
|
||||
<TextField multiline sx={{marginBottom: "20px"}}
|
||||
id="what" label="What" variant="outlined" onChange={handleInputChange}
|
||||
defaultValue={ what || '[{"action":"","args":[""]}]' }
|
||||
error={!!errors.what} helperText={errors.what}/>
|
||||
<TextField multiline sx={{ marginBottom: "20px" }}
|
||||
id="where" label="Where" variant="outlined" onChange={handleInputChange}
|
||||
defaultValue={where || '{"url":"","selectors":[""]}'}
|
||||
error={!!errors.where} helperText={errors.where} />
|
||||
<TextField multiline sx={{ marginBottom: "20px" }}
|
||||
id="what" label="What" variant="outlined" onChange={handleInputChange}
|
||||
defaultValue={what || '[{"action":"","args":[""]}]'}
|
||||
error={!!errors.what} helperText={errors.what} />
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
@@ -3,7 +3,7 @@ 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 "../atoms/Box";
|
||||
import { SimpleBox } from "../ui/Box";
|
||||
import { WorkflowFile } from "maxun-core";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||
@@ -12,7 +12,7 @@ import { useBrowserSteps } from '../../context/browserSteps';
|
||||
import { useSocketStore } from '../../context/socket';
|
||||
import { ScreenshotSettings } from '../../shared/types';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import { SidePanelHeader } from '../molecules/SidePanelHeader';
|
||||
import { SidePanelHeader } from './SidePanelHeader';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import FormLabel from '@mui/material/FormLabel';
|
||||
@@ -21,7 +21,8 @@ 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 '../molecules/ActionDescriptionBox';
|
||||
import ActionDescriptionBox from '../action/ActionDescriptionBox';
|
||||
import { useThemeMode } from '../../context/theme-provider';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const fetchWorkflow = (id: string, callback: (response: WorkflowFile) => void) => {
|
||||
@@ -56,6 +57,8 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
const [showCaptureText, setShowCaptureText] = useState(true);
|
||||
const [hoverStates, setHoverStates] = useState<{ [id: string]: boolean }>({});
|
||||
const [browserStepIdList, setBrowserStepIdList] = useState<number[]>([]);
|
||||
const [isCaptureTextConfirmed, setIsCaptureTextConfirmed] = useState(false);
|
||||
const [isCaptureListConfirmed, setIsCaptureListConfirmed] = useState(false);
|
||||
|
||||
const { lastAction, notify, currentWorkflowActionsState, setCurrentWorkflowActionsState, resetInterpretationLog } = useGlobalInfoStore();
|
||||
const { getText, startGetText, stopGetText, getScreenshot, startGetScreenshot, stopGetScreenshot, getList, startGetList, stopGetList, startPaginationMode, stopPaginationMode, paginationType, updatePaginationType, limitType, customLimit, updateLimitType, updateCustomLimit, stopLimitMode, startLimitMode, captureStage, setCaptureStage } = useActionContext();
|
||||
@@ -130,6 +133,16 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
|
||||
const handlePairDelete = () => { }
|
||||
|
||||
const handleStartGetText = () => {
|
||||
setIsCaptureTextConfirmed(false);
|
||||
startGetText();
|
||||
}
|
||||
|
||||
const handleStartGetList = () => {
|
||||
setIsCaptureListConfirmed(false);
|
||||
startGetList();
|
||||
}
|
||||
|
||||
const handleTextLabelChange = (id: number, label: string, listId?: number, fieldKey?: string) => {
|
||||
if (listId !== undefined && fieldKey !== undefined) {
|
||||
// Prevent editing if the field is confirmed
|
||||
@@ -169,6 +182,22 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
});
|
||||
};
|
||||
|
||||
const handleTextStepDelete = (id: number) => {
|
||||
deleteBrowserStep(id);
|
||||
setTextLabels(prevLabels => {
|
||||
const { [id]: _, ...rest } = prevLabels;
|
||||
return rest;
|
||||
});
|
||||
setConfirmedTextSteps(prev => {
|
||||
const { [id]: _, ...rest } = prev;
|
||||
return rest;
|
||||
});
|
||||
setErrors(prevErrors => {
|
||||
const { [id]: _, ...rest } = prevErrors;
|
||||
return rest;
|
||||
});
|
||||
};
|
||||
|
||||
const handleListTextFieldConfirm = (listId: number, fieldKey: string) => {
|
||||
setConfirmedListTextFields(prev => ({
|
||||
...prev,
|
||||
@@ -195,13 +224,29 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
});
|
||||
};
|
||||
|
||||
const handleListTextFieldDelete = (listId: number, fieldKey: string) => {
|
||||
removeListTextField(listId, fieldKey);
|
||||
setConfirmedListTextFields(prev => {
|
||||
const updatedListFields = { ...(prev[listId] || {}) };
|
||||
delete updatedListFields[fieldKey];
|
||||
return {
|
||||
...prev,
|
||||
[listId]: updatedListFields
|
||||
};
|
||||
});
|
||||
setErrors(prev => {
|
||||
const { [fieldKey]: _, ...rest } = prev;
|
||||
return rest;
|
||||
});
|
||||
};
|
||||
|
||||
const getTextSettingsObject = useCallback(() => {
|
||||
const settings: Record<string, { selector: string; tag?: string;[key: string]: any }> = {};
|
||||
browserSteps.forEach(step => {
|
||||
if (browserStepIdList.includes(step.id)) {
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (step.type === 'text' && step.label && step.selectorObj?.selector) {
|
||||
settings[step.label] = step.selectorObj;
|
||||
}
|
||||
@@ -224,6 +269,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
if (hasTextSteps) {
|
||||
socket?.emit('action', { action: 'scrapeSchema', settings });
|
||||
}
|
||||
setIsCaptureTextConfirmed(true);
|
||||
resetInterpretationLog();
|
||||
onFinishCapture();
|
||||
}, [stopGetText, getTextSettingsObject, socket, browserSteps, confirmedTextSteps, resetInterpretationLog]);
|
||||
@@ -326,6 +372,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
}
|
||||
stopLimitMode();
|
||||
setShowLimitOptions(false);
|
||||
setIsCaptureListConfirmed(true);
|
||||
stopCaptureAndEmitGetListSettings();
|
||||
setCaptureStage('complete');
|
||||
break;
|
||||
@@ -336,6 +383,23 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
}
|
||||
}, [captureStage, paginationType, limitType, customLimit, startPaginationMode, stopPaginationMode, startLimitMode, stopLimitMode, notify, stopCaptureAndEmitGetListSettings, getListSettingsObject]);
|
||||
|
||||
const handleBackCaptureList = useCallback(() => {
|
||||
switch (captureStage) {
|
||||
case 'limit':
|
||||
stopLimitMode();
|
||||
setShowLimitOptions(false);
|
||||
startPaginationMode();
|
||||
setShowPaginationOptions(true);
|
||||
setCaptureStage('pagination');
|
||||
break;
|
||||
case 'pagination':
|
||||
stopPaginationMode();
|
||||
setShowPaginationOptions(false);
|
||||
setCaptureStage('initial');
|
||||
break;
|
||||
}
|
||||
}, [captureStage, stopLimitMode, startPaginationMode, stopPaginationMode]);
|
||||
|
||||
const handlePaginationSettingSelect = (option: PaginationType) => {
|
||||
updatePaginationType(option);
|
||||
};
|
||||
@@ -350,6 +414,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
setTextLabels({});
|
||||
setErrors({});
|
||||
setConfirmedTextSteps({});
|
||||
setIsCaptureTextConfirmed(false);
|
||||
notify('error', t('right_panel.errors.capture_text_discarded'));
|
||||
}, [browserSteps, stopGetText, deleteBrowserStep]);
|
||||
|
||||
@@ -365,6 +430,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
setShowLimitOptions(false);
|
||||
setCaptureStage('initial');
|
||||
setConfirmedListTextFields({});
|
||||
setIsCaptureListConfirmed(false);
|
||||
notify('error', t('right_panel.errors.capture_list_discarded'));
|
||||
}, [browserSteps, stopGetList, deleteBrowserStep, resetListState]);
|
||||
|
||||
@@ -386,50 +452,125 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ 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 (
|
||||
<Paper sx={{ height: '520px', width: 'auto', alignItems: "center", background: 'inherit' }} id="browser-actions" elevation={0}>
|
||||
{/* <SimpleBox height={60} width='100%' background='lightGray' radius='0%'>
|
||||
<Typography sx={{ padding: '10px' }}>Last action: {` ${lastAction}`}</Typography>
|
||||
</SimpleBox> */}
|
||||
<ActionDescriptionBox />
|
||||
<ActionDescriptionBox isDarkMode={isDarkMode} />
|
||||
<Box display="flex" flexDirection="column" gap={2} style={{ margin: '13px' }}>
|
||||
{!getText && !getScreenshot && !getList && showCaptureList && <Button variant="contained" onClick={startGetList}>{t('right_panel.buttons.capture_list')}</Button>}
|
||||
|
||||
{getList && (
|
||||
<>
|
||||
<Box display="flex" justifyContent="space-between" gap={2} style={{ margin: '15px' }}>
|
||||
{(captureStage === 'pagination' || captureStage === 'limit') && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleBackCaptureList}
|
||||
sx={{
|
||||
color: '#ff00c3 !important',
|
||||
borderColor: '#ff00c3 !important',
|
||||
backgroundColor: 'whitesmoke !important',
|
||||
}}
|
||||
>
|
||||
{t('right_panel.buttons.back')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
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')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={discardGetList}
|
||||
sx={{
|
||||
color: 'red !important',
|
||||
borderColor: 'red !important',
|
||||
backgroundColor: 'whitesmoke !important',
|
||||
}} >
|
||||
{t('right_panel.buttons.discard')}
|
||||
</Button>
|
||||
<Button variant="outlined" color="error" onClick={discardGetList}>{t('right_panel.buttons.discard')}</Button>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{showPaginationOptions && (
|
||||
<Box display="flex" flexDirection="column" gap={2} style={{ margin: '13px' }}>
|
||||
<Typography>{t('right_panel.pagination.title')}</Typography>
|
||||
<Button variant={paginationType === 'clickNext' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('clickNext')}>{t('right_panel.pagination.click_next')}</Button>
|
||||
<Button variant={paginationType === 'clickLoadMore' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('clickLoadMore')}>{t('right_panel.pagination.click_load_more')}</Button>
|
||||
<Button variant={paginationType === 'scrollDown' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('scrollDown')}>{t('right_panel.pagination.scroll_down')}</Button>
|
||||
<Button variant={paginationType === 'scrollUp' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('scrollUp')}>{t('right_panel.pagination.scroll_up')}</Button>
|
||||
<Button variant={paginationType === 'none' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('none')}>{t('right_panel.pagination.none')}</Button>
|
||||
<Button
|
||||
variant={paginationType === 'clickNext' ? "contained" : "outlined"}
|
||||
onClick={() => 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')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={paginationType === 'clickLoadMore' ? "contained" : "outlined"}
|
||||
onClick={() => handlePaginationSettingSelect('clickLoadMore')}
|
||||
sx={{
|
||||
color: paginationType === 'clickLoadMore' ? 'whitesmoke !important' : '#ff00c3 !important',
|
||||
borderColor: '#ff00c3 !important',
|
||||
backgroundColor: paginationType === 'clickLoadMore' ? '#ff00c3 !important' : 'whitesmoke !important',
|
||||
}}>
|
||||
{t('right_panel.pagination.click_load_more')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={paginationType === 'scrollDown' ? "contained" : "outlined"}
|
||||
onClick={() => handlePaginationSettingSelect('scrollDown')}
|
||||
sx={{
|
||||
color: paginationType === 'scrollDown' ? 'whitesmoke !important' : '#ff00c3 !important',
|
||||
borderColor: '#ff00c3 !important',
|
||||
backgroundColor: paginationType === 'scrollDown' ? '#ff00c3 !important' : 'whitesmoke !important',
|
||||
}}>
|
||||
{t('right_panel.pagination.scroll_down')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={paginationType === 'scrollUp' ? "contained" : "outlined"}
|
||||
onClick={() => handlePaginationSettingSelect('scrollUp')}
|
||||
sx={{
|
||||
color: paginationType === 'scrollUp' ? 'whitesmoke !important' : '#ff00c3 !important',
|
||||
borderColor: '#ff00c3 !important',
|
||||
backgroundColor: paginationType === 'scrollUp' ? '#ff00c3 !important' : 'whitesmoke !important',
|
||||
}}>
|
||||
{t('right_panel.pagination.scroll_up')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={paginationType === 'none' ? "contained" : "outlined"}
|
||||
onClick={() => handlePaginationSettingSelect('none')}
|
||||
sx={{
|
||||
color: paginationType === 'none' ? 'whitesmoke !important' : '#ff00c3 !important',
|
||||
borderColor: '#ff00c3 !important',
|
||||
backgroundColor: paginationType === 'none' ? '#ff00c3 !important' : 'whitesmoke !important',
|
||||
}}>
|
||||
{t('right_panel.pagination.none')}</Button>
|
||||
</Box>
|
||||
)}
|
||||
{showLimitOptions && (
|
||||
@@ -452,45 +593,94 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
<FormControlLabel value="custom" control={<Radio />} label={t('right_panel.limit.custom')} />
|
||||
{limitType === 'custom' && (
|
||||
<TextField
|
||||
type="number"
|
||||
value={customLimit}
|
||||
onChange={(e) => 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
|
||||
}}
|
||||
type="number"
|
||||
value={customLimit}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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<HTMLInputElement>) => {
|
||||
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
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
)}
|
||||
{!getText && !getScreenshot && !getList && showCaptureText && <Button variant="contained" onClick={startGetText}>{t('right_panel.buttons.capture_text')}</Button>}
|
||||
{/* {!getText && !getScreenshot && !getList && showCaptureText && <Button variant="contained" sx={{backgroundColor:"#ff00c3",color:`${isDarkMode?'white':'black'}`}} onClick={startGetText}>{t('right_panel.buttons.capture_text')}</Button>} */}
|
||||
|
||||
{!getText && !getScreenshot && !getList && showCaptureText && <Button variant="contained" onClick={handleStartGetText}>{t('right_panel.buttons.capture_text')}</Button>}
|
||||
{getText &&
|
||||
<>
|
||||
<Box display="flex" justifyContent="space-between" gap={2} style={{ margin: '15px' }}>
|
||||
<Button variant="outlined" onClick={stopCaptureAndEmitGetTextSettings} >{t('right_panel.buttons.confirm')}</Button>
|
||||
<Button variant="outlined" color="error" onClick={discardGetText} >{t('right_panel.buttons.discard')}</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={stopCaptureAndEmitGetTextSettings}
|
||||
sx={{
|
||||
color: '#ff00c3 !important',
|
||||
borderColor: '#ff00c3 !important',
|
||||
backgroundColor: 'whitesmoke !important',
|
||||
}}>
|
||||
{t('right_panel.buttons.confirm')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={discardGetText}
|
||||
sx={{
|
||||
color: '#ff00c3 !important',
|
||||
borderColor: '#ff00c3 !important',
|
||||
backgroundColor: 'whitesmoke !important',
|
||||
}}>
|
||||
{t('right_panel.buttons.discard')}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
{/* {!getText && !getScreenshot && !getList && showCaptureScreenshot && <Button variant="contained" sx={{backgroundColor:"#ff00c3",color:`${isDarkMode?'white':'black'}`}} onClick={startGetScreenshot}>{t('right_panel.buttons.capture_screenshot')}</Button>} */}
|
||||
{!getText && !getScreenshot && !getList && showCaptureScreenshot && <Button variant="contained" onClick={startGetScreenshot}>{t('right_panel.buttons.capture_screenshot')}</Button>}
|
||||
{getScreenshot && (
|
||||
<Box display="flex" flexDirection="column" gap={2}>
|
||||
<Button variant="contained" onClick={() => captureScreenshot(true)}>{t('right_panel.screenshot.capture_fullpage')}</Button>
|
||||
<Button variant="contained" onClick={() => captureScreenshot(false)}>{t('right_panel.screenshot.capture_visible')}</Button>
|
||||
<Button variant="outlined" color="error" onClick={stopGetScreenshot}>{t('right_panel.buttons.discard')}</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={stopGetScreenshot}
|
||||
sx={{
|
||||
color: '#ff00c3 !important',
|
||||
borderColor: '#ff00c3 !important',
|
||||
backgroundColor: 'whitesmoke !important',
|
||||
}}>
|
||||
{t('right_panel.buttons.discard')}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
{browserSteps.map(step => (
|
||||
<Box key={step.id} onMouseEnter={() => handleMouseEnter(step.id)} onMouseLeave={() => handleMouseLeave(step.id)} sx={{ padding: '10px', margin: '11px', borderRadius: '5px', position: 'relative', background: 'white' }}>
|
||||
<Box key={step.id} onMouseEnter={() => handleMouseEnter(step.id)} onMouseLeave={() => handleMouseLeave(step.id)} sx={{ padding: '10px', margin: '11px', borderRadius: '5px', position: 'relative', background: isDarkMode ? "#1E2124" : 'white', color: isDarkMode ? "white" : 'black' }}>
|
||||
{
|
||||
step.type === 'text' && (
|
||||
<>
|
||||
@@ -511,6 +701,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
sx={{ background: isDarkMode ? "#1E2124" : 'white', color: isDarkMode ? "white" : 'black' }}
|
||||
/>
|
||||
<TextField
|
||||
label={t('right_panel.fields.data')}
|
||||
@@ -525,12 +716,23 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
|
||||
/>
|
||||
{!confirmedTextSteps[step.id] && (
|
||||
{!confirmedTextSteps[step.id] ? (
|
||||
<Box display="flex" justifyContent="space-between" gap={2}>
|
||||
<Button variant="contained" onClick={() => handleTextStepConfirm(step.id)} disabled={!textLabels[step.id]?.trim()}>{t('right_panel.buttons.confirm')}</Button>
|
||||
<Button variant="contained" color="error" onClick={() => handleTextStepDiscard(step.id)}>{t('right_panel.buttons.discard')}</Button>
|
||||
</Box>
|
||||
) : !isCaptureTextConfirmed && (
|
||||
<Box display="flex" justifyContent="flex-end" gap={2}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => handleTextStepDelete(step.id)}
|
||||
>
|
||||
{t('right_panel.buttons.delete')}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@@ -538,8 +740,8 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
<Box display="flex" alignItems="center">
|
||||
<DocumentScannerIcon sx={{ mr: 1 }} />
|
||||
<Typography>
|
||||
{step.fullPage ?
|
||||
t('right_panel.screenshot.display_fullpage') :
|
||||
{step.fullPage ?
|
||||
t('right_panel.screenshot.display_fullpage') :
|
||||
t('right_panel.screenshot.display_visible')}
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -548,7 +750,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
<>
|
||||
<Typography>{t('right_panel.messages.list_selected')}</Typography>
|
||||
{Object.entries(step.fields).map(([key, field]) => (
|
||||
<Box key={key}>
|
||||
<Box key={key} sx={{ background: `${isDarkMode ? "#1E2124" : 'white'}` }}>
|
||||
<TextField
|
||||
label={t('right_panel.fields.field_label')}
|
||||
value={field.label || ''}
|
||||
@@ -577,8 +779,9 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
|
||||
/>
|
||||
{!confirmedListTextFields[step.id]?.[key] && (
|
||||
{!confirmedListTextFields[step.id]?.[key] ? (
|
||||
<Box display="flex" justifyContent="space-between" gap={2}>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -595,6 +798,16 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
{t('right_panel.buttons.discard')}
|
||||
</Button>
|
||||
</Box>
|
||||
) : !isCaptureListConfirmed && (
|
||||
<Box display="flex" justifyContent="flex-end" gap={2}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => handleListTextFieldDelete(step.id, key)}
|
||||
>
|
||||
{t('right_panel.buttons.delete')}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
@@ -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 (
|
||||
<div>
|
||||
<Button onClick={() => setOpenModal(true)} variant="outlined" sx={{ marginRight: '20px' }} size="small" color="success">
|
||||
{/* <Button onClick={() => setOpenModal(true)} variant='contained' sx={{ marginRight: '20px',backgroundColor: '#ff00c3',color: 'white' }} size="small" color="success">
|
||||
Finish */}
|
||||
|
||||
<Button
|
||||
onClick={() => setOpenModal(true)}
|
||||
variant="outlined"
|
||||
color="success"
|
||||
sx={{
|
||||
marginRight: '20px',
|
||||
color: '#00c853 !important',
|
||||
borderColor: '#00c853 !important',
|
||||
backgroundColor: 'whitesmoke !important',
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
{t('right_panel.buttons.finish')}
|
||||
</Button>
|
||||
|
||||
@@ -101,12 +115,12 @@ export const SaveRecording = ({ fileName }: SaveRecordingProps) => {
|
||||
</Button>
|
||||
<WarningText>
|
||||
<NotificationImportantIcon color="warning" />
|
||||
{t('save_recording.warnings.robot_exists')}
|
||||
{t('save_recording.errors.exists_warning')}
|
||||
</WarningText>
|
||||
</React.Fragment>)
|
||||
: <Button type="submit" variant="contained" sx={{ marginTop: '10px' }}>
|
||||
{t('save_recording.buttons.save')}
|
||||
</Button>
|
||||
{t('save_recording.buttons.save')}
|
||||
</Button>
|
||||
}
|
||||
{waitingForSave &&
|
||||
<Tooltip title={t('save_recording.tooltips.optimizing')} placement={"bottom"}>
|
||||
@@ -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 = () => {
|
||||
@@ -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<HTMLCanvasElement>): 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);
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import { RecordingsTable } from "../molecules/RecordingsTable";
|
||||
import { RecordingsTable } from "./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';
|
||||
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';
|
||||
|
||||
interface RecordingsProps {
|
||||
handleEditRecording: (id: string, fileName: string) => void;
|
||||
@@ -15,7 +15,7 @@ interface RecordingsProps {
|
||||
setRecordingInfo: (id: string, name: string) => void;
|
||||
}
|
||||
|
||||
export const Recordings = ({ handleEditRecording, handleRunRecording, setRecordingInfo, handleScheduleRecording}: RecordingsProps) => {
|
||||
export const Recordings = ({ handleEditRecording, handleRunRecording, setRecordingInfo, handleScheduleRecording }: RecordingsProps) => {
|
||||
const [runSettingsAreOpen, setRunSettingsAreOpen] = useState(false);
|
||||
const [scheduleSettingsAreOpen, setScheduleSettingsAreOpen] = useState(false);
|
||||
const [integrateSettingsAreOpen, setIntegrateSettingsAreOpen] = useState(false);
|
||||
@@ -24,10 +24,10 @@ export const Recordings = ({ handleEditRecording, handleRunRecording, setRecordi
|
||||
const [robotDuplicateAreOpen, setRobotDuplicateAreOpen] = useState(false);
|
||||
const [params, setParams] = useState<string[]>([]);
|
||||
const [selectedRecordingId, setSelectedRecordingId] = useState<string>('');
|
||||
const handleIntegrateRecording = (id: string, settings: IntegrationSettings) => {};
|
||||
const handleSettingsRecording = (id: string, settings: RobotSettings) => {};
|
||||
const handleEditRobot = (id: string, settings: RobotSettings) => {};
|
||||
const handleDuplicateRobot = (id: string, settings: RobotSettings) => {};
|
||||
const 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) {
|
||||
@@ -169,9 +169,9 @@ export const Recordings = ({ handleEditRecording, handleRunRecording, setRecordi
|
||||
handleClose={handleRobotSettingsClose}
|
||||
handleStart={(settings) => handleSettingsRecording(selectedRecordingId, settings)}
|
||||
/>
|
||||
<RobotEditModal isOpen={robotEditAreOpen}
|
||||
<RobotEditModal isOpen={robotEditAreOpen}
|
||||
handleClose={handleRobotEditClose}
|
||||
handleStart={(settings) => handleEditRobot(selectedRecordingId,settings)}
|
||||
handleStart={(settings) => handleEditRobot(selectedRecordingId, settings)}
|
||||
/>
|
||||
<RobotDuplicationModal isOpen={robotDuplicateAreOpen}
|
||||
handleClose={handleRobotDuplicateClose}
|
||||
@@ -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:
|
||||
@@ -33,10 +33,6 @@ interface Column {
|
||||
format?: (value: string) => string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
interface Data {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -57,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<Data[]>([]);
|
||||
@@ -405,7 +401,7 @@ const OptionsButton = ({ handleEdit, handleDelete, handleDuplicate }: OptionsBut
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const {t} = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -441,7 +437,6 @@ const OptionsButton = ({ handleEdit, handleDelete, handleDuplicate }: OptionsBut
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('recordingtable.duplicate')}</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
@@ -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: '<code>producthunt.com/topics/api</code>',
|
||||
url2: '<code>producthunt.com/topics/database</code>'
|
||||
})
|
||||
}}/>
|
||||
}} />
|
||||
<br />
|
||||
<span>
|
||||
<b>{t('robot_duplication.descriptions.warning')}</b>
|
||||
@@ -152,7 +152,16 @@ export const RobotDuplicationModal = ({ isOpen, handleStart, handleClose, initia
|
||||
<Button variant="contained" color="primary" onClick={handleSave}>
|
||||
{t('robot_duplication.buttons.duplicate')}
|
||||
</Button>
|
||||
<Button onClick={handleClose} color="primary" variant="outlined" style={{ marginLeft: '10px' }}>
|
||||
<Button
|
||||
onClick={handleClose}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
style={{ marginLeft: '10px' }}
|
||||
sx={{
|
||||
color: '#ff00c3 !important',
|
||||
borderColor: '#ff00c3 !important',
|
||||
backgroundColor: 'whitesmoke !important',
|
||||
}} >
|
||||
{t('robot_duplication.buttons.cancel')}
|
||||
</Button>
|
||||
</Box>
|
||||
@@ -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
|
||||
<Button variant="contained" color="primary" onClick={handleSave}>
|
||||
{t('robot_edit.save')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleClose}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
<Button
|
||||
onClick={handleClose}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
style={{ marginLeft: '10px' }}
|
||||
>
|
||||
sx={{
|
||||
color: '#ff00c3 !important',
|
||||
borderColor: '#ff00c3 !important',
|
||||
backgroundColor: 'whitesmoke !important',
|
||||
}}>
|
||||
{t('robot_edit.cancel')}
|
||||
</Button>
|
||||
</Box>
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -79,12 +79,13 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initia
|
||||
'SUNDAY'
|
||||
];
|
||||
|
||||
const { recordingId } = useGlobalInfoStore();
|
||||
const { recordingId, notify } = useGlobalInfoStore();
|
||||
|
||||
const deleteRobotSchedule = () => {
|
||||
if (recordingId) {
|
||||
deleteSchedule(recordingId);
|
||||
setSchedule(null);
|
||||
notify('success', t('Schedule deleted successfully'));
|
||||
} else {
|
||||
console.error('No recording id provided');
|
||||
}
|
||||
@@ -122,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');
|
||||
@@ -272,7 +273,16 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initia
|
||||
<Button onClick={() => handleStart(settings)} variant="contained" color="primary">
|
||||
{t('schedule_settings.buttons.save_schedule')}
|
||||
</Button>
|
||||
<Button onClick={handleClose} color="primary" variant="outlined" style={{ marginLeft: '10px' }}>
|
||||
<Button
|
||||
onClick={handleClose}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
style={{ marginLeft: '10px' }}
|
||||
sx={{
|
||||
color: '#ff00c3 !important',
|
||||
borderColor: '#ff00c3 !important',
|
||||
backgroundColor: 'whitesmoke !important',
|
||||
}}>
|
||||
{t('schedule_settings.buttons.cancel')}
|
||||
</Button>
|
||||
</Box>
|
||||
@@ -8,8 +8,8 @@ interface ToggleButtonProps {
|
||||
|
||||
export const ToggleButton: FC<ToggleButtonProps> = ({ isChecked = false, onChange }) => (
|
||||
<CheckBoxWrapper>
|
||||
<CheckBox id="checkbox" type="checkbox" onClick={onChange} checked={isChecked}/>
|
||||
<CheckBoxLabel htmlFor="checkbox"/>
|
||||
<CheckBox id="checkbox" type="checkbox" onClick={onChange} checked={isChecked} />
|
||||
<CheckBoxLabel htmlFor="checkbox" />
|
||||
</CheckBoxWrapper>
|
||||
);
|
||||
|
||||
@@ -7,8 +7,8 @@ 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";
|
||||
|
||||
@@ -103,7 +103,7 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRun
|
||||
} else {
|
||||
switch (column.id) {
|
||||
case 'runStatus':
|
||||
return (
|
||||
return (
|
||||
<TableCell key={column.id} align={column.align}>
|
||||
{row.status === 'success' && <Chip label={t('runs_table.run_status_chips.success')} color="success" variant="outlined" />}
|
||||
{row.status === 'running' && <Chip label={t('runs_table.run_status_chips.running')} color="warning" variant="outlined" />}
|
||||
@@ -148,11 +148,11 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRun
|
||||
/>
|
||||
<TextField
|
||||
label={
|
||||
row.runByUserId
|
||||
? t('runs_table.run_settings_modal.labels.run_by_user')
|
||||
: row.runByScheduleId
|
||||
? t('runs_table.run_settings_modal.labels.run_by_schedule')
|
||||
: t('runs_table.run_settings_modal.labels.run_by_api')
|
||||
row.runByUserId
|
||||
? t('runs_table.run_settings_modal.labels.run_by_user')
|
||||
: row.runByScheduleId
|
||||
? t('runs_table.run_settings_modal.labels.run_by_schedule')
|
||||
: t('runs_table.run_settings_modal.labels.run_by_api')
|
||||
}
|
||||
value={runByLabel}
|
||||
InputProps={{ readOnly: true }}
|
||||
@@ -161,10 +161,10 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRun
|
||||
<Typography variant="body1">
|
||||
{t('runs_table.run_settings_modal.labels.run_type')}:
|
||||
</Typography>
|
||||
<RunTypeChip
|
||||
runByUserId={row.runByUserId}
|
||||
runByScheduledId={row.runByScheduleId}
|
||||
runByAPI={row.runByAPI ?? false}
|
||||
<RunTypeChip
|
||||
runByUserId={row.runByUserId}
|
||||
runByScheduledId={row.runByScheduleId}
|
||||
runByAPI={row.runByAPI ?? false}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -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
|
||||
</Typography>
|
||||
<Box style={{ marginTop: '4px' }}>
|
||||
<Typography>
|
||||
{t('interpretation_buttons.modal.previous_action')} <b>{decisionModal.action}</b>,
|
||||
{t('interpretation_buttons.modal.previous_action')} <b>{decisionModal.action}</b>,
|
||||
{t('interpretation_buttons.modal.element_text')} <b>{decisionModal.innerText}</b>
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -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<InterpretationLogProps> = ({ 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<InterpretationLogProps> = ({ isOpen, se
|
||||
}
|
||||
}, [hasScrapeListAction, hasScrapeSchemaAction, hasScreenshotAction, setIsOpen]);
|
||||
|
||||
const { darkMode } = useThemeMode();
|
||||
|
||||
return (
|
||||
<Grid container>
|
||||
<Grid item xs={12} md={9} lg={9}>
|
||||
@@ -147,7 +150,7 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ArrowUpwardIcon fontSize="inherit" sx={{ marginRight: '10px'}} />
|
||||
<ArrowUpwardIcon fontSize="inherit" sx={{ marginRight: '10px' }} />
|
||||
{t('interpretation_log.titles.output_preview')}
|
||||
</Button>
|
||||
<SwipeableDrawer
|
||||
@@ -157,8 +160,8 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ 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<InterpretationLogProps> = ({ isOpen, se
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" gutterBottom style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<StorageIcon style={{ marginRight: '8px' }} />
|
||||
<StorageIcon style={{ marginRight: '8px' }} />
|
||||
{t('interpretation_log.titles.output_preview')}
|
||||
</Typography>
|
||||
<div
|
||||
@@ -77,9 +77,49 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<TabContext value={tab}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Tabs value={tab} onChange={(e, newTab) => setTab(newTab)} aria-label="run-content-tabs">
|
||||
<Tab label={t('run_content.tabs.output_data')} value='output' />
|
||||
<Tab label={t('run_content.tabs.log')} value='log' />
|
||||
<Tabs
|
||||
value={tab}
|
||||
onChange={(e, newTab) => setTab(newTab)}
|
||||
aria-label="run-content-tabs"
|
||||
sx={{
|
||||
// Remove the default blue indicator
|
||||
'& .MuiTabs-indicator': {
|
||||
backgroundColor: '#FF00C3', // Change to pink
|
||||
},
|
||||
// Remove default transition effects
|
||||
'& .MuiTab-root': {
|
||||
'&.Mui-selected': {
|
||||
color: '#FF00C3',
|
||||
},
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tab
|
||||
label={t('run_content.tabs.output_data')}
|
||||
value='output'
|
||||
sx={{
|
||||
color: (theme) => theme.palette.mode === 'dark' ? '#fff' : '#000',
|
||||
'&:hover': {
|
||||
color: '#FF00C3'
|
||||
},
|
||||
'&.Mui-selected': {
|
||||
color: '#FF00C3',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Tab
|
||||
label={t('run_content.tabs.log')}
|
||||
value='log'
|
||||
sx={{
|
||||
color: (theme) => theme.palette.mode === 'dark' ? '#fff' : '#000',
|
||||
'&:hover': {
|
||||
color: '#FF00C3'
|
||||
},
|
||||
'&.Mui-selected': {
|
||||
color: '#FF00C3',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
</Box>
|
||||
<TabPanel value='log'>
|
||||
@@ -161,6 +201,7 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
|
||||
background: 'rgba(0,0,0,0.06)',
|
||||
maxHeight: '300px',
|
||||
overflow: 'scroll',
|
||||
backgroundColor: '#19171c'
|
||||
}}>
|
||||
<pre>
|
||||
{JSON.stringify(row.serializableOutput, null, 2)}
|
||||
@@ -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;
|
||||
@@ -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 (
|
||||
<Grid container direction="column" sx={{ padding: '30px'}}>
|
||||
<Grid container direction="column" sx={{ padding: '30px' }}>
|
||||
<Grid item xs>
|
||||
<RunsTable
|
||||
currentInterpretationLog={currentInterpretationLog}
|
||||
@@ -61,11 +61,11 @@ interface RunsTableProps {
|
||||
runningRecordingName: string;
|
||||
}
|
||||
|
||||
export const RunsTable: React.FC<RunsTableProps> = ({
|
||||
currentInterpretationLog,
|
||||
abortRunHandler,
|
||||
runId,
|
||||
runningRecordingName
|
||||
export const RunsTable: React.FC<RunsTableProps> = ({
|
||||
currentInterpretationLog,
|
||||
abortRunHandler,
|
||||
runId,
|
||||
runningRecordingName
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -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<LoaderProps> = ({ text }) => {
|
||||
const { darkMode } = useThemeMode();
|
||||
|
||||
return (
|
||||
<Stack direction="column" sx={{ margin: "30px 0px", alignItems: "center" }}>
|
||||
<DotsContainer>
|
||||
@@ -14,15 +17,19 @@ export const Loader: React.FC<LoaderProps> = ({ text }) => {
|
||||
<Dot />
|
||||
<Dot />
|
||||
</DotsContainer>
|
||||
<StyledParagraph>{text}</StyledParagraph>
|
||||
<StyledParagraph darkMode={darkMode}>{text}</StyledParagraph>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledParagraph = styled.p`
|
||||
interface StyledParagraphProps {
|
||||
darkMode: boolean;
|
||||
}
|
||||
|
||||
const StyledParagraph = styled.p<StyledParagraphProps>`
|
||||
font-size: large;
|
||||
font-family: inherit;
|
||||
color: #333;
|
||||
color: ${({ darkMode }) => (darkMode ? 'white' : '#333')};
|
||||
margin-top: 20px;
|
||||
`;
|
||||
|
||||
@@ -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`
|
||||
@@ -32,6 +32,7 @@ export interface SelectorObject {
|
||||
selector: string;
|
||||
tag?: string;
|
||||
attribute?: string;
|
||||
shadow?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
256
src/context/theme-provider.tsx
Normal file
256
src/context/theme-provider.tsx
Normal file
@@ -0,0 +1,256 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
|
||||
const lightTheme = createTheme({
|
||||
palette: {
|
||||
primary: {
|
||||
main: "#ff00c3",
|
||||
contrastText: "#ffffff",
|
||||
},
|
||||
},
|
||||
components: {
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
// Default styles for all buttons (optional)
|
||||
textTransform: "none",
|
||||
},
|
||||
containedPrimary: {
|
||||
// Styles for 'contained' variant with 'primary' color
|
||||
"&:hover": {
|
||||
backgroundColor: "#ff66d9",
|
||||
},
|
||||
},
|
||||
outlined: {
|
||||
// Apply white background for all 'outlined' variant buttons
|
||||
backgroundColor: "#ffffff",
|
||||
"&:hover": {
|
||||
backgroundColor: "#f0f0f0", // Optional lighter background on hover
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiLink: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
"&:hover": {
|
||||
color: "#ff00c3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiIconButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
// '&:hover': {
|
||||
// color: "#ff66d9",
|
||||
// },
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTab: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
textTransform: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiAlert: {
|
||||
styleOverrides: {
|
||||
standardInfo: {
|
||||
backgroundColor: "#fce1f4",
|
||||
color: "#ff00c3",
|
||||
"& .MuiAlert-icon": {
|
||||
color: "#ff00c3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiAlertTitle: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
"& .MuiAlert-icon": {
|
||||
color: "#ffffff",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const darkTheme = createTheme({
|
||||
palette: {
|
||||
mode: 'dark',
|
||||
primary: {
|
||||
main: "#ff00c3",
|
||||
contrastText: "#ffffff",
|
||||
},
|
||||
background: {
|
||||
default: '#121212',
|
||||
paper: '#1e1e1e',
|
||||
},
|
||||
text: {
|
||||
primary: '#ffffff',
|
||||
secondary: '#b3b3b3',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
textTransform: "none",
|
||||
color: '#ffffff',
|
||||
'&.MuiButton-outlined': {
|
||||
borderColor: '#ffffff',
|
||||
color: '#ffffff',
|
||||
"&:hover": {
|
||||
borderColor: '#ffffff',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
||||
},
|
||||
},
|
||||
},
|
||||
containedPrimary: {
|
||||
"&:hover": {
|
||||
backgroundColor: "#ff66d9",
|
||||
},
|
||||
},
|
||||
outlined: {
|
||||
// Dark mode outlined buttons
|
||||
backgroundColor: '#1e1e1e',
|
||||
borderColor: '#ff00c3',
|
||||
color: '#ff00c3',
|
||||
"&:hover": {
|
||||
backgroundColor: 'rgba(255, 0, 195, 0.08)',
|
||||
borderColor: '#ff66d9',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiLink: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
color: '#ff66d9',
|
||||
"&:hover": {
|
||||
color: "#ff00c3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiIconButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
color: '#ffffff',
|
||||
"&:hover": {
|
||||
backgroundColor: 'rgba(255, 0, 195, 0.08)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTab: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
textTransform: "none",
|
||||
color: '#ffffff',
|
||||
"&.Mui-selected": {
|
||||
color: '#ff00c3',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiAlert: {
|
||||
styleOverrides: {
|
||||
standardInfo: {
|
||||
backgroundColor: "rgba(255, 0, 195, 0.15)",
|
||||
color: "#ff66d9",
|
||||
"& .MuiAlert-icon": {
|
||||
color: "#ff66d9",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiAlertTitle: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
"& .MuiAlert-icon": {
|
||||
color: "#ff66d9",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// Additional dark mode specific components
|
||||
MuiPaper: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: '#1e1e1e',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiAppBar: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: '#121212',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiDrawer: {
|
||||
styleOverrides: {
|
||||
paper: {
|
||||
backgroundColor: '#121212',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTableCell: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderBottom: '1px solid rgba(255, 255, 255, 0.12)',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiDivider: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderColor: 'rgba(255, 255, 255, 0.12)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const ThemeModeContext = createContext({
|
||||
toggleTheme: () => {},
|
||||
darkMode: false,
|
||||
});
|
||||
|
||||
export const useThemeMode = () => useContext(ThemeModeContext);
|
||||
|
||||
const ThemeModeProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
// Load saved mode from localStorage or default to light mode
|
||||
const [darkMode, setDarkMode] = useState(() => {
|
||||
const savedMode = localStorage.getItem('darkMode');
|
||||
return savedMode ? JSON.parse(savedMode) : false;
|
||||
});
|
||||
|
||||
const toggleTheme = () => {
|
||||
setDarkMode((prevMode: any) => {
|
||||
const newMode = !prevMode;
|
||||
localStorage.setItem('darkMode', JSON.stringify(newMode)); // Save new mode to localStorage
|
||||
return newMode;
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('darkMode', JSON.stringify(darkMode)); // Save initial mode
|
||||
}, [darkMode]);
|
||||
|
||||
return (
|
||||
<ThemeModeContext.Provider value={{ toggleTheme, darkMode }}>
|
||||
<ThemeProvider theme={darkMode ? darkTheme : lightTheme}>
|
||||
<CssBaseline />
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</ThemeModeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeModeProvider;
|
||||
@@ -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;
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
@@ -71,14 +76,15 @@ const Login = () => {
|
||||
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 ? (
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
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 {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user