Merge branch 'develop' into ui-fix
This commit is contained in:
74
src/components/atoms/DatePicker.tsx
Normal file
74
src/components/atoms/DatePicker.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSocketStore } from '../../context/socket';
|
||||
import { Coordinates } from './canvas';
|
||||
|
||||
interface DatePickerProps {
|
||||
coordinates: Coordinates;
|
||||
selector: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const DatePicker: React.FC<DatePickerProps> = ({ coordinates, selector, onClose }) => {
|
||||
const { socket } = useSocketStore();
|
||||
const [selectedDate, setSelectedDate] = useState<string>('');
|
||||
|
||||
const handleDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSelectedDate(e.target.value);
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (socket && selectedDate) {
|
||||
socket.emit('input:date', {
|
||||
selector,
|
||||
value: selectedDate
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${coordinates.x}px`,
|
||||
top: `${coordinates.y}px`,
|
||||
zIndex: 1000,
|
||||
backgroundColor: 'white',
|
||||
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
|
||||
padding: '10px',
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<input
|
||||
type="date"
|
||||
onChange={handleDateChange}
|
||||
value={selectedDate}
|
||||
className="p-2 border rounded"
|
||||
autoFocus
|
||||
/>
|
||||
<div className="flex justify-end space-x-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-3 py-1 text-sm text-gray-600 hover:text-gray-800 border rounded"
|
||||
>
|
||||
Cancel
|
||||
</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'
|
||||
}`}
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatePicker;
|
||||
74
src/components/atoms/DateTimeLocalPicker.tsx
Normal file
74
src/components/atoms/DateTimeLocalPicker.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSocketStore } from '../../context/socket';
|
||||
import { Coordinates } from './canvas';
|
||||
|
||||
interface DateTimeLocalPickerProps {
|
||||
coordinates: Coordinates;
|
||||
selector: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const DateTimeLocalPicker: React.FC<DateTimeLocalPickerProps> = ({ coordinates, selector, onClose }) => {
|
||||
const { socket } = useSocketStore();
|
||||
const [selectedDateTime, setSelectedDateTime] = useState<string>('');
|
||||
|
||||
const handleDateTimeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSelectedDateTime(e.target.value);
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (socket && selectedDateTime) {
|
||||
socket.emit('input:datetime-local', {
|
||||
selector,
|
||||
value: selectedDateTime
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${coordinates.x}px`,
|
||||
top: `${coordinates.y}px`,
|
||||
zIndex: 1000,
|
||||
backgroundColor: 'white',
|
||||
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
|
||||
padding: '10px',
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<input
|
||||
type="datetime-local"
|
||||
onChange={handleDateTimeChange}
|
||||
value={selectedDateTime}
|
||||
className="p-2 border rounded"
|
||||
autoFocus
|
||||
/>
|
||||
<div className="flex justify-end space-x-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-3 py-1 text-sm text-gray-600 hover:text-gray-800 border rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirm}
|
||||
disabled={!selectedDateTime}
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateTimeLocalPicker;
|
||||
85
src/components/atoms/Dropdown.tsx
Normal file
85
src/components/atoms/Dropdown.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSocketStore } from '../../context/socket';
|
||||
import { Coordinates } from './canvas';
|
||||
|
||||
interface DropdownProps {
|
||||
coordinates: Coordinates;
|
||||
selector: string;
|
||||
options: Array<{
|
||||
value: string;
|
||||
text: string;
|
||||
disabled: boolean;
|
||||
selected: boolean;
|
||||
}>;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const Dropdown = ({ coordinates, selector, options, onClose }: DropdownProps) => {
|
||||
const { socket } = useSocketStore();
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||
|
||||
const handleSelect = (value: string) => {
|
||||
if (socket) {
|
||||
socket.emit('input:dropdown', { selector, value });
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const containerStyle: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
left: coordinates.x,
|
||||
top: coordinates.y,
|
||||
zIndex: 1000,
|
||||
width: '200px',
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid rgb(169, 169, 169)',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.15)',
|
||||
};
|
||||
|
||||
const scrollContainerStyle: React.CSSProperties = {
|
||||
maxHeight: '180px',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
};
|
||||
|
||||
const getOptionStyle = (option: any, index: number): React.CSSProperties => ({
|
||||
fontSize: '13.333px',
|
||||
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',
|
||||
userSelect: 'none',
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
style={containerStyle}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<div style={scrollContainerStyle}>
|
||||
{options.map((option, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={getOptionStyle(option, index)}
|
||||
onMouseEnter={() => !option.disabled && setHoveredIndex(index)}
|
||||
onMouseLeave={() => setHoveredIndex(null)}
|
||||
onClick={() => !option.disabled && handleSelect(option.value)}
|
||||
>
|
||||
{option.text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dropdown;
|
||||
130
src/components/atoms/TimePicker.tsx
Normal file
130
src/components/atoms/TimePicker.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSocketStore } from '../../context/socket';
|
||||
import { Coordinates } from './canvas';
|
||||
|
||||
interface TimePickerProps {
|
||||
coordinates: Coordinates;
|
||||
selector: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const TimePicker = ({ coordinates, selector, onClose }: TimePickerProps) => {
|
||||
const { socket } = useSocketStore();
|
||||
const [hoveredHour, setHoveredHour] = useState<number | null>(null);
|
||||
const [hoveredMinute, setHoveredMinute] = useState<number | null>(null);
|
||||
const [selectedHour, setSelectedHour] = useState<number | null>(null);
|
||||
const [selectedMinute, setSelectedMinute] = useState<number | null>(null);
|
||||
|
||||
const handleHourSelect = (hour: number) => {
|
||||
setSelectedHour(hour);
|
||||
// If minute is already selected, complete the selection
|
||||
if (selectedMinute !== null) {
|
||||
const formattedHour = hour.toString().padStart(2, '0');
|
||||
const formattedMinute = selectedMinute.toString().padStart(2, '0');
|
||||
if (socket) {
|
||||
socket.emit('input:time', {
|
||||
selector,
|
||||
value: `${formattedHour}:${formattedMinute}`
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleMinuteSelect = (minute: number) => {
|
||||
setSelectedMinute(minute);
|
||||
// If hour is already selected, complete the selection
|
||||
if (selectedHour !== null) {
|
||||
const formattedHour = selectedHour.toString().padStart(2, '0');
|
||||
const formattedMinute = minute.toString().padStart(2, '0');
|
||||
if (socket) {
|
||||
socket.emit('input:time', {
|
||||
selector,
|
||||
value: `${formattedHour}:${formattedMinute}`
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const containerStyle: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
left: coordinates.x,
|
||||
top: coordinates.y,
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid rgb(169, 169, 169)',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.15)',
|
||||
};
|
||||
|
||||
const columnStyle: React.CSSProperties = {
|
||||
width: '60px',
|
||||
maxHeight: '180px',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
borderRight: '1px solid rgb(169, 169, 169)',
|
||||
};
|
||||
|
||||
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',
|
||||
padding: '0 3px',
|
||||
cursor: 'default',
|
||||
backgroundColor: isSelected ? '#0078D7' : isHovered ? '#0078D7' : 'white',
|
||||
color: (isSelected || isHovered) ? 'white' : 'black',
|
||||
userSelect: 'none',
|
||||
};
|
||||
};
|
||||
|
||||
const hours = Array.from({ length: 24 }, (_, i) => i);
|
||||
const minutes = Array.from({ length: 60 }, (_, i) => i);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
style={containerStyle}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{/* Hours column */}
|
||||
<div style={columnStyle}>
|
||||
{hours.map((hour) => (
|
||||
<div
|
||||
key={hour}
|
||||
style={getOptionStyle(hour, true)}
|
||||
onMouseEnter={() => setHoveredHour(hour)}
|
||||
onMouseLeave={() => setHoveredHour(null)}
|
||||
onClick={() => handleHourSelect(hour)}
|
||||
>
|
||||
{hour.toString().padStart(2, '0')}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Minutes column */}
|
||||
<div style={{...columnStyle, borderRight: 'none'}}>
|
||||
{minutes.map((minute) => (
|
||||
<div
|
||||
key={minute}
|
||||
style={getOptionStyle(minute, false)}
|
||||
onMouseEnter={() => setHoveredMinute(minute)}
|
||||
onMouseLeave={() => setHoveredMinute(null)}
|
||||
onClick={() => handleMinuteSelect(minute)}
|
||||
>
|
||||
{minute.toString().padStart(2, '0')}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimePicker;
|
||||
@@ -1,17 +1,147 @@
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useMemo, Suspense } from 'react';
|
||||
import { useSocketStore } from '../../context/socket';
|
||||
import { getMappedCoordinates } from "../../helpers/inputHelpers";
|
||||
import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||
import { useActionContext } from '../../context/browserActions';
|
||||
const DatePicker = React.lazy(() => import('./DatePicker'));
|
||||
const Dropdown = React.lazy(() => import('./Dropdown'));
|
||||
const TimePicker = React.lazy(() => import('./TimePicker'));
|
||||
const DateTimeLocalPicker = React.lazy(() => import('./DateTimeLocalPicker'));
|
||||
|
||||
interface CreateRefCallback {
|
||||
(ref: React.RefObject<HTMLCanvasElement>): void;
|
||||
class RAFScheduler {
|
||||
private queue: Set<() => void> = new Set();
|
||||
private isProcessing: boolean = false;
|
||||
private frameId: number | null = null;
|
||||
|
||||
schedule(callback: () => void): void {
|
||||
this.queue.add(callback);
|
||||
if (!this.isProcessing) {
|
||||
this.process();
|
||||
}
|
||||
}
|
||||
|
||||
private process = (): void => {
|
||||
this.isProcessing = true;
|
||||
this.frameId = requestAnimationFrame(() => {
|
||||
const callbacks = Array.from(this.queue);
|
||||
this.queue.clear();
|
||||
|
||||
callbacks.forEach(callback => {
|
||||
try {
|
||||
callback();
|
||||
} catch (error) {
|
||||
console.error('RAF Scheduler error:', error);
|
||||
}
|
||||
});
|
||||
|
||||
this.isProcessing = false;
|
||||
this.frameId = null;
|
||||
|
||||
if (this.queue.size > 0) {
|
||||
this.process();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.queue.clear();
|
||||
if (this.frameId !== null) {
|
||||
cancelAnimationFrame(this.frameId);
|
||||
this.frameId = null;
|
||||
}
|
||||
this.isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
class EventDebouncer {
|
||||
private highPriorityQueue: Array<() => void> = [];
|
||||
private lowPriorityQueue: Array<() => void> = [];
|
||||
private processing: boolean = false;
|
||||
private scheduler: RAFScheduler;
|
||||
|
||||
constructor(scheduler: RAFScheduler) {
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
|
||||
add(callback: () => void, highPriority: boolean = false): void {
|
||||
if (highPriority) {
|
||||
this.highPriorityQueue.push(callback);
|
||||
} else {
|
||||
this.lowPriorityQueue.push(callback);
|
||||
}
|
||||
|
||||
if (!this.processing) {
|
||||
this.process();
|
||||
}
|
||||
}
|
||||
|
||||
private process(): void {
|
||||
this.processing = true;
|
||||
this.scheduler.schedule(() => {
|
||||
while (this.highPriorityQueue.length > 0) {
|
||||
const callback = this.highPriorityQueue.shift();
|
||||
callback?.();
|
||||
}
|
||||
|
||||
if (this.lowPriorityQueue.length > 0) {
|
||||
const callback = this.lowPriorityQueue.shift();
|
||||
callback?.();
|
||||
|
||||
if (this.lowPriorityQueue.length > 0) {
|
||||
this.process();
|
||||
}
|
||||
}
|
||||
|
||||
this.processing = false;
|
||||
});
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.highPriorityQueue = [];
|
||||
this.lowPriorityQueue = [];
|
||||
this.processing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Optimized measurement cache with LRU
|
||||
class MeasurementCache {
|
||||
private cache: Map<HTMLElement, DOMRect>;
|
||||
private maxSize: number;
|
||||
|
||||
constructor(maxSize: number = 100) {
|
||||
this.cache = new Map();
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
get(element: HTMLElement): DOMRect | undefined {
|
||||
const cached = this.cache.get(element);
|
||||
if (cached) {
|
||||
// Refresh the entry
|
||||
this.cache.delete(element);
|
||||
this.cache.set(element, cached);
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
set(element: HTMLElement, rect: DOMRect): void {
|
||||
if (this.cache.size >= this.maxSize) {
|
||||
// Remove oldest entry
|
||||
const firstKey = this.cache.keys().next().value;
|
||||
if (firstKey !== undefined) {
|
||||
this.cache.delete(firstKey);
|
||||
}
|
||||
}
|
||||
this.cache.set(element, rect);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
interface CanvasProps {
|
||||
width: number;
|
||||
height: number;
|
||||
onCreateRef: CreateRefCallback;
|
||||
onCreateRef: (ref: React.RefObject<HTMLCanvasElement>) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -22,134 +152,229 @@ export interface Coordinates {
|
||||
y: number;
|
||||
};
|
||||
|
||||
const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
|
||||
|
||||
const Canvas = React.memo(({ width, height, onCreateRef }: CanvasProps) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const { socket } = useSocketStore();
|
||||
const { setLastAction, lastAction } = useGlobalInfoStore();
|
||||
const { getText, getList } = useActionContext();
|
||||
const getTextRef = useRef(getText);
|
||||
const getListRef = useRef(getList);
|
||||
|
||||
const notifyLastAction = (action: string) => {
|
||||
if (lastAction !== action) {
|
||||
setLastAction(action);
|
||||
const scheduler = useRef(new RAFScheduler());
|
||||
const debouncer = useRef(new EventDebouncer(scheduler.current));
|
||||
const measurementCache = useRef(new MeasurementCache(50));
|
||||
//const performanceMonitor = useRef(new FrontendPerformanceMonitor());
|
||||
|
||||
const refs = useRef({
|
||||
getText,
|
||||
getList,
|
||||
lastMousePosition: { x: 0, y: 0 },
|
||||
lastFrameTime: 0,
|
||||
context: null as CanvasRenderingContext2D | null,
|
||||
});
|
||||
|
||||
const [state, dispatch] = React.useReducer((state: any, action: any) => {
|
||||
switch (action.type) {
|
||||
case 'BATCH_UPDATE':
|
||||
return { ...state, ...action.payload };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
}, {
|
||||
datePickerInfo: null,
|
||||
dropdownInfo: null,
|
||||
timePickerInfo: null,
|
||||
dateTimeLocalInfo: null
|
||||
});
|
||||
|
||||
const lastMousePosition = useRef<Coordinates>({ x: 0, y: 0 });
|
||||
const getEventCoordinates = useCallback((event: MouseEvent): { x: number; y: number } => {
|
||||
if (!canvasRef.current) return { x: 0, y: 0 };
|
||||
|
||||
useEffect(() => {
|
||||
getTextRef.current = getText;
|
||||
getListRef.current = getList;
|
||||
}, [getText, getList]);
|
||||
let rect = measurementCache.current.get(canvasRef.current);
|
||||
if (!rect) {
|
||||
rect = canvasRef.current.getBoundingClientRect();
|
||||
measurementCache.current.set(canvasRef.current, rect);
|
||||
}
|
||||
|
||||
const onMouseEvent = useCallback((event: MouseEvent) => {
|
||||
if (socket && canvasRef.current) {
|
||||
// Get the canvas bounding rectangle
|
||||
const rect = canvasRef.current.getBoundingClientRect();
|
||||
const clickCoordinates = {
|
||||
x: event.clientX - rect.left, // Use relative x coordinate
|
||||
y: event.clientY - rect.top, // Use relative y coordinate
|
||||
};
|
||||
return {
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top
|
||||
};
|
||||
}, []);
|
||||
|
||||
switch (event.type) {
|
||||
case 'mousedown':
|
||||
if (getTextRef.current === true) {
|
||||
const handleMouseEvent = useCallback((event: MouseEvent) => {
|
||||
if (!socket || !canvasRef.current) return;
|
||||
|
||||
//performanceMonitor.current.measureEventLatency(event);
|
||||
const coordinates = getEventCoordinates(event);
|
||||
|
||||
switch (event.type) {
|
||||
case 'mousedown':
|
||||
debouncer.current.add(() => {
|
||||
if (refs.current.getText) {
|
||||
console.log('Capturing Text...');
|
||||
} else if (getListRef.current === true) {
|
||||
} else if (refs.current.getList) {
|
||||
console.log('Capturing List...');
|
||||
} else {
|
||||
socket.emit('input:mousedown', clickCoordinates);
|
||||
socket.emit('input:mousedown', coordinates);
|
||||
}
|
||||
notifyLastAction('click');
|
||||
break;
|
||||
case 'mousemove':
|
||||
if (lastMousePosition.current.x !== clickCoordinates.x ||
|
||||
lastMousePosition.current.y !== clickCoordinates.y) {
|
||||
lastMousePosition.current = {
|
||||
x: clickCoordinates.x,
|
||||
y: clickCoordinates.y,
|
||||
};
|
||||
socket.emit('input:mousemove', {
|
||||
x: clickCoordinates.x,
|
||||
y: clickCoordinates.y,
|
||||
});
|
||||
notifyLastAction('move');
|
||||
}
|
||||
break;
|
||||
case 'wheel':
|
||||
const wheelEvent = event as WheelEvent;
|
||||
const deltas = {
|
||||
deltaX: Math.round(wheelEvent.deltaX),
|
||||
deltaY: Math.round(wheelEvent.deltaY),
|
||||
};
|
||||
socket.emit('input:wheel', deltas);
|
||||
notifyLastAction('scroll');
|
||||
break;
|
||||
default:
|
||||
console.log('Default mouseEvent registered');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [socket]);
|
||||
setLastAction('click');
|
||||
}, true); // High priority
|
||||
break;
|
||||
|
||||
const onKeyboardEvent = useCallback((event: KeyboardEvent) => {
|
||||
if (socket) {
|
||||
case 'mousemove':
|
||||
if (refs.current.lastMousePosition.x !== coordinates.x ||
|
||||
refs.current.lastMousePosition.y !== coordinates.y) {
|
||||
debouncer.current.add(() => {
|
||||
refs.current.lastMousePosition = coordinates;
|
||||
socket.emit('input:mousemove', coordinates);
|
||||
setLastAction('move');
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'wheel':
|
||||
const wheelEvent = event as WheelEvent;
|
||||
debouncer.current.add(() => {
|
||||
socket.emit('input:wheel', {
|
||||
deltaX: Math.round(wheelEvent.deltaX),
|
||||
deltaY: Math.round(wheelEvent.deltaY)
|
||||
});
|
||||
setLastAction('scroll');
|
||||
});
|
||||
break;
|
||||
}
|
||||
}, [socket, getEventCoordinates]);
|
||||
|
||||
const handleKeyboardEvent = useCallback((event: KeyboardEvent) => {
|
||||
if (!socket) return;
|
||||
|
||||
debouncer.current.add(() => {
|
||||
switch (event.type) {
|
||||
case 'keydown':
|
||||
socket.emit('input:keydown', { key: event.key, coordinates: lastMousePosition.current });
|
||||
notifyLastAction(`${event.key} pressed`);
|
||||
socket.emit('input:keydown', {
|
||||
key: event.key,
|
||||
coordinates: refs.current.lastMousePosition
|
||||
});
|
||||
setLastAction(`${event.key} pressed`);
|
||||
break;
|
||||
case 'keyup':
|
||||
socket.emit('input:keyup', event.key);
|
||||
break;
|
||||
default:
|
||||
console.log('Default keyEvent registered');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, event.type === 'keydown'); // High priority for keydown
|
||||
}, [socket]);
|
||||
|
||||
// Setup and cleanup
|
||||
useEffect(() => {
|
||||
if (!canvasRef.current) return;
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
refs.current.context = canvas.getContext('2d', {
|
||||
alpha: false,
|
||||
desynchronized: true
|
||||
});
|
||||
|
||||
onCreateRef(canvasRef);
|
||||
|
||||
const options = { passive: true };
|
||||
canvas.addEventListener('mousedown', handleMouseEvent, options);
|
||||
canvas.addEventListener('mousemove', handleMouseEvent, options);
|
||||
canvas.addEventListener('wheel', handleMouseEvent, options);
|
||||
canvas.addEventListener('keydown', handleKeyboardEvent, options);
|
||||
canvas.addEventListener('keyup', handleKeyboardEvent, options);
|
||||
|
||||
return () => {
|
||||
canvas.removeEventListener('mousedown', handleMouseEvent);
|
||||
canvas.removeEventListener('mousemove', handleMouseEvent);
|
||||
canvas.removeEventListener('wheel', handleMouseEvent);
|
||||
canvas.removeEventListener('keydown', handleKeyboardEvent);
|
||||
canvas.removeEventListener('keyup', handleKeyboardEvent);
|
||||
|
||||
scheduler.current.clear();
|
||||
debouncer.current.clear();
|
||||
measurementCache.current.clear();
|
||||
};
|
||||
}, [handleMouseEvent, handleKeyboardEvent, onCreateRef]);
|
||||
|
||||
// Performance monitoring
|
||||
// useEffect(() => {
|
||||
// const intervalId = setInterval(() => {
|
||||
// console.log('Performance Report:', performanceMonitor.current.getPerformanceReport());
|
||||
// }, 20000);
|
||||
|
||||
// return () => clearInterval(intervalId);
|
||||
// }, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (canvasRef.current) {
|
||||
onCreateRef(canvasRef);
|
||||
canvasRef.current.addEventListener('mousedown', onMouseEvent);
|
||||
canvasRef.current.addEventListener('mousemove', onMouseEvent);
|
||||
canvasRef.current.addEventListener('wheel', onMouseEvent, { passive: true });
|
||||
canvasRef.current.addEventListener('keydown', onKeyboardEvent);
|
||||
canvasRef.current.addEventListener('keyup', onKeyboardEvent);
|
||||
if (!socket) return;
|
||||
|
||||
return () => {
|
||||
if (canvasRef.current) {
|
||||
canvasRef.current.removeEventListener('mousedown', onMouseEvent);
|
||||
canvasRef.current.removeEventListener('mousemove', onMouseEvent);
|
||||
canvasRef.current.removeEventListener('wheel', onMouseEvent);
|
||||
canvasRef.current.removeEventListener('keydown', onKeyboardEvent);
|
||||
canvasRef.current.removeEventListener('keyup', onKeyboardEvent);
|
||||
}
|
||||
const handlers = {
|
||||
showDatePicker: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { datePickerInfo: info } }),
|
||||
showDropdown: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { dropdownInfo: info } }),
|
||||
showTimePicker: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { timePickerInfo: info } }),
|
||||
showDateTimePicker: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { dateTimeLocalInfo: info } })
|
||||
};
|
||||
|
||||
};
|
||||
} else {
|
||||
console.log('Canvas not initialized');
|
||||
}
|
||||
Object.entries(handlers).forEach(([event, handler]) => socket.on(event, handler));
|
||||
return () => {
|
||||
Object.keys(handlers).forEach(event => socket.off(event));
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
}, [onMouseEvent]);
|
||||
const memoizedDimensions = useMemo(() => ({
|
||||
width: width || 900,
|
||||
height: height || 400
|
||||
}), [width, height]);
|
||||
|
||||
return (
|
||||
<div style={{ borderRadius: '0px 0px 5px 5px', overflow: 'hidden', backgroundColor: 'white' }}>
|
||||
<div className="relative bg-white rounded-b-md overflow-hidden">
|
||||
<canvas
|
||||
tabIndex={0}
|
||||
ref={canvasRef}
|
||||
height={1000}
|
||||
width={900}
|
||||
style={{ display: 'block' }}
|
||||
height={memoizedDimensions.height}
|
||||
width={memoizedDimensions.width}
|
||||
className="block"
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
{state.datePickerInfo && (
|
||||
<DatePicker
|
||||
coordinates={state.datePickerInfo.coordinates}
|
||||
selector={state.datePickerInfo.selector}
|
||||
onClose={() => dispatch({
|
||||
type: 'BATCH_UPDATE',
|
||||
payload: { datePickerInfo: null }
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{state.dropdownInfo && (
|
||||
<Dropdown
|
||||
coordinates={state.dropdownInfo.coordinates}
|
||||
selector={state.dropdownInfo.selector}
|
||||
options={state.dropdownInfo.options}
|
||||
onClose={() => dispatch({
|
||||
type: 'BATCH_UPDATE',
|
||||
payload: { dropdownInfo: null }
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{state.timePickerInfo && (
|
||||
<TimePicker
|
||||
coordinates={state.timePickerInfo.coordinates}
|
||||
selector={state.timePickerInfo.selector}
|
||||
onClose={() => dispatch({ type: 'SET_TIME_PICKER', payload: null })}
|
||||
/>
|
||||
)}
|
||||
{state.dateTimeLocalInfo && (
|
||||
<DateTimeLocalPicker
|
||||
coordinates={state.dateTimeLocalInfo.coordinates}
|
||||
selector={state.dateTimeLocalInfo.selector}
|
||||
onClose={() => dispatch({ type: 'SET_DATETIME_PICKER', payload: null })}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
Canvas.displayName = 'Canvas';
|
||||
|
||||
export default Canvas;
|
||||
@@ -3,6 +3,7 @@ import styled from 'styled-components';
|
||||
import { Typography, FormControlLabel, Checkbox, Box } from '@mui/material';
|
||||
import { useActionContext } from '../../context/browserActions';
|
||||
import MaxunLogo from "../../assets/maxunlogo.png";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface CustomBoxContainerProps {
|
||||
isDarkMode: boolean;
|
||||
@@ -48,7 +49,9 @@ const Content = styled.div`
|
||||
text-align: left;
|
||||
`;
|
||||
|
||||
|
||||
const ActionDescriptionBox = ({ isDarkMode }: { isDarkMode: boolean }) => {
|
||||
const { t } = useTranslation();
|
||||
const { getText, getScreenshot, getList, captureStage } = useActionContext() as {
|
||||
getText: boolean;
|
||||
getScreenshot: boolean;
|
||||
@@ -57,10 +60,10 @@ const ActionDescriptionBox = ({ isDarkMode }: { isDarkMode: boolean }) => {
|
||||
};
|
||||
|
||||
const messages = [
|
||||
{ stage: 'initial', text: 'Select the list you want to extract along with the texts inside it' },
|
||||
{ stage: 'pagination', text: 'Select how the robot can capture the rest of the list' },
|
||||
{ stage: 'limit', text: 'Choose the number of items to extract' },
|
||||
{ stage: 'complete', text: 'Capture is complete' },
|
||||
{ stage: 'initial' as const, text: t('action_description.list_stages.initial') },
|
||||
{ stage: 'pagination' as const, text: t('action_description.list_stages.pagination') },
|
||||
{ stage: 'limit' as const, text: t('action_description.list_stages.limit') },
|
||||
{ stage: 'complete' as const, text: t('action_description.list_stages.complete') },
|
||||
];
|
||||
|
||||
const stages = messages.map(({ stage }) => stage);
|
||||
@@ -70,23 +73,23 @@ const ActionDescriptionBox = ({ isDarkMode }: { isDarkMode: boolean }) => {
|
||||
if (getText) {
|
||||
return (
|
||||
<>
|
||||
<Typography variant="subtitle2" gutterBottom>Capture Text</Typography>
|
||||
<Typography variant="body2" gutterBottom>Hover over the texts you want to extract and click to select them</Typography>
|
||||
<Typography variant="subtitle2" gutterBottom>{t('action_description.text.title')}</Typography>
|
||||
<Typography variant="body2" gutterBottom>{t('action_description.text.description')}</Typography>
|
||||
</>
|
||||
);
|
||||
} else if (getScreenshot) {
|
||||
return (
|
||||
<>
|
||||
<Typography variant="subtitle2" gutterBottom>Capture Screenshot</Typography>
|
||||
<Typography variant="body2" gutterBottom>Capture a partial or full page screenshot of the current page.</Typography>
|
||||
<Typography variant="subtitle2" gutterBottom>{t('action_description.screenshot.title')}</Typography>
|
||||
<Typography variant="body2" gutterBottom>{t('action_description.screenshot.description')}</Typography>
|
||||
</>
|
||||
);
|
||||
} else if (getList) {
|
||||
return (
|
||||
<>
|
||||
<Typography variant="subtitle2" gutterBottom>Capture List</Typography>
|
||||
<Typography variant="subtitle2" gutterBottom>{t('action_description.list.title')}</Typography>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
Hover over the list you want to extract. Once selected, you can hover over all texts inside the list you selected. Click to select them.
|
||||
{t('action_description.list.description')}
|
||||
</Typography>
|
||||
<Box>
|
||||
{messages.map(({ stage, text }, index) => (
|
||||
@@ -117,8 +120,8 @@ const ActionDescriptionBox = ({ isDarkMode }: { isDarkMode: boolean }) => {
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<Typography variant="subtitle2" gutterBottom>What data do you want to extract?</Typography>
|
||||
<Typography variant="body2" gutterBottom>A robot is designed to perform one action at a time. You can choose any of the options below.</Typography>
|
||||
<Typography variant="subtitle2" gutterBottom>{t('action_description.default.title')}</Typography>
|
||||
<Typography variant="body2" gutterBottom>{t('action_description.default.description')}</Typography>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -135,4 +138,4 @@ const ActionDescriptionBox = ({ isDarkMode }: { isDarkMode: boolean }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionDescriptionBox;
|
||||
export default ActionDescriptionBox;
|
||||
@@ -5,8 +5,10 @@ import { useGlobalInfoStore } from '../../context/globalInfo';
|
||||
import { stopRecording } from "../../api/recording";
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { GenericModal } from "../atoms/GenericModal";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const BrowserRecordingSave = () => {
|
||||
const { t } = useTranslation();
|
||||
const [openModal, setOpenModal] = useState<boolean>(false);
|
||||
const { recordingName, browserId, setBrowserId, notify } = useGlobalInfoStore();
|
||||
const navigate = useNavigate();
|
||||
@@ -14,7 +16,7 @@ const BrowserRecordingSave = () => {
|
||||
const goToMainMenu = async () => {
|
||||
if (browserId) {
|
||||
await stopRecording(browserId);
|
||||
notify('warning', 'Current Recording was terminated');
|
||||
notify('warning', t('browser_recording.notifications.terminated'));
|
||||
setBrowserId(null);
|
||||
}
|
||||
navigate('/');
|
||||
@@ -24,32 +26,31 @@ const BrowserRecordingSave = () => {
|
||||
<Grid container>
|
||||
<Grid item xs={12} md={3} lg={3}>
|
||||
<div style={{
|
||||
|
||||
// marginLeft: '10px',
|
||||
marginTop: '12px',
|
||||
color: 'white',
|
||||
position: 'absolute',
|
||||
background: '#ff00c3',
|
||||
border: 'none',
|
||||
borderRadius: '0px 0px 8px 8px',
|
||||
padding: '7.5px',
|
||||
width: '100%', // Ensure it takes full width but with padding
|
||||
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">
|
||||
Discard
|
||||
{t('right_panel.buttons.discard')}
|
||||
</Button>
|
||||
<GenericModal isOpen={openModal} onClose={() => setOpenModal(false)} modalStyle={modalStyle}>
|
||||
<Box p={2}>
|
||||
<Typography variant="h6">Are you sure you want to discard the recording?</Typography>
|
||||
<Typography variant="h6">{t('browser_recording.modal.confirm_discard')}</Typography>
|
||||
<Box display="flex" justifyContent="space-between" mt={2}>
|
||||
<Button onClick={goToMainMenu} variant="contained" color="error">
|
||||
Discard
|
||||
{t('right_panel.buttons.discard')}
|
||||
</Button>
|
||||
<Button onClick={() => setOpenModal(false)} variant="outlined">
|
||||
Cancel
|
||||
{t('right_panel.buttons.cancel')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -61,7 +62,7 @@ const BrowserRecordingSave = () => {
|
||||
);
|
||||
}
|
||||
|
||||
export default BrowserRecordingSave
|
||||
export default BrowserRecordingSave;
|
||||
|
||||
const modalStyle = {
|
||||
top: '25%',
|
||||
|
||||
@@ -10,6 +10,7 @@ import { RunContent } from "./RunContent";
|
||||
import { GenericModal } from "../atoms/GenericModal";
|
||||
import { modalStyle } from "./AddWhereCondModal";
|
||||
import { getUserById } from "../../api/auth";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface RunTypeChipProps {
|
||||
runByUserId?: string;
|
||||
@@ -18,10 +19,12 @@ interface RunTypeChipProps {
|
||||
}
|
||||
|
||||
const RunTypeChip: React.FC<RunTypeChipProps> = ({ runByUserId, runByScheduledId, runByAPI }) => {
|
||||
if (runByUserId) return <Chip label="Manual Run" color="primary" variant="outlined" />;
|
||||
if (runByScheduledId) return <Chip label="Scheduled Run" color="primary" variant="outlined" />;
|
||||
if (runByAPI) return <Chip label="API" color="primary" variant="outlined" />;
|
||||
return <Chip label="Unknown Run Type" color="primary" variant="outlined" />;
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (runByUserId) return <Chip label={t('runs_table.run_type_chips.manual_run')} color="primary" variant="outlined" />;
|
||||
if (runByScheduledId) return <Chip label={t('runs_table.run_type_chips.scheduled_run')} color="primary" variant="outlined" />;
|
||||
if (runByAPI) return <Chip label={t('runs_table.run_type_chips.api')} color="primary" variant="outlined" />;
|
||||
return <Chip label={t('runs_table.run_type_chips.unknown_run_type')} color="primary" variant="outlined" />;
|
||||
};
|
||||
|
||||
interface CollapsibleRowProps {
|
||||
@@ -33,6 +36,7 @@ interface CollapsibleRowProps {
|
||||
runningRecordingName: string;
|
||||
}
|
||||
export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRunHandler, runningRecordingName }: CollapsibleRowProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(isOpen);
|
||||
const [openSettingsModal, setOpenSettingsModal] = useState(false);
|
||||
const [userEmail, setUserEmail] = useState<string | null>(null);
|
||||
@@ -99,12 +103,12 @@ 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="Success" color="success" variant="outlined" />}
|
||||
{row.status === 'running' && <Chip label="Running" color="warning" variant="outlined" />}
|
||||
{row.status === 'scheduled' && <Chip label="Scheduled" variant="outlined" />}
|
||||
{row.status === 'failed' && <Chip label="Failed" color="error" variant="outlined" />}
|
||||
{row.status === 'success' && <Chip label={t('runs_table.run_status_chips.success')} color="success" variant="outlined" />}
|
||||
{row.status === 'running' && <Chip label={t('runs_table.run_status_chips.running')} color="warning" variant="outlined" />}
|
||||
{row.status === 'scheduled' && <Chip label={t('runs_table.run_status_chips.scheduled')} variant="outlined" />}
|
||||
{row.status === 'failed' && <Chip label={t('runs_table.run_status_chips.failed')} color="error" variant="outlined" />}
|
||||
</TableCell>
|
||||
)
|
||||
case 'delete':
|
||||
@@ -133,21 +137,35 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRun
|
||||
modalStyle={modalStyle}
|
||||
>
|
||||
<>
|
||||
<Typography variant="h5" style={{ marginBottom: '20px' }}>Run Settings</Typography>
|
||||
<Typography variant="h5" style={{ marginBottom: '20px' }}>
|
||||
{t('runs_table.run_settings_modal.title')}
|
||||
</Typography>
|
||||
<Box style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||
<TextField
|
||||
label="Run ID"
|
||||
label={t('runs_table.run_settings_modal.labels.run_id')}
|
||||
value={row.runId}
|
||||
InputProps={{ readOnly: true }}
|
||||
/>
|
||||
<TextField
|
||||
label={row.runByUserId ? "Run by User" : row.runByScheduleId ? "Run by Schedule ID" : "Run by API"}
|
||||
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')
|
||||
}
|
||||
value={runByLabel}
|
||||
InputProps={{ readOnly: true }}
|
||||
/>
|
||||
<Box style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
<Typography variant="body1">Run Type:</Typography>
|
||||
<RunTypeChip runByUserId={row.runByUserId} runByScheduledId={row.runByScheduleId} runByAPI={row.runByAPI ?? false} />
|
||||
<Typography variant="body1">
|
||||
{t('runs_table.run_settings_modal.labels.run_type')}:
|
||||
</Typography>
|
||||
<RunTypeChip
|
||||
runByUserId={row.runByUserId}
|
||||
runByScheduledId={row.runByScheduleId}
|
||||
runByAPI={row.runByAPI ?? false}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
|
||||
@@ -14,6 +14,9 @@ import axios from "axios";
|
||||
import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||
import { getStoredRecording } from "../../api/storage";
|
||||
import { apiUrl } from "../../apiConfig.js";
|
||||
import Cookies from 'js-cookie';
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
|
||||
interface IntegrationProps {
|
||||
isOpen: boolean;
|
||||
@@ -46,6 +49,7 @@ export const IntegrationSettingsModal = ({
|
||||
handleStart,
|
||||
handleClose,
|
||||
}: IntegrationProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [settings, setSettings] = useState<IntegrationSettings>({
|
||||
spreadsheetId: "",
|
||||
spreadsheetName: "",
|
||||
@@ -90,9 +94,9 @@ export const IntegrationSettingsModal = ({
|
||||
);
|
||||
notify(
|
||||
"error",
|
||||
`Error fetching spreadsheet files: ${
|
||||
error.response?.data?.message || error.message
|
||||
}`
|
||||
t('integration_settings.errors.fetch_error', {
|
||||
message: error.response?.data?.message || error.message
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -121,7 +125,7 @@ export const IntegrationSettingsModal = ({
|
||||
},
|
||||
{ withCredentials: true }
|
||||
);
|
||||
notify(`success`, `Google Sheet selected successfully`);
|
||||
notify(`success`, t('integration_settings.notifications.sheet_selected'));
|
||||
console.log("Google Sheet ID updated:", response.data);
|
||||
} catch (error: any) {
|
||||
console.error(
|
||||
@@ -182,37 +186,28 @@ export const IntegrationSettingsModal = ({
|
||||
|
||||
return (
|
||||
<GenericModal isOpen={isOpen} onClose={handleClose} modalStyle={modalStyle}>
|
||||
<div
|
||||
style={{
|
||||
<div style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
marginLeft: "65px",
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Typography variant="h6">
|
||||
Integrate with Google Sheet{" "}
|
||||
{t('integration_settings.title')}
|
||||
</Typography>
|
||||
|
||||
{recording && recording.google_sheet_id ? (
|
||||
<>
|
||||
<Alert severity="info" sx={{ marginTop: '10px', border: '1px solid #ff00c3' }}>
|
||||
<AlertTitle>Google Sheet Integrated Successfully.</AlertTitle>
|
||||
Every time this robot creates a successful run, its captured data
|
||||
is appended to your {recording.google_sheet_name} Google Sheet.
|
||||
You can check the data updates{" "}
|
||||
<a
|
||||
href={`https://docs.google.com/spreadsheets/d/${recording.google_sheet_id}`}
|
||||
<AlertTitle>{t('integration_settings.alerts.success.title')}</AlertTitle>
|
||||
{t('integration_settings.alerts.success.content', { sheetName: recording.google_sheet_name })}
|
||||
<a href={`https://docs.google.com/spreadsheets/d/${recording.google_sheet_id}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
rel="noreferrer">
|
||||
{t('integration_settings.alerts.success.here')}
|
||||
</a>.
|
||||
<br />
|
||||
<strong>Note:</strong> The data extracted before integrating with
|
||||
Google Sheets will not be synced in the Google Sheet. Only the
|
||||
data extracted after the integration will be synced.
|
||||
<strong>{t('integration_settings.alerts.success.note')}</strong> {t('integration_settings.alerts.success.sync_limitation')}
|
||||
</Alert>
|
||||
<Button
|
||||
variant="outlined"
|
||||
@@ -220,31 +215,29 @@ export const IntegrationSettingsModal = ({
|
||||
onClick={removeIntegration}
|
||||
style={{ marginTop: "15px" }}
|
||||
>
|
||||
Remove Integration
|
||||
{t('integration_settings.buttons.remove_integration')}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{!recording?.google_sheet_email ? (
|
||||
<>
|
||||
<p>
|
||||
If you enable this option, every time this robot runs a task
|
||||
successfully, its captured data will be appended to your
|
||||
Google Sheet.
|
||||
</p>
|
||||
<p>{t('integration_settings.descriptions.sync_info')}</p>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={authenticateWithGoogle}
|
||||
>
|
||||
Authenticate with Google
|
||||
{t('integration_settings.buttons.authenticate')}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{recording.google_sheet_email && (
|
||||
<Typography sx={{ margin: "20px 0px 30px 0px" }}>
|
||||
Authenticated as: {recording.google_sheet_email}
|
||||
{t('integration_settings.descriptions.authenticated_as', {
|
||||
email: recording.google_sheet_email
|
||||
})}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
@@ -260,14 +253,14 @@ export const IntegrationSettingsModal = ({
|
||||
color="primary"
|
||||
onClick={fetchSpreadsheetFiles}
|
||||
>
|
||||
Fetch Google Spreadsheets
|
||||
{t('integration_settings.buttons.fetch_sheets')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={removeIntegration}
|
||||
>
|
||||
Remove Integration
|
||||
{t('integration_settings.buttons.remove_integration')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
@@ -276,7 +269,7 @@ export const IntegrationSettingsModal = ({
|
||||
<TextField
|
||||
sx={{ marginBottom: "15px" }}
|
||||
select
|
||||
label="Select Google Sheet"
|
||||
label={t('integration_settings.fields.select_sheet')}
|
||||
required
|
||||
value={settings.spreadsheetId}
|
||||
onChange={handleSpreadsheetSelect}
|
||||
@@ -291,13 +284,10 @@ export const IntegrationSettingsModal = ({
|
||||
|
||||
{settings.spreadsheetId && (
|
||||
<Typography sx={{ marginBottom: "10px" }}>
|
||||
Selected Sheet:{" "}
|
||||
{
|
||||
spreadsheets.find(
|
||||
(s) => s.id === settings.spreadsheetId
|
||||
)?.name
|
||||
}{" "}
|
||||
(ID: {settings.spreadsheetId})
|
||||
{t('integration_settings.fields.selected_sheet', {
|
||||
name: spreadsheets.find((s) => s.id === settings.spreadsheetId)?.name,
|
||||
id: settings.spreadsheetId
|
||||
})}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
@@ -311,7 +301,7 @@ export const IntegrationSettingsModal = ({
|
||||
style={{ marginTop: "10px" }}
|
||||
disabled={!settings.spreadsheetId || loading}
|
||||
>
|
||||
Submit
|
||||
{t('integration_settings.buttons.submit')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||
import { GenericModal } from "../atoms/GenericModal";
|
||||
import { WhereWhatPair } from "maxun-core";
|
||||
import HelpIcon from '@mui/icons-material/Help';
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface InterpretationButtonsProps {
|
||||
enableStepping: (isPaused: boolean) => void;
|
||||
@@ -23,6 +24,7 @@ const interpretationInfo: InterpretationInfo = {
|
||||
};
|
||||
|
||||
export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [info, setInfo] = useState<InterpretationInfo>(interpretationInfo);
|
||||
const [decisionModal, setDecisionModal] = useState<{
|
||||
pair: WhereWhatPair | null,
|
||||
@@ -44,9 +46,9 @@ export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsP
|
||||
|
||||
const breakpointHitHandler = useCallback(() => {
|
||||
setInfo({ running: false, isPaused: true });
|
||||
notify('warning', 'Please restart the interpretation after updating the recording');
|
||||
notify('warning', t('interpretation_buttons.messages.restart_required'));
|
||||
enableStepping(true);
|
||||
}, [enableStepping]);
|
||||
}, [enableStepping, t]);
|
||||
|
||||
const decisionHandler = useCallback(
|
||||
({ pair, actionType, lastData }: { pair: WhereWhatPair | null, actionType: string, lastData: { selector: string, action: string, tagName: string, innerText: string } }) => {
|
||||
@@ -73,11 +75,12 @@ export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsP
|
||||
return (
|
||||
<>
|
||||
<Typography>
|
||||
Do you want to use your previous selection as a condition for performing this action?
|
||||
{t('interpretation_buttons.modal.use_previous')}
|
||||
</Typography>
|
||||
<Box style={{ marginTop: '4px' }}>
|
||||
<Typography>
|
||||
Your previous action was: <b>{decisionModal.action}</b>, on an element with text <b>{decisionModal.innerText}</b>
|
||||
{t('interpretation_buttons.modal.previous_action')} <b>{decisionModal.action}</b>,
|
||||
{t('interpretation_buttons.modal.element_text')} <b>{decisionModal.innerText}</b>
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
@@ -105,9 +108,9 @@ export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsP
|
||||
const finished = await interpretCurrentRecording();
|
||||
setInfo({ ...info, running: false });
|
||||
if (finished) {
|
||||
notify('info', 'Run finished');
|
||||
notify('info', t('interpretation_buttons.messages.run_finished'));
|
||||
} else {
|
||||
notify('error', 'Run failed to start');
|
||||
notify('error', t('interpretation_buttons.messages.run_failed'));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -139,9 +142,12 @@ export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsP
|
||||
disabled={info.running}
|
||||
sx={{ display: 'grid' }}
|
||||
>
|
||||
{info.running ? <Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<CircularProgress size={22} color="inherit" sx={{ marginRight: '10px' }} /> Extracting data...please wait for 10secs to 1min
|
||||
</Box> : 'Get Preview of Output Data'}
|
||||
{info.running ? (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<CircularProgress size={22} color="inherit" sx={{ marginRight: '10px' }} />
|
||||
{t('interpretation_buttons.messages.extracting')}
|
||||
</Box>
|
||||
) : t('interpretation_buttons.buttons.preview')}
|
||||
</Button>
|
||||
<GenericModal
|
||||
onClose={() => { }}
|
||||
@@ -166,8 +172,12 @@ export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsP
|
||||
<HelpIcon />
|
||||
{handleDescription()}
|
||||
<div style={{ float: 'right' }}>
|
||||
<Button onClick={() => handleDecision(true)} color='success'>Yes</Button>
|
||||
<Button onClick={() => handleDecision(false)} color='error'>No</Button>
|
||||
<Button onClick={() => handleDecision(true)} color='success'>
|
||||
{t('interpretation_buttons.buttons.yes')}
|
||||
</Button>
|
||||
<Button onClick={() => handleDecision(false)} color='error'>
|
||||
{t('interpretation_buttons.buttons.no')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</GenericModal>
|
||||
|
||||
@@ -18,6 +18,7 @@ import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
||||
import { SidePanelHeader } from './SidePanelHeader';
|
||||
import { useGlobalInfoStore } from '../../context/globalInfo';
|
||||
import { useThemeMode } from '../../context/theme-provider';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface InterpretationLogProps {
|
||||
isOpen: boolean;
|
||||
@@ -25,6 +26,7 @@ interface InterpretationLogProps {
|
||||
}
|
||||
|
||||
export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, setIsOpen }) => {
|
||||
const { t } = useTranslation();
|
||||
const [log, setLog] = useState<string>('');
|
||||
const [customValue, setCustomValue] = useState('');
|
||||
const [tableData, setTableData] = useState<any[]>([]);
|
||||
@@ -34,7 +36,7 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
|
||||
|
||||
const { width } = useBrowserDimensionsStore();
|
||||
const { socket } = useSocketStore();
|
||||
const { currentWorkflowActionsState } = useGlobalInfoStore();
|
||||
const { currentWorkflowActionsState, shouldResetInterpretationLog, notify } = useGlobalInfoStore();
|
||||
|
||||
const toggleDrawer = (newOpen: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => {
|
||||
if (
|
||||
@@ -64,34 +66,43 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
|
||||
|
||||
const handleSerializableCallback = useCallback((data: any) => {
|
||||
setLog((prevState) =>
|
||||
prevState + '\n' + '---------- Serializable output data received ----------' + '\n'
|
||||
+ JSON.stringify(data, null, 2) + '\n' + '--------------------------------------------------');
|
||||
prevState + '\n' + t('interpretation_log.data_sections.serializable_received') + '\n'
|
||||
+ JSON.stringify(data, null, 2) + '\n' + t('interpretation_log.data_sections.separator'));
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
setTableData(data);
|
||||
}
|
||||
|
||||
scrollLogToBottom();
|
||||
}, [log, scrollLogToBottom]);
|
||||
}, [log, scrollLogToBottom, t]);
|
||||
|
||||
const handleBinaryCallback = useCallback(({ data, mimetype }: any) => {
|
||||
const base64String = Buffer.from(data).toString('base64');
|
||||
const imageSrc = `data:${mimetype};base64,${base64String}`;
|
||||
|
||||
setLog((prevState) =>
|
||||
prevState + '\n' + '---------- Binary output data received ----------' + '\n'
|
||||
+ `mimetype: ${mimetype}` + '\n' + 'Image is rendered below:' + '\n'
|
||||
+ '------------------------------------------------');
|
||||
prevState + '\n' + t('interpretation_log.data_sections.binary_received') + '\n'
|
||||
+ t('interpretation_log.data_sections.mimetype') + mimetype + '\n'
|
||||
+ t('interpretation_log.data_sections.image_below') + '\n'
|
||||
+ t('interpretation_log.data_sections.separator'));
|
||||
|
||||
setBinaryData(imageSrc);
|
||||
scrollLogToBottom();
|
||||
}, [log, scrollLogToBottom]);
|
||||
}, [log, scrollLogToBottom, t]);
|
||||
|
||||
|
||||
const handleCustomValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setCustomValue(event.target.value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldResetInterpretationLog) {
|
||||
setLog('');
|
||||
setTableData([]);
|
||||
setBinaryData(null);
|
||||
}
|
||||
}, [shouldResetInterpretationLog]);
|
||||
|
||||
useEffect(() => {
|
||||
socket?.on('log', handleLog);
|
||||
socket?.on('serializableCallback', handleSerializableCallback);
|
||||
@@ -141,7 +152,8 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ArrowUpwardIcon fontSize="inherit" sx={{ marginRight: '10px'}} /> Output Data Preview
|
||||
<ArrowUpwardIcon fontSize="inherit" sx={{ marginRight: '10px'}} />
|
||||
{t('interpretation_log.titles.output_preview')}
|
||||
</Button>
|
||||
<SwipeableDrawer
|
||||
anchor="bottom"
|
||||
@@ -160,9 +172,10 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" gutterBottom style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<StorageIcon style={{ marginRight: '8px' }} /> Output Data Preview
|
||||
</Typography>
|
||||
<Typography variant="h6" gutterBottom style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<StorageIcon style={{ marginRight: '8px' }} />
|
||||
{t('interpretation_log.titles.output_preview')}
|
||||
</Typography>
|
||||
<div
|
||||
style={{
|
||||
height: '50vh',
|
||||
@@ -173,8 +186,10 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
|
||||
{
|
||||
binaryData ? (
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<Typography variant="body1" gutterBottom>Screenshot</Typography>
|
||||
<img src={binaryData} alt="Binary Output" style={{ maxWidth: '100%' }} />
|
||||
<Typography variant="body1" gutterBottom>
|
||||
{t('interpretation_log.titles.screenshot')}
|
||||
</Typography>
|
||||
<img src={binaryData} alt={t('interpretation_log.titles.screenshot')} style={{ maxWidth: '100%' }} />
|
||||
</div>
|
||||
) : tableData.length > 0 ? (
|
||||
<>
|
||||
@@ -198,7 +213,9 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<span style={{ marginLeft: '15px', marginTop: '10px', fontSize: '12px' }}>Additional rows of data will be extracted once you finish recording. </span>
|
||||
<span style={{ marginLeft: '15px', marginTop: '10px', fontSize: '12px' }}>
|
||||
{t('interpretation_log.messages.additional_rows')}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<Grid container justifyContent="center" alignItems="center" style={{ height: '100%' }}>
|
||||
@@ -206,13 +223,13 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
|
||||
{hasScrapeListAction || hasScrapeSchemaAction || hasScreenshotAction ? (
|
||||
<>
|
||||
<Typography variant="h6" gutterBottom align="left">
|
||||
You've successfully trained the robot to perform actions! Click on the button below to get a preview of the data your robot will extract.
|
||||
{t('interpretation_log.messages.successful_training')}
|
||||
</Typography>
|
||||
<SidePanelHeader />
|
||||
</>
|
||||
) : (
|
||||
<Typography variant="h6" gutterBottom align="left">
|
||||
It looks like you have not selected anything for extraction yet. Once you do, the robot will show a preview of your selections here.
|
||||
{t('interpretation_log.messages.no_selection')}
|
||||
</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
@@ -224,4 +241,4 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useContext } from 'react';
|
||||
import { useTranslation } from "react-i18next";
|
||||
import React, { useState, useContext, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
@@ -20,173 +21,592 @@ 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 { useNavigate } from 'react-router-dom';
|
||||
import { AuthContext } from '../../context/auth';
|
||||
import { SaveRecording } from '../molecules/SaveRecording';
|
||||
import DiscordIcon from '../atoms/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;
|
||||
}
|
||||
|
||||
export const NavBar: React.FC<NavBarProps> = ({ recordingName, isRecording }) => {
|
||||
export const NavBar: React.FC<NavBarProps> = ({
|
||||
recordingName,
|
||||
isRecording,
|
||||
}) => {
|
||||
const { notify, browserId, setBrowserId } = useGlobalInfoStore();
|
||||
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);
|
||||
|
||||
const [langAnchorEl, setLangAnchorEl] = useState<null | HTMLElement>(null);
|
||||
|
||||
const currentVersion = packageJson.version;
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [latestVersion, setLatestVersion] = useState<string | null>(null);
|
||||
const [tab, setTab] = useState(0);
|
||||
const [isUpdateAvailable, setIsUpdateAvailable] = useState(false);
|
||||
|
||||
const fetchLatestVersion = async (): Promise<string | null> => {
|
||||
try {
|
||||
const response = await fetch("https://api.github.com/repos/getmaxun/maxun/releases/latest");
|
||||
const data = await response.json();
|
||||
const version = data.tag_name.replace(/^v/, ""); // Remove 'v' prefix
|
||||
return version;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch latest version:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateOpen = () => {
|
||||
setOpen(true);
|
||||
fetchLatestVersion();
|
||||
};
|
||||
|
||||
const handleUpdateClose = () => {
|
||||
setOpen(false);
|
||||
setTab(0); // Reset tab to the first tab
|
||||
};
|
||||
|
||||
const handleUpdateTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
setTab(newValue);
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleLangMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setLangAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null);
|
||||
setLangAnchorEl(null);
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
dispatch({ type: 'LOGOUT' });
|
||||
window.localStorage.removeItem('user');
|
||||
dispatch({ type: "LOGOUT" });
|
||||
window.localStorage.removeItem("user");
|
||||
const { data } = await axios.get(`${apiUrl}/auth/logout`);
|
||||
notify('success', data.message);
|
||||
navigate('/login');
|
||||
notify("success", data.message);
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
const goToMainMenu = async () => {
|
||||
if (browserId) {
|
||||
await stopRecording(browserId);
|
||||
notify('warning', 'Current Recording was terminated');
|
||||
notify("warning", t('browser_recording.notifications.terminated'));
|
||||
setBrowserId(null);
|
||||
}
|
||||
navigate('/');
|
||||
navigate("/");
|
||||
};
|
||||
|
||||
const renderBrandSection = () => (
|
||||
<BrandContainer>
|
||||
<LogoImage src={MaxunLogo} alt="Maxun Logo" />
|
||||
<ProjectName mode={darkMode ? 'dark' : 'light'}>Maxun</ProjectName>
|
||||
<Chip
|
||||
label="beta"
|
||||
variant="outlined"
|
||||
sx={{
|
||||
marginTop: '10px',
|
||||
borderColor: '#ff00c3',
|
||||
color: '#ff00c3'
|
||||
}}
|
||||
/>
|
||||
</BrandContainer>
|
||||
);
|
||||
const changeLanguage = (lang: string) => {
|
||||
i18n.changeLanguage(lang);
|
||||
localStorage.setItem("language", lang);
|
||||
};
|
||||
|
||||
const renderSocialButtons = () => (
|
||||
<>
|
||||
<IconButton
|
||||
component="a"
|
||||
href="https://discord.gg/5GbPjBUkws"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
sx={{
|
||||
...styles.socialButton,
|
||||
color: darkMode ? '#ffffff' : '#333333',
|
||||
'&:hover': {
|
||||
color: '#ff00c3'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DiscordIcon sx={{ marginRight: '5px' }} />
|
||||
</IconButton>
|
||||
<iframe
|
||||
src="https://ghbtns.com/github-btn.html?user=getmaxun&repo=maxun&type=star&count=true&size=large"
|
||||
frameBorder="0"
|
||||
scrolling="0"
|
||||
width="170"
|
||||
height="30"
|
||||
title="GitHub"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
// const renderBrandSection = () => (
|
||||
// <BrandContainer>
|
||||
// <LogoImage src={MaxunLogo} alt="Maxun Logo" />
|
||||
// <ProjectName mode={darkMode ? 'dark' : 'light'}>Maxun</ProjectName>
|
||||
// <Chip
|
||||
// label="beta"
|
||||
// variant="outlined"
|
||||
// sx={{
|
||||
// marginTop: '10px',
|
||||
// borderColor: '#ff00c3',
|
||||
// color: '#ff00c3'
|
||||
// }}
|
||||
// />
|
||||
// </BrandContainer>
|
||||
// );
|
||||
|
||||
const renderUserMenu = () => (
|
||||
<>
|
||||
<IconButton
|
||||
onClick={handleMenuOpen}
|
||||
sx={styles.userButton(darkMode)}
|
||||
>
|
||||
<AccountCircle sx={{ marginRight: '5px' }} />
|
||||
<Typography variant="body1">{user?.email}</Typography>
|
||||
</IconButton>
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
backgroundColor: darkMode ? '#1e2124' : '#ffffff',
|
||||
color: darkMode ? '#ffffff' : '#333333'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem onClick={() => { handleMenuClose(); logout(); }}>
|
||||
<Logout sx={{ marginRight: '5px' }} /> Logout
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
// const renderSocialButtons = () => (
|
||||
// <>
|
||||
// <IconButton
|
||||
// component="a"
|
||||
// href="https://discord.gg/5GbPjBUkws"
|
||||
// target="_blank"
|
||||
// rel="noopener noreferrer"
|
||||
// sx={{
|
||||
// ...styles.socialButton,
|
||||
// color: darkMode ? '#ffffff' : '#333333',
|
||||
// '&:hover': {
|
||||
// color: '#ff00c3'
|
||||
// }
|
||||
// }}
|
||||
// >
|
||||
// <DiscordIcon sx={{ marginRight: '5px' }} />
|
||||
// </IconButton>
|
||||
// <iframe
|
||||
// src="https://ghbtns.com/github-btn.html?user=getmaxun&repo=maxun&type=star&count=true&size=large"
|
||||
// frameBorder="0"
|
||||
// scrolling="0"
|
||||
// width="170"
|
||||
// height="30"
|
||||
// title="GitHub"
|
||||
// />
|
||||
// </>
|
||||
// );
|
||||
|
||||
const renderThemeToggle = () => (
|
||||
<Tooltip title="Toggle light/dark theme">
|
||||
<IconButton
|
||||
onClick={toggleTheme}
|
||||
sx={{
|
||||
color: darkMode ? '#ffffff' : '#333333',
|
||||
'&:hover': {
|
||||
color: '#ff00c3'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{darkMode ? <Brightness7 /> : <Brightness4 />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
// const renderUserMenu = () => (
|
||||
// <>
|
||||
// <IconButton
|
||||
// onClick={handleMenuOpen}
|
||||
// sx={styles.userButton(darkMode)}
|
||||
// >
|
||||
// <AccountCircle sx={{ marginRight: '5px' }} />
|
||||
// <Typography variant="body1">{user?.email}</Typography>
|
||||
// </IconButton>
|
||||
// <Menu
|
||||
// anchorEl={anchorEl}
|
||||
// open={Boolean(anchorEl)}
|
||||
// onClose={handleMenuClose}
|
||||
// anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
// transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
// PaperProps={{
|
||||
// sx: {
|
||||
// backgroundColor: darkMode ? '#1e2124' : '#ffffff',
|
||||
// color: darkMode ? '#ffffff' : '#333333'
|
||||
// }
|
||||
// }}
|
||||
// >
|
||||
// <MenuItem onClick={() => { handleMenuClose(); logout(); }}>
|
||||
// <Logout sx={{ marginRight: '5px' }} /> Logout
|
||||
// </MenuItem>
|
||||
// </Menu>
|
||||
// </>
|
||||
// );
|
||||
|
||||
const renderRecordingControls = () => (
|
||||
<>
|
||||
<IconButton
|
||||
onClick={goToMainMenu}
|
||||
sx={styles.discardButton}
|
||||
>
|
||||
<Clear sx={{ marginRight: '5px' }} />
|
||||
Discard
|
||||
</IconButton>
|
||||
<SaveRecording fileName={recordingName} />
|
||||
</>
|
||||
);
|
||||
// const renderThemeToggle = () => (
|
||||
// <Tooltip title="Toggle light/dark theme">
|
||||
// <IconButton
|
||||
// onClick={toggleTheme}
|
||||
// sx={{
|
||||
// color: darkMode ? '#ffffff' : '#333333',
|
||||
// '&:hover': {
|
||||
// color: '#ff00c3'
|
||||
// }
|
||||
// }}
|
||||
// >
|
||||
// {darkMode ? <Brightness7 /> : <Brightness4 />}
|
||||
// </IconButton>
|
||||
// </Tooltip>
|
||||
// );
|
||||
|
||||
return (
|
||||
// const renderRecordingControls = () => (
|
||||
// <>
|
||||
// <IconButton
|
||||
// onClick={goToMainMenu}
|
||||
// sx={styles.discardButton}
|
||||
// >
|
||||
// <Clear sx={{ marginRight: '5px' }} />
|
||||
// Discard
|
||||
// </IconButton>
|
||||
// <SaveRecording fileName={recordingName} />
|
||||
// </>
|
||||
// );
|
||||
|
||||
<NavBarWrapper mode={darkMode ? 'dark' : 'light'}>
|
||||
{renderBrandSection()}
|
||||
{user && (
|
||||
<ControlsContainer>
|
||||
{!isRecording ? (
|
||||
<>
|
||||
{renderSocialButtons()}
|
||||
{renderUserMenu()}
|
||||
{renderThemeToggle()}
|
||||
</>
|
||||
) : (
|
||||
renderRecordingControls()
|
||||
)}
|
||||
</ControlsContainer>
|
||||
)}
|
||||
// return (
|
||||
|
||||
// <NavBarWrapper mode={darkMode ? 'dark' : 'light'}>
|
||||
// {renderBrandSection()}
|
||||
// {user && (
|
||||
// <ControlsContainer>
|
||||
// {!isRecording ? (
|
||||
// <>
|
||||
// {renderSocialButtons()}
|
||||
// {renderUserMenu()}
|
||||
// {renderThemeToggle()}
|
||||
// </>
|
||||
// ) : (
|
||||
// renderRecordingControls()
|
||||
// )}
|
||||
// </ControlsContainer>
|
||||
// )}
|
||||
|
||||
|
||||
</NavBarWrapper>
|
||||
// </NavBarWrapper>
|
||||
|
||||
useEffect(() => {
|
||||
const checkForUpdates = async () => {
|
||||
const latestVersion = await fetchLatestVersion();
|
||||
setLatestVersion(latestVersion);
|
||||
if (latestVersion && latestVersion !== currentVersion) {
|
||||
setIsUpdateAvailable(true);
|
||||
}
|
||||
};
|
||||
checkForUpdates();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isUpdateAvailable && (
|
||||
<Snackbar
|
||||
open={isUpdateAvailable}
|
||||
onClose={() => setIsUpdateAvailable(false)}
|
||||
message={
|
||||
`${t('navbar.upgrade.modal.new_version_available', { version: latestVersion })} ${t('navbar.upgrade.modal.view_updates')}`
|
||||
}
|
||||
action={
|
||||
<>
|
||||
<Button
|
||||
color="primary"
|
||||
size="small"
|
||||
onClick={handleUpdateOpen}
|
||||
style={{
|
||||
backgroundColor: '#ff00c3',
|
||||
color: 'white',
|
||||
fontWeight: 'bold',
|
||||
textTransform: 'none',
|
||||
marginRight: '8px',
|
||||
borderRadius: '5px',
|
||||
}}
|
||||
>
|
||||
{t('navbar.upgrade.button')}
|
||||
</Button>
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label="close"
|
||||
color="inherit"
|
||||
onClick={() => setIsUpdateAvailable(false)}
|
||||
style={{ color: 'black' }}
|
||||
>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</>
|
||||
}
|
||||
ContentProps={{
|
||||
sx: {
|
||||
background: "white",
|
||||
color: "black",
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<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>
|
||||
<Chip
|
||||
label={`${currentVersion}`}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
sx={{ marginTop: '10px' }}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
user ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
|
||||
{!isRecording ? (
|
||||
<>
|
||||
<Button variant="outlined" onClick={handleUpdateOpen} sx={{
|
||||
marginRight: '40px',
|
||||
color: "#00000099",
|
||||
border: "#00000099 1px solid",
|
||||
'&:hover': { color: '#ff00c3', border: '#ff00c3 1px solid' }
|
||||
}}>
|
||||
<Update sx={{ marginRight: '5px' }} /> {t('navbar.upgrade.button')} Maxun
|
||||
</Button>
|
||||
<Modal open={open} onClose={handleUpdateClose}>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 500,
|
||||
bgcolor: "background.paper",
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
{latestVersion === null ? (
|
||||
<Typography>Checking for updates...</Typography>
|
||||
) : currentVersion === latestVersion ? (
|
||||
<Typography variant="h6" textAlign="center">
|
||||
{t('navbar.upgrade.modal.up_to_date')}
|
||||
</Typography>
|
||||
) : (
|
||||
<>
|
||||
<Typography variant="body1" textAlign="left" sx={{ marginLeft: '30px' }}>
|
||||
{t('navbar.upgrade.modal.new_version_available', { version: latestVersion })}
|
||||
<br />
|
||||
{t('navbar.upgrade.modal.view_updates')}
|
||||
<a href="https://github.com/getmaxun/maxun/releases/" target="_blank" style={{ textDecoration: 'none' }}>{' '}here.</a>
|
||||
</Typography>
|
||||
<Tabs
|
||||
value={tab}
|
||||
onChange={handleUpdateTabChange}
|
||||
sx={{ marginTop: 2, marginBottom: 2 }}
|
||||
centered
|
||||
>
|
||||
<Tab label={t('navbar.upgrade.modal.tabs.manual_setup')} />
|
||||
<Tab label={t('navbar.upgrade.modal.tabs.docker_setup')} />
|
||||
</Tabs>
|
||||
{tab === 0 && (
|
||||
<Box sx={{ marginLeft: '30px', background: '#cfd0d1', padding: 1, borderRadius: 3 }}>
|
||||
<code style={{ color: 'black' }}>
|
||||
<p>Run the commands below</p>
|
||||
# cd to project directory (eg: maxun)
|
||||
<br />
|
||||
cd maxun
|
||||
<br />
|
||||
<br />
|
||||
# pull latest changes
|
||||
<br />
|
||||
git pull origin master
|
||||
<br />
|
||||
<br />
|
||||
# install dependencies
|
||||
<br />
|
||||
npm install
|
||||
<br />
|
||||
<br />
|
||||
# start maxun
|
||||
<br />
|
||||
npm run start
|
||||
</code>
|
||||
</Box>
|
||||
)}
|
||||
{tab === 1 && (
|
||||
<Box sx={{ marginLeft: '30px', background: '#cfd0d1', padding: 1, borderRadius: 3 }}>
|
||||
<code style={{ color: 'black' }}>
|
||||
<p>Run the commands below</p>
|
||||
# cd to project directory (eg: maxun)
|
||||
<br />
|
||||
cd maxun
|
||||
<br />
|
||||
<br />
|
||||
# stop the working containers
|
||||
<br />
|
||||
docker-compose down
|
||||
<br />
|
||||
<br />
|
||||
# pull latest docker images
|
||||
<br />
|
||||
docker-compose pull
|
||||
<br />
|
||||
<br />
|
||||
# start maxun
|
||||
<br />
|
||||
docker-compose up -d
|
||||
</code>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Modal>
|
||||
<iframe src="https://ghbtns.com/github-btn.html?user=getmaxun&repo=maxun&type=star&count=true&size=large" frameBorder="0" scrolling="0" width="170" height="30" title="GitHub"></iframe>
|
||||
<IconButton onClick={handleMenuOpen} sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderRadius: '5px',
|
||||
padding: '8px',
|
||||
marginRight: '10px',
|
||||
'&:hover': { backgroundColor: 'white', color: '#ff00c3' }
|
||||
}}>
|
||||
<AccountCircle sx={{ marginRight: '5px' }} />
|
||||
<Typography variant="body1">{user.email}</Typography>
|
||||
</IconButton>
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
PaperProps={{ sx: { width: '180px' } }}
|
||||
>
|
||||
<MenuItem onClick={() => { handleMenuClose(); logout(); }}>
|
||||
<Logout sx={{ marginRight: '5px' }} /> {t('navbar.menu_items.logout')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => {
|
||||
window.open('https://discord.gg/5GbPjBUkws', '_blank');
|
||||
}}>
|
||||
<DiscordIcon sx={{ marginRight: '5px' }} /> Discord
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => {
|
||||
window.open('https://www.youtube.com/@MaxunOSS/videos?ref=app', '_blank');
|
||||
}}>
|
||||
<YouTube sx={{ marginRight: '5px' }} /> YouTube
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => {
|
||||
window.open('https://x.com/maxun_io?ref=app', '_blank');
|
||||
}}>
|
||||
<X sx={{ marginRight: '5px' }} /> Twitter (X)
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleLangMenuOpen}>
|
||||
<Language sx={{ marginRight: '5px' }} /> {t('navbar.menu_items.language')}
|
||||
</MenuItem>
|
||||
<Menu
|
||||
anchorEl={langAnchorEl}
|
||||
open={Boolean(langAnchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "right",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
changeLanguage("en");
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
English
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
changeLanguage("es");
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
Español
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
changeLanguage("ja");
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
日本語
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
changeLanguage("zh");
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
中文
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
changeLanguage("de");
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
Deutsch
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Menu>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IconButton onClick={goToMainMenu} sx={{
|
||||
borderRadius: '5px',
|
||||
padding: '8px',
|
||||
background: 'red',
|
||||
color: 'white',
|
||||
marginRight: '10px',
|
||||
'&:hover': { color: 'white', backgroundColor: 'red' }
|
||||
}}>
|
||||
<Clear sx={{ marginRight: '5px' }} />
|
||||
{t('navbar.recording.discard')}
|
||||
</IconButton>
|
||||
<SaveRecording fileName={recordingName} />
|
||||
</>
|
||||
)}
|
||||
</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>
|
||||
<Menu
|
||||
anchorEl={langAnchorEl}
|
||||
open={Boolean(langAnchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "right",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
changeLanguage("en");
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
English
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
changeLanguage("es");
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
Español
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
changeLanguage("ja");
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
日本語
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
changeLanguage("zh");
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
中文
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
changeLanguage("de");
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
Deutsch
|
||||
</MenuItem>
|
||||
</Menu></>
|
||||
)}
|
||||
</NavBarWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { FC, useState } from 'react';
|
||||
import { Stack, Button, IconButton, Tooltip, Chip, Badge } from "@mui/material";
|
||||
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";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
@@ -19,6 +20,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { stopRecording } from "../../api/recording";
|
||||
import { GenericModal } from '../atoms/GenericModal';
|
||||
|
||||
|
||||
/** TODO:
|
||||
* 1. allow editing existing robot after persisting browser steps
|
||||
*/
|
||||
@@ -31,31 +33,6 @@ interface Column {
|
||||
format?: (value: string) => string;
|
||||
}
|
||||
|
||||
const columns: readonly Column[] = [
|
||||
{ id: 'interpret', label: 'Run', minWidth: 80 },
|
||||
{ id: 'name', label: 'Name', minWidth: 80 },
|
||||
{
|
||||
id: 'schedule',
|
||||
label: 'Schedule',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
id: 'integrate',
|
||||
label: 'Integrate',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
label: 'Settings',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
id: 'options',
|
||||
label: 'Options',
|
||||
minWidth: 80,
|
||||
},
|
||||
];
|
||||
|
||||
interface Data {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -76,12 +53,38 @@ interface RecordingsTableProps {
|
||||
}
|
||||
|
||||
export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handleScheduleRecording, handleIntegrateRecording, handleSettingsRecording, handleEditRobot, handleDuplicateRobot }: RecordingsTableProps) => {
|
||||
const {t} = useTranslation();
|
||||
const [page, setPage] = React.useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = React.useState(10);
|
||||
const [rows, setRows] = React.useState<Data[]>([]);
|
||||
const [isModalOpen, setModalOpen] = React.useState(false);
|
||||
const [searchTerm, setSearchTerm] = React.useState('');
|
||||
|
||||
const columns: readonly Column[] = [
|
||||
{ id: 'interpret', label: t('recordingtable.run'), minWidth: 80 },
|
||||
{ id: 'name', label: t('recordingtable.name'), minWidth: 80 },
|
||||
{
|
||||
id: 'schedule',
|
||||
label: t('recordingtable.schedule'),
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
id: 'integrate',
|
||||
label: t('recordingtable.integrate'),
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
label: t('recordingtable.settings'),
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
id: 'options',
|
||||
label: t('recordingtable.options'),
|
||||
minWidth: 80,
|
||||
},
|
||||
];
|
||||
|
||||
const { notify, setRecordings, browserId, setBrowserId, recordingUrl, setRecordingUrl, recordingName, setRecordingName, recordingId, setRecordingId } = useGlobalInfoStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -151,16 +154,17 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
|
||||
row.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="h6" gutterBottom>
|
||||
My Robots
|
||||
{t('recordingtable.heading')}
|
||||
</Typography>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<TextField
|
||||
size="small"
|
||||
placeholder="Search robots..."
|
||||
placeholder={t('recordingtable.search')}
|
||||
value={searchTerm}
|
||||
onChange={handleSearchChange}
|
||||
InputProps={{
|
||||
@@ -187,7 +191,7 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
|
||||
'&:hover': { color: 'white', backgroundColor: '#ff00c3' }
|
||||
}}
|
||||
>
|
||||
<Add sx={{ marginRight: '5px' }} /> Create Robot
|
||||
<Add sx={{ marginRight: '5px' }} /> {t('recordingtable.new')}
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -253,14 +257,14 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
|
||||
|
||||
checkRunsForRecording(row.id).then((result: boolean) => {
|
||||
if (result) {
|
||||
notify('warning', 'Cannot delete robot as it has associated runs');
|
||||
notify('warning', t('recordingtable.notifications.delete_warning'));
|
||||
}
|
||||
})
|
||||
|
||||
deleteRecordingFromStorage(row.id).then((result: boolean) => {
|
||||
if (result) {
|
||||
setRows([]);
|
||||
notify('success', 'Robot deleted successfully');
|
||||
notify('success', t('recordingtable.notifications.delete_success'));
|
||||
fetchRecordings();
|
||||
}
|
||||
})
|
||||
@@ -297,9 +301,9 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
|
||||
/>
|
||||
<GenericModal isOpen={isModalOpen} onClose={() => setModalOpen(false)} modalStyle={modalStyle}>
|
||||
<div style={{ padding: '20px' }}>
|
||||
<Typography variant="h6" gutterBottom>Enter URL To Extract Data</Typography>
|
||||
<Typography variant="h6" gutterBottom>{t('recordingtable.modal.title')}</Typography>
|
||||
<TextField
|
||||
label="URL"
|
||||
label={t('recordingtable.modal.label')}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={recordingUrl}
|
||||
@@ -312,7 +316,7 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
|
||||
onClick={startRecording}
|
||||
disabled={!recordingUrl}
|
||||
>
|
||||
Start Training Robot
|
||||
{t('recordingtable.modal.button')}
|
||||
</Button>
|
||||
</div>
|
||||
</GenericModal>
|
||||
@@ -397,6 +401,8 @@ const OptionsButton = ({ handleEdit, handleDelete, handleDuplicate }: OptionsBut
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const {t} = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
@@ -415,19 +421,21 @@ const OptionsButton = ({ handleEdit, handleDelete, handleDuplicate }: OptionsBut
|
||||
<ListItemIcon>
|
||||
<Edit fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Edit</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => { handleDuplicate(); handleClose(); }}>
|
||||
<ListItemIcon>
|
||||
<ContentCopy fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Duplicate</ListItemText>
|
||||
<ListItemText>{t('recordingtable.edit')}</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onClick={() => { handleDelete(); handleClose(); }}>
|
||||
<ListItemIcon>
|
||||
<DeleteForever fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Delete</ListItemText>
|
||||
<ListItemText>{t('recordingtable.delete')}</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onClick={() => { handleDuplicate(); handleClose(); }}>
|
||||
<ListItemIcon>
|
||||
<ContentCopy fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('recordingtable.duplicate')}</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { GenericModal } from "../atoms/GenericModal";
|
||||
import { TextField, Typography, Box, Button, Chip } from "@mui/material";
|
||||
import { TextField, Typography, Box, Button } from "@mui/material";
|
||||
import { modalStyle } from "./AddWhereCondModal";
|
||||
import { useGlobalInfoStore } from '../../context/globalInfo';
|
||||
import { duplicateRecording, getStoredRecording } from '../../api/storage';
|
||||
import { WhereWhatPair } from 'maxun-core';
|
||||
import { getUserById } from "../../api/auth";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface RobotMeta {
|
||||
name: string;
|
||||
@@ -54,6 +55,7 @@ interface RobotSettingsProps {
|
||||
}
|
||||
|
||||
export const RobotDuplicationModal = ({ isOpen, handleStart, handleClose, initialSettings }: RobotSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [robot, setRobot] = useState<RobotSettings | null>(null);
|
||||
const [targetUrl, setTargetUrl] = useState<string | undefined>('');
|
||||
const { recordingId, notify } = useGlobalInfoStore();
|
||||
@@ -65,7 +67,6 @@ export const RobotDuplicationModal = ({ isOpen, handleStart, handleClose, initia
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
// Update the targetUrl when the robot data is loaded
|
||||
if (robot) {
|
||||
const lastPair = robot?.recording.workflow[robot?.recording.workflow.length - 1];
|
||||
const url = lastPair?.what.find(action => action.action === "goto")?.args?.[0];
|
||||
@@ -78,43 +79,36 @@ export const RobotDuplicationModal = ({ isOpen, handleStart, handleClose, initia
|
||||
const robot = await getStoredRecording(recordingId);
|
||||
setRobot(robot);
|
||||
} else {
|
||||
notify('error', 'Could not find robot details. Please try again.');
|
||||
notify('error', t('robot_duplication.notifications.robot_not_found'));
|
||||
}
|
||||
}
|
||||
|
||||
// const lastPair = robot?.recording.workflow[robot?.recording.workflow.length - 1];
|
||||
|
||||
// // Find the `goto` action in `what` and retrieve its arguments
|
||||
// const targetUrl = lastPair?.what.find(action => action.action === "goto")?.args?.[0];
|
||||
|
||||
const handleTargetUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTargetUrl(e.target.value);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!robot || !targetUrl) {
|
||||
notify('error', 'Target URL is required.');
|
||||
notify('error', t('robot_duplication.notifications.url_required'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("handle save");
|
||||
|
||||
try {
|
||||
const success = await duplicateRecording(robot.recording_meta.id, targetUrl);
|
||||
|
||||
if (success) {
|
||||
notify('success', 'Robot duplicated successfully.');
|
||||
handleStart(robot); // Inform parent about the updated robot
|
||||
notify('success', t('robot_duplication.notifications.duplicate_success'));
|
||||
handleStart(robot);
|
||||
handleClose();
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
notify('error', 'Failed to update the Target URL. Please try again.');
|
||||
notify('error', t('robot_duplication.notifications.duplicate_error'));
|
||||
}
|
||||
} catch (error) {
|
||||
notify('error', 'An error occurred while updating the Target URL.');
|
||||
notify('error', t('robot_duplication.notifications.unknown_error'));
|
||||
console.error('Error updating Target URL:', error);
|
||||
}
|
||||
};
|
||||
@@ -126,34 +120,40 @@ export const RobotDuplicationModal = ({ isOpen, handleStart, handleClose, initia
|
||||
modalStyle={modalStyle}
|
||||
>
|
||||
<>
|
||||
<Typography variant="h5" style={{ marginBottom: '20px' }}>Duplicate Robot</Typography>
|
||||
<Typography variant="h5" style={{ marginBottom: '20px' }}>
|
||||
{t('robot_duplication.title')}
|
||||
</Typography>
|
||||
<Box style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{
|
||||
robot && (
|
||||
<>
|
||||
<span>Robot duplication is useful to extract data from pages with the same structure.</span>
|
||||
<span>
|
||||
{t('robot_duplication.descriptions.purpose')}
|
||||
</span>
|
||||
<br />
|
||||
<span dangerouslySetInnerHTML={{
|
||||
__html: t('robot_duplication.descriptions.example', {
|
||||
url1: '<code>producthunt.com/topics/api</code>',
|
||||
url2: '<code>producthunt.com/topics/database</code>'
|
||||
})
|
||||
}}/>
|
||||
<br />
|
||||
<span>
|
||||
Example: If you've created a robot for <code>producthunt.com/topics/api</code>, you can duplicate it to scrape similar pages
|
||||
like <code>producthunt.com/topics/database</code> without training a robot from scratch.
|
||||
</span>
|
||||
<br />
|
||||
<span>
|
||||
<b>⚠️ Ensure the new page has the same structure as the original page.</b>
|
||||
<b>{t('robot_duplication.descriptions.warning')}</b>
|
||||
</span>
|
||||
<TextField
|
||||
label="Robot Target URL"
|
||||
key="Robot Target URL"
|
||||
label={t('robot_duplication.fields.target_url')}
|
||||
key={t('robot_duplication.fields.target_url')}
|
||||
value={targetUrl}
|
||||
onChange={handleTargetUrlChange}
|
||||
style={{ marginBottom: '20px', marginTop: '30px' }}
|
||||
/>
|
||||
<Box mt={2} display="flex" justifyContent="flex-end" onClick={handleSave}>
|
||||
<Button variant="contained" color="primary">
|
||||
Duplicate Robot
|
||||
<Box mt={2} display="flex" justifyContent="flex-end">
|
||||
<Button variant="contained" color="primary" onClick={handleSave}>
|
||||
{t('robot_duplication.buttons.duplicate')}
|
||||
</Button>
|
||||
<Button onClick={handleClose} color="primary" variant="outlined" style={{ marginLeft: '10px' }}>
|
||||
Cancel
|
||||
{t('robot_duplication.buttons.cancel')}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { GenericModal } from "../atoms/GenericModal";
|
||||
import { TextField, Typography, Box, Button } from "@mui/material";
|
||||
import { modalStyle } from "./AddWhereCondModal";
|
||||
@@ -54,10 +55,10 @@ interface RobotSettingsProps {
|
||||
handleStart: (settings: RobotSettings) => void;
|
||||
handleClose: () => void;
|
||||
initialSettings?: RobotSettings | null;
|
||||
|
||||
}
|
||||
|
||||
export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettings }: RobotSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [robot, setRobot] = useState<RobotSettings | null>(null);
|
||||
const { recordingId, notify } = useGlobalInfoStore();
|
||||
|
||||
@@ -72,7 +73,7 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin
|
||||
const robot = await getStoredRecording(recordingId);
|
||||
setRobot(robot);
|
||||
} else {
|
||||
notify('error', 'Could not find robot details. Please try again.');
|
||||
notify('error', t('robot_edit.notifications.update_failed'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +103,7 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin
|
||||
return { ...prev, recording: { ...prev.recording, workflow: updatedWorkflow } };
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!robot) return;
|
||||
|
||||
@@ -114,7 +116,7 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin
|
||||
const success = await updateRecording(robot.recording_meta.id, payload);
|
||||
|
||||
if (success) {
|
||||
notify('success', 'Robot updated successfully.');
|
||||
notify('success', t('robot_edit.notifications.update_success'));
|
||||
handleStart(robot); // Inform parent about the updated robot
|
||||
handleClose();
|
||||
|
||||
@@ -122,10 +124,10 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
notify('error', 'Failed to update the robot. Please try again.');
|
||||
notify('error', t('robot_edit.notifications.update_failed'));
|
||||
}
|
||||
} catch (error) {
|
||||
notify('error', 'An error occurred while updating the robot.');
|
||||
notify('error', t('robot_edit.notifications.update_error'));
|
||||
console.error('Error updating robot:', error);
|
||||
}
|
||||
};
|
||||
@@ -137,14 +139,16 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin
|
||||
modalStyle={modalStyle}
|
||||
>
|
||||
<>
|
||||
<Typography variant="h5" style={{ marginBottom: '20px' }}>Edit Robot</Typography>
|
||||
<Typography variant="h5" style={{ marginBottom: '20px' }}>
|
||||
{t('robot_edit.title')}
|
||||
</Typography>
|
||||
<Box style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{
|
||||
robot && (
|
||||
<>
|
||||
<TextField
|
||||
label="Change Robot Name"
|
||||
key="Change Robot Name"
|
||||
label={t('robot_edit.change_name')}
|
||||
key="Robot Name"
|
||||
type='text'
|
||||
value={robot.recording_meta.name}
|
||||
onChange={(e) => handleRobotNameChange(e.target.value)}
|
||||
@@ -152,7 +156,7 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin
|
||||
/>
|
||||
{robot.recording.workflow?.[0]?.what?.[0]?.args?.[0]?.limit !== undefined && (
|
||||
<TextField
|
||||
label="Robot Limit"
|
||||
label={t('robot_edit.robot_limit')}
|
||||
type="number"
|
||||
value={robot.recording.workflow[0].what[0].args[0].limit || ''}
|
||||
onChange={(e) =>{
|
||||
@@ -166,12 +170,17 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box mt={2} display="flex" justifyContent="flex-end" onClick={handleSave}>
|
||||
<Button variant="contained" color="primary">
|
||||
Save Changes
|
||||
<Box mt={2} display="flex" justifyContent="flex-end">
|
||||
<Button variant="contained" color="primary" onClick={handleSave}>
|
||||
{t('robot_edit.save')}
|
||||
</Button>
|
||||
<Button onClick={handleClose} color="primary" variant="outlined" style={{ marginLeft: '10px' }}>
|
||||
Cancel
|
||||
<Button
|
||||
onClick={handleClose}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
style={{ marginLeft: '10px' }}
|
||||
>
|
||||
{t('robot_edit.cancel')}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { GenericModal } from "../atoms/GenericModal";
|
||||
import { TextField, Typography, Box } from "@mui/material";
|
||||
import { modalStyle } from "./AddWhereCondModal";
|
||||
@@ -50,10 +51,10 @@ interface RobotSettingsProps {
|
||||
handleStart: (settings: RobotSettings) => void;
|
||||
handleClose: () => void;
|
||||
initialSettings?: RobotSettings | null;
|
||||
|
||||
}
|
||||
|
||||
export const RobotSettingsModal = ({ isOpen, handleStart, handleClose, initialSettings }: RobotSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [robot, setRobot] = useState<RobotSettings | null>(null);
|
||||
const [userEmail, setUserEmail] = useState<string | null>(null);
|
||||
const { recordingId, notify } = useGlobalInfoStore();
|
||||
@@ -69,7 +70,7 @@ export const RobotSettingsModal = ({ isOpen, handleStart, handleClose, initialSe
|
||||
const robot = await getStoredRecording(recordingId);
|
||||
setRobot(robot);
|
||||
} else {
|
||||
notify('error', 'Could not find robot details. Please try again.');
|
||||
notify('error', t('robot_settings.errors.robot_not_found'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,13 +98,15 @@ export const RobotSettingsModal = ({ isOpen, handleStart, handleClose, initialSe
|
||||
modalStyle={modalStyle}
|
||||
>
|
||||
<>
|
||||
<Typography variant="h5" style={{ marginBottom: '20px' }}>Robot Settings</Typography>
|
||||
<Typography variant="h5" style={{ marginBottom: '20px' }}>
|
||||
{t('robot_settings.title')}
|
||||
</Typography>
|
||||
<Box style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{
|
||||
robot && (
|
||||
<>
|
||||
<TextField
|
||||
label="Robot Target URL"
|
||||
label={t('robot_settings.target_url')}
|
||||
key="Robot Target URL"
|
||||
value={targetUrl}
|
||||
InputProps={{
|
||||
@@ -112,7 +115,7 @@ export const RobotSettingsModal = ({ isOpen, handleStart, handleClose, initialSe
|
||||
style={{ marginBottom: '20px' }}
|
||||
/>
|
||||
<TextField
|
||||
label="Robot ID"
|
||||
label={t('robot_settings.robot_id')}
|
||||
key="Robot ID"
|
||||
value={robot.recording_meta.id}
|
||||
InputProps={{
|
||||
@@ -122,17 +125,17 @@ export const RobotSettingsModal = ({ isOpen, handleStart, handleClose, initialSe
|
||||
/>
|
||||
{robot.recording.workflow?.[0]?.what?.[0]?.args?.[0]?.limit !== undefined && (
|
||||
<TextField
|
||||
label="Robot Limit"
|
||||
label={t('robot_settings.robot_limit')}
|
||||
type="number"
|
||||
value={robot.recording.workflow[0].what[0].args[0].limit || ''}
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
}}
|
||||
readOnly: true,
|
||||
}}
|
||||
style={{ marginBottom: '20px' }}
|
||||
/>
|
||||
)}
|
||||
<TextField
|
||||
label="Created By User"
|
||||
label={t('robot_settings.created_by_user')}
|
||||
key="Created By User"
|
||||
value={userEmail ? userEmail : ''}
|
||||
InputProps={{
|
||||
@@ -141,7 +144,7 @@ export const RobotSettingsModal = ({ isOpen, handleStart, handleClose, initialSe
|
||||
style={{ marginBottom: '20px' }}
|
||||
/>
|
||||
<TextField
|
||||
label="Robot Created At"
|
||||
label={t('robot_settings.created_at')}
|
||||
key="Robot Created At"
|
||||
value={robot.recording_meta.createdAt}
|
||||
InputProps={{
|
||||
@@ -156,4 +159,4 @@ export const RobotSettingsModal = ({ isOpen, handleStart, handleClose, initialSe
|
||||
</>
|
||||
</GenericModal>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -13,6 +13,7 @@ import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import 'highlight.js/styles/github.css';
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface RunContentProps {
|
||||
row: Data,
|
||||
@@ -23,6 +24,7 @@ interface RunContentProps {
|
||||
}
|
||||
|
||||
export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRef, abortRunHandler }: RunContentProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [tab, setTab] = React.useState<string>('log');
|
||||
const [tableData, setTableData] = useState<any[]>([]);
|
||||
const [columns, setColumns] = useState<string[]>([]);
|
||||
@@ -76,49 +78,49 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
|
||||
<TabContext value={tab}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<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="Output Data"
|
||||
value='output'
|
||||
sx={{
|
||||
color: (theme) => theme.palette.mode === 'dark' ? '#fff' : '#000',
|
||||
'&:hover': {
|
||||
color: '#FF00C3'
|
||||
},
|
||||
'&.Mui-selected': {
|
||||
color: '#FF00C3',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Tab
|
||||
label="Log"
|
||||
value='log'
|
||||
sx={{
|
||||
color: (theme) => theme.palette.mode === 'dark' ? '#fff' : '#000',
|
||||
'&:hover': {
|
||||
color: '#FF00C3'
|
||||
},
|
||||
'&.Mui-selected': {
|
||||
color: '#FF00C3',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</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'>
|
||||
<Box sx={{
|
||||
@@ -142,32 +144,32 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
|
||||
color="error"
|
||||
onClick={abortRunHandler}
|
||||
>
|
||||
Stop
|
||||
{t('run_content.buttons.stop')}
|
||||
</Button> : null}
|
||||
</TabPanel>
|
||||
<TabPanel value='output' sx={{ width: '700px' }}>
|
||||
{!row || !row.serializableOutput || !row.binaryOutput
|
||||
|| (Object.keys(row.serializableOutput).length === 0 && Object.keys(row.binaryOutput).length === 0)
|
||||
? <Typography>The output is empty.</Typography> : null}
|
||||
? <Typography>{t('run_content.empty_output')}</Typography> : null}
|
||||
|
||||
{row.serializableOutput &&
|
||||
Object.keys(row.serializableOutput).length !== 0 &&
|
||||
<div>
|
||||
<Typography variant='h6' sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<ArticleIcon sx={{ marginRight: '15px' }} />
|
||||
Captured Data
|
||||
{t('run_content.captured_data.title')}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 2 }}>
|
||||
<Typography>
|
||||
<a style={{ textDecoration: 'none' }} href={`data:application/json;utf8,${JSON.stringify(row.serializableOutput, null, 2)}`}
|
||||
download="data.json">
|
||||
Download as JSON
|
||||
{t('run_content.captured_data.download_json')}
|
||||
</a>
|
||||
</Typography>
|
||||
<Typography
|
||||
onClick={downloadCSV}
|
||||
>
|
||||
<a style={{ textDecoration: 'none', cursor: 'pointer' }}>Download as CSV</a>
|
||||
<a style={{ textDecoration: 'none', cursor: 'pointer' }}>{t('run_content.captured_data.download_csv')}</a>
|
||||
</Typography>
|
||||
</Box>
|
||||
{tableData.length > 0 ? (
|
||||
@@ -212,7 +214,7 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
|
||||
<div>
|
||||
<Typography variant='h6' sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<ImageIcon sx={{ marginRight: '15px' }} />
|
||||
Captured Screenshot
|
||||
{t('run_content.captured_screenshot.title')}
|
||||
</Typography>
|
||||
{Object.keys(row.binaryOutput).map((key) => {
|
||||
try {
|
||||
@@ -222,7 +224,7 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
|
||||
width: 'max-content',
|
||||
}}>
|
||||
<Typography sx={{ margin: '20px 0px' }}>
|
||||
<a href={imageUrl} download={key} style={{ textDecoration: 'none' }}>Download Screenshot</a>
|
||||
<a href={imageUrl} download={key} style={{ textDecoration: 'none' }}>{t('run_content.captured_screenshot.download')}</a>
|
||||
</Typography>
|
||||
<img src={imageUrl} alt={key} height='auto' width='700px' />
|
||||
</Box>
|
||||
@@ -230,7 +232,7 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return <Typography key={`number-of-binary-output-${key}`}>
|
||||
{key}: The image failed to render
|
||||
{key}: {t('run_content.captured_screenshot.render_failed')}
|
||||
</Typography>
|
||||
}
|
||||
})}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
@@ -7,14 +9,24 @@ import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TablePagination from '@mui/material/TablePagination';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import { useEffect, useState } from "react";
|
||||
import { Accordion, AccordionSummary, AccordionDetails, Typography, Box, TextField } from '@mui/material';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
|
||||
import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||
import { getStoredRuns } from "../../api/storage";
|
||||
import { RunSettings } from "./RunSettings";
|
||||
import { CollapsibleRow } from "./ColapsibleRow";
|
||||
import { Accordion, AccordionSummary, AccordionDetails, Typography, Box, TextField } from '@mui/material';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
|
||||
// Export columns before the component
|
||||
export const columns: readonly Column[] = [
|
||||
{ id: 'runStatus', label: 'Status', minWidth: 80 },
|
||||
{ id: 'name', label: 'Name', minWidth: 80 },
|
||||
{ id: 'startedAt', label: 'Started At', minWidth: 80 },
|
||||
{ id: 'finishedAt', label: 'Finished At', minWidth: 80 },
|
||||
{ id: 'settings', label: 'Settings', minWidth: 80 },
|
||||
{ id: 'delete', label: 'Delete', minWidth: 80 },
|
||||
];
|
||||
|
||||
interface Column {
|
||||
id: 'runStatus' | 'name' | 'startedAt' | 'finishedAt' | 'delete' | 'settings';
|
||||
@@ -24,15 +36,6 @@ interface Column {
|
||||
format?: (value: string) => string;
|
||||
}
|
||||
|
||||
export const columns: readonly Column[] = [
|
||||
{ id: 'runStatus', label: 'Status', minWidth: 80 },
|
||||
{ id: 'name', label: 'Robot Name', minWidth: 80 },
|
||||
{ id: 'startedAt', label: 'Started at', minWidth: 80 },
|
||||
{ id: 'finishedAt', label: 'Finished at', minWidth: 80 },
|
||||
{ id: 'settings', label: 'Settings', minWidth: 80 },
|
||||
{ id: 'delete', label: 'Delete', minWidth: 80 },
|
||||
];
|
||||
|
||||
export interface Data {
|
||||
id: number;
|
||||
status: string;
|
||||
@@ -58,15 +61,25 @@ interface RunsTableProps {
|
||||
runningRecordingName: string;
|
||||
}
|
||||
|
||||
export const RunsTable = (
|
||||
{ currentInterpretationLog, abortRunHandler, runId, runningRecordingName }: RunsTableProps) => {
|
||||
export const RunsTable: React.FC<RunsTableProps> = ({
|
||||
currentInterpretationLog,
|
||||
abortRunHandler,
|
||||
runId,
|
||||
runningRecordingName
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Update column labels using translation if needed
|
||||
const translatedColumns = columns.map(column => ({
|
||||
...column,
|
||||
label: t(`runstable.${column.id}`, column.label)
|
||||
}));
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const [rows, setRows] = useState<Data[]>([]);
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
|
||||
const { notify, rerenderRuns, setRerenderRuns } = useGlobalInfoStore();
|
||||
|
||||
const handleChangePage = (event: unknown, newPage: number) => {
|
||||
@@ -86,16 +99,13 @@ export const RunsTable = (
|
||||
const fetchRuns = async () => {
|
||||
const runs = await getStoredRuns();
|
||||
if (runs) {
|
||||
const parsedRows: Data[] = [];
|
||||
runs.map((run: any, index) => {
|
||||
parsedRows.push({
|
||||
id: index,
|
||||
...run,
|
||||
});
|
||||
});
|
||||
const parsedRows: Data[] = runs.map((run: any, index: number) => ({
|
||||
id: index,
|
||||
...run,
|
||||
}));
|
||||
setRows(parsedRows);
|
||||
} else {
|
||||
notify('error', 'No runs found. Please try again.')
|
||||
notify('error', t('runstable.notifications.no_runs'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -104,15 +114,14 @@ export const RunsTable = (
|
||||
fetchRuns();
|
||||
setRerenderRuns(false);
|
||||
}
|
||||
}, [rerenderRuns]);
|
||||
}, [rerenderRuns, rows.length, setRerenderRuns]);
|
||||
|
||||
const handleDelete = () => {
|
||||
setRows([]);
|
||||
notify('success', 'Run deleted successfully');
|
||||
notify('success', t('runstable.notifications.delete_success'));
|
||||
fetchRuns();
|
||||
};
|
||||
|
||||
|
||||
// Filter rows based on search term
|
||||
const filteredRows = rows.filter((row) =>
|
||||
row.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
@@ -120,7 +129,6 @@ export const RunsTable = (
|
||||
|
||||
// Group filtered rows by robot meta id
|
||||
const groupedRows = filteredRows.reduce((acc, row) => {
|
||||
|
||||
if (!acc[row.robotMetaId]) {
|
||||
acc[row.robotMetaId] = [];
|
||||
}
|
||||
@@ -132,11 +140,11 @@ export const RunsTable = (
|
||||
<React.Fragment>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
All Runs
|
||||
{t('runstable.runs', 'Runs')}
|
||||
</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
placeholder="Search runs..."
|
||||
placeholder={t('runstable.search', 'Search runs...')}
|
||||
value={searchTerm}
|
||||
onChange={handleSearchChange}
|
||||
InputProps={{
|
||||
@@ -149,16 +157,14 @@ export const RunsTable = (
|
||||
{Object.entries(groupedRows).map(([id, data]) => (
|
||||
<Accordion key={id}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
|
||||
<Typography variant="h6">{data[data.length - 1].name}</Typography>
|
||||
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Table stickyHeader aria-label="sticky table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell />
|
||||
{columns.map((column) => (
|
||||
{translatedColumns.map((column) => (
|
||||
<TableCell
|
||||
key={column.id}
|
||||
align={column.align}
|
||||
@@ -200,4 +206,4 @@ export const RunsTable = (
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -9,13 +9,14 @@ import { TextField, Typography } from "@mui/material";
|
||||
import { WarningText } from "../atoms/texts";
|
||||
import NotificationImportantIcon from "@mui/icons-material/NotificationImportant";
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface SaveRecordingProps {
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export const SaveRecording = ({ fileName }: SaveRecordingProps) => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [openModal, setOpenModal] = useState<boolean>(false);
|
||||
const [needConfirm, setNeedConfirm] = useState<boolean>(false);
|
||||
const [recordingName, setRecordingName] = useState<string>(fileName);
|
||||
@@ -46,7 +47,7 @@ export const SaveRecording = ({ fileName }: SaveRecordingProps) => {
|
||||
};
|
||||
|
||||
const exitRecording = useCallback(async () => {
|
||||
notify('success', 'Robot saved successfully');
|
||||
notify('success', t('save_recording.notifications.save_success'));
|
||||
if (browserId) {
|
||||
await stopRecording(browserId);
|
||||
}
|
||||
@@ -63,7 +64,7 @@ export const SaveRecording = ({ fileName }: SaveRecordingProps) => {
|
||||
setWaitingForSave(true);
|
||||
console.log(`Saving the recording as ${recordingName} for userId ${user.id}`);
|
||||
} else {
|
||||
console.error('User not logged in. Cannot save recording.');
|
||||
console.error(t('save_recording.notifications.user_not_logged'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -76,35 +77,42 @@ export const SaveRecording = ({ fileName }: SaveRecordingProps) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button onClick={() => setOpenModal(true)} variant='contained' sx={{ marginRight: '20px',backgroundColor: '#ff00c3',color: 'white' }} size="small" color="success">
|
||||
Finish
|
||||
<!-- <Button onClick={() => setOpenModal(true)} variant='contained' sx={{ marginRight: '20px',backgroundColor: '#ff00c3',color: 'white' }} size="small" color="success">
|
||||
Finish -->
|
||||
|
||||
<Button onClick={() => setOpenModal(true)} variant="outlined" sx={{ marginRight: '20px' }} size="small" color="success">
|
||||
{t('right_panel.buttons.finish')}
|
||||
</Button>
|
||||
|
||||
<GenericModal isOpen={openModal} onClose={() => setOpenModal(false)} modalStyle={modalStyle}>
|
||||
<form onSubmit={handleSaveRecording} style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Typography variant="h6">Save Robot</Typography>
|
||||
<Typography variant="h6">{t('save_recording.title')}</Typography>
|
||||
<TextField
|
||||
required
|
||||
sx={{ width: '300px', margin: '15px 0px' }}
|
||||
onChange={handleChangeOfTitle}
|
||||
id="title"
|
||||
label="Robot Name"
|
||||
label={t('save_recording.robot_name')}
|
||||
variant="outlined"
|
||||
defaultValue={recordingName ? recordingName : null}
|
||||
/>
|
||||
{needConfirm
|
||||
?
|
||||
(<React.Fragment>
|
||||
<Button color="error" variant="contained" onClick={saveRecording} sx={{ marginTop: '10px' }}>Confirm</Button>
|
||||
<Button color="error" variant="contained" onClick={saveRecording} sx={{ marginTop: '10px' }}>
|
||||
{t('save_recording.buttons.confirm')}
|
||||
</Button>
|
||||
<WarningText>
|
||||
<NotificationImportantIcon color="warning" />
|
||||
Robot with this name already exists, please confirm the Robot's overwrite.
|
||||
{t('save_recording.errors.exists_warning')}
|
||||
</WarningText>
|
||||
</React.Fragment>)
|
||||
: <Button type="submit" variant="contained" sx={{ marginTop: '10px' }}>Save</Button>
|
||||
: <Button type="submit" variant="contained" sx={{ marginTop: '10px' }}>
|
||||
{t('save_recording.buttons.save')}
|
||||
</Button>
|
||||
}
|
||||
{waitingForSave &&
|
||||
<Tooltip title='Optimizing and saving the workflow' placement={"bottom"}>
|
||||
<Tooltip title={t('save_recording.tooltips.optimizing')} placement={"bottom"}>
|
||||
<Box sx={{ width: '100%', marginTop: '10px' }}>
|
||||
<LinearProgress />
|
||||
</Box>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { GenericModal } from "../atoms/GenericModal";
|
||||
import { MenuItem, TextField, Typography, Box } from "@mui/material";
|
||||
import { Dropdown } from "../atoms/DropdownMui";
|
||||
@@ -25,6 +26,7 @@ export interface ScheduleSettings {
|
||||
}
|
||||
|
||||
export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initialSettings }: ScheduleSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [schedule, setSchedule] = useState<ScheduleSettings | null>(null);
|
||||
const [settings, setSettings] = useState<ScheduleSettings>({
|
||||
runEvery: 1,
|
||||
@@ -77,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');
|
||||
}
|
||||
@@ -116,6 +119,25 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initia
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const getDayOrdinal = (day: string | undefined) => {
|
||||
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');
|
||||
case '2': return t('schedule_settings.labels.on_day.nd');
|
||||
case '3': return t('schedule_settings.labels.on_day.rd');
|
||||
default: return t('schedule_settings.labels.on_day.th');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<GenericModal
|
||||
isOpen={isOpen}
|
||||
@@ -129,30 +151,30 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initia
|
||||
padding: '20px',
|
||||
'& > *': { marginBottom: '20px' },
|
||||
}}>
|
||||
<Typography variant="h6" sx={{ marginBottom: '20px' }}>Schedule Settings</Typography>
|
||||
<Typography variant="h6" sx={{ marginBottom: '20px' }}>{t('schedule_settings.title')}</Typography>
|
||||
<>
|
||||
{schedule !== null ? (
|
||||
<>
|
||||
<Typography>Run every: {schedule.runEvery} {schedule.runEveryUnit.toLowerCase()}</Typography>
|
||||
<Typography>{['MONTHS', 'WEEKS'].includes(settings.runEveryUnit) ? "Start From" : "On"} {schedule.startFrom.charAt(0).toUpperCase() + schedule.startFrom.slice(1).toLowerCase()}</Typography>
|
||||
<Typography>{t('schedule_settings.run_every')}: {schedule.runEvery} {schedule.runEveryUnit.toLowerCase()}</Typography>
|
||||
<Typography>{['MONTHS', 'WEEKS'].includes(settings.runEveryUnit) ? t('schedule_settings.start_from') : t('schedule_settings.start_from')}: {schedule.startFrom.charAt(0).toUpperCase() + schedule.startFrom.slice(1).toLowerCase()}</Typography>
|
||||
{schedule.runEveryUnit === 'MONTHS' && (
|
||||
<Typography>On day: {schedule.dayOfMonth}{['1', '21', '31'].includes(schedule.dayOfMonth || '') ? 'st' : ['2', '22'].includes(schedule.dayOfMonth || '') ? 'nd' : ['3', '23'].includes(schedule.dayOfMonth || '') ? 'rd' : 'th'} of the month</Typography>
|
||||
<Typography>{t('schedule_settings.on_day')}: {schedule.dayOfMonth}{getDayOrdinal(schedule.dayOfMonth)} of the month</Typography>
|
||||
)}
|
||||
<Typography>At around: {schedule.atTimeStart}, {schedule.timezone} Timezone</Typography>
|
||||
<Typography>{t('schedule_settings.at_around')}: {schedule.atTimeStart}, {schedule.timezone} {t('schedule_settings.timezone')}</Typography>
|
||||
<Box mt={2} display="flex" justifyContent="space-between">
|
||||
<Button
|
||||
onClick={deleteRobotSchedule}
|
||||
variant="outlined"
|
||||
color="error"
|
||||
>
|
||||
Delete Schedule
|
||||
{t('schedule_settings.buttons.delete_schedule')}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
||||
<Typography sx={{ marginRight: '10px' }}>Run once every</Typography>
|
||||
<Typography sx={{ marginRight: '10px' }}>{t('schedule_settings.labels.run_once_every')}</Typography>
|
||||
<TextField
|
||||
type="number"
|
||||
value={settings.runEvery}
|
||||
@@ -174,7 +196,9 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initia
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
||||
<Typography sx={{ marginBottom: '5px', marginRight: '25px' }}>{['MONTHS', 'WEEKS'].includes(settings.runEveryUnit) ? "Start From" : "On"}</Typography>
|
||||
<Typography sx={{ marginBottom: '5px', marginRight: '25px' }}>
|
||||
{['MONTHS', 'WEEKS'].includes(settings.runEveryUnit) ? t('schedule_settings.labels.start_from_label') : t('schedule_settings.labels.start_from_label')}
|
||||
</Typography>
|
||||
<Dropdown
|
||||
label=""
|
||||
id="startFrom"
|
||||
@@ -190,7 +214,7 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initia
|
||||
|
||||
{settings.runEveryUnit === 'MONTHS' && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
||||
<Typography sx={{ marginBottom: '5px', marginRight: '25px' }}>On Day of the Month</Typography>
|
||||
<Typography sx={{ marginBottom: '5px', marginRight: '25px' }}>{t('schedule_settings.labels.on_day_of_month')}</Typography>
|
||||
<TextField
|
||||
type="number"
|
||||
value={settings.dayOfMonth}
|
||||
@@ -204,7 +228,7 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initia
|
||||
{['MINUTES', 'HOURS'].includes(settings.runEveryUnit) ? (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
||||
<Box sx={{ marginRight: '20px' }}>
|
||||
<Typography sx={{ marginBottom: '5px' }}>In Between</Typography>
|
||||
<Typography sx={{ marginBottom: '5px' }}>{t('schedule_settings.labels.in_between')}</Typography>
|
||||
<TextField
|
||||
type="time"
|
||||
value={settings.atTimeStart}
|
||||
@@ -221,7 +245,7 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initia
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
||||
<Typography sx={{ marginBottom: '5px', marginRight: '10px' }}>At Around</Typography>
|
||||
<Typography sx={{ marginBottom: '5px', marginRight: '10px' }}>{t('schedule_settings.at_around')}</Typography>
|
||||
<TextField
|
||||
type="time"
|
||||
value={settings.atTimeStart}
|
||||
@@ -232,7 +256,7 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initia
|
||||
)}
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
||||
<Typography sx={{ marginRight: '10px' }}>Timezone</Typography>
|
||||
<Typography sx={{ marginRight: '10px' }}>{t('schedule_settings.timezone')}</Typography>
|
||||
<Dropdown
|
||||
label=""
|
||||
id="timezone"
|
||||
@@ -247,10 +271,10 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initia
|
||||
</Box>
|
||||
<Box mt={2} display="flex" justifyContent="flex-end">
|
||||
<Button onClick={() => handleStart(settings)} variant="contained" color="primary">
|
||||
Save Schedule
|
||||
{t('schedule_settings.buttons.save_schedule')}
|
||||
</Button>
|
||||
<Button onClick={handleClose} color="primary" variant="outlined" style={{ marginLeft: '10px' }}>
|
||||
Cancel
|
||||
{t('schedule_settings.buttons.cancel')}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
@@ -271,4 +295,4 @@ const modalStyle = {
|
||||
height: 'fit-content',
|
||||
display: 'block',
|
||||
padding: '20px',
|
||||
};
|
||||
};
|
||||
@@ -19,6 +19,7 @@ import styled from 'styled-components';
|
||||
import axios from 'axios';
|
||||
import { useGlobalInfoStore } from '../../context/globalInfo';
|
||||
import { apiUrl } from '../../apiConfig';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const Container = styled(Box)`
|
||||
display: flex;
|
||||
@@ -29,24 +30,21 @@ const Container = styled(Box)`
|
||||
`;
|
||||
|
||||
const ApiKeyManager = () => {
|
||||
const { t } = useTranslation();
|
||||
const [apiKey, setApiKey] = useState<string | null>(null);
|
||||
const [apiKeyName, setApiKeyName] = useState<string>('Maxun API Key');
|
||||
const [apiKeyName, setApiKeyName] = useState<string>(t('apikey.default_name'));
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [showKey, setShowKey] = useState<boolean>(false);
|
||||
const [copySuccess, setCopySuccess] = useState<boolean>(false);
|
||||
const { notify } = useGlobalInfoStore();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const fetchApiKey = async () => {
|
||||
try {
|
||||
const { data } = await axios.get(`${apiUrl}/auth/api-key`);
|
||||
setApiKey(data.api_key);
|
||||
} catch (error: any) {
|
||||
notify('error', `Failed to fetch API Key - ${error.message}`);
|
||||
notify('error', t('apikey.notifications.fetch_error', { error: error.message }));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -62,9 +60,9 @@ const ApiKeyManager = () => {
|
||||
const { data } = await axios.post(`${apiUrl}/auth/generate-api-key`);
|
||||
setApiKey(data.api_key);
|
||||
|
||||
notify('success', `Generated API Key successfully`);
|
||||
notify('success', t('apikey.notifications.generate_success'));
|
||||
} catch (error: any) {
|
||||
notify('error', `Failed to generate API Key - ${error.message}`);
|
||||
notify('error', t('apikey.notifications.generate_error', { error: error.message }));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -75,9 +73,9 @@ const ApiKeyManager = () => {
|
||||
try {
|
||||
await axios.delete(`${apiUrl}/auth/delete-api-key`);
|
||||
setApiKey(null);
|
||||
notify('success', 'API Key deleted successfully');
|
||||
notify('success', t('apikey.notifications.delete_success'));
|
||||
} catch (error: any) {
|
||||
notify('error', `Failed to delete API Key - ${error.message}`);
|
||||
notify('error', t('apikey.notifications.delete_error', { error: error.message }));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -88,7 +86,7 @@ const ApiKeyManager = () => {
|
||||
navigator.clipboard.writeText(apiKey);
|
||||
setCopySuccess(true);
|
||||
setTimeout(() => setCopySuccess(false), 2000);
|
||||
notify('info', 'Copied API Key successfully');
|
||||
notify('info', t('apikey.notifications.copy_success'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -111,34 +109,38 @@ const ApiKeyManager = () => {
|
||||
return (
|
||||
<Container sx={{ alignSelf: 'flex-start' }}>
|
||||
<Typography variant="h6" gutterBottom component="div" style={{ marginBottom: '20px' }}>
|
||||
Manage Your API Key
|
||||
{t('apikey.title')}
|
||||
</Typography>
|
||||
{apiKey ? (
|
||||
<TableContainer component={Paper} sx={{ width: '100%', overflow: 'hidden' }}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>API Key Name</TableCell>
|
||||
<TableCell>API Key</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
<TableCell>{t('apikey.table.name')}</TableCell>
|
||||
<TableCell>{t('apikey.table.key')}</TableCell>
|
||||
<TableCell>{t('apikey.table.actions')}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>{apiKeyName}</TableCell>
|
||||
<TableCell>{showKey ? `${apiKey?.substring(0, 10)}...` : '***************'}</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip title="Copy">
|
||||
<Box sx={{ fontFamily: 'monospace', width: '10ch' }}>
|
||||
{showKey ? `${apiKey?.substring(0, 10)}...` : '**********'}
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip title={t('apikey.actions.copy')}>
|
||||
<IconButton onClick={copyToClipboard}>
|
||||
<ContentCopy />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={showKey ? 'Hide' : 'Show'}>
|
||||
<Tooltip title={showKey ? t('apikey.actions.hide') : t('apikey.actions.show')}>
|
||||
<IconButton onClick={() => setShowKey(!showKey)}>
|
||||
<Visibility />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete">
|
||||
<Tooltip title={t('apikey.actions.delete')}>
|
||||
<IconButton onClick={deleteApiKey} color="error">
|
||||
<Delete />
|
||||
</IconButton>
|
||||
@@ -150,9 +152,9 @@ const ApiKeyManager = () => {
|
||||
</TableContainer>
|
||||
) : (
|
||||
<>
|
||||
<Typography>You haven't generated an API key yet.</Typography>
|
||||
<Typography>{t('apikey.no_key_message')}</Typography>
|
||||
<Button onClick={generateApiKey} variant="contained" color="primary" sx={{ marginTop: '15px' }}>
|
||||
Generate API Key
|
||||
{t('apikey.generate_button')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -7,11 +7,13 @@ import { GenericModal } from '../atoms/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;
|
||||
@@ -52,6 +54,7 @@ const getAttributeOptions = (tagName: string, elementInfo: ElementInfo | null):
|
||||
};
|
||||
|
||||
export const BrowserWindow = () => {
|
||||
const { t } = useTranslation();
|
||||
const [canvasRef, setCanvasReference] = useState<React.RefObject<HTMLCanvasElement> | undefined>(undefined);
|
||||
const [screenShot, setScreenShot] = useState<string>("");
|
||||
const [highlighterData, setHighlighterData] = useState<{ rect: DOMRect, selector: string, elementInfo: ElementInfo | null, childSelectors?: string[] } | null>(null);
|
||||
@@ -66,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) => {
|
||||
@@ -115,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 {
|
||||
} 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
|
||||
}
|
||||
} else {
|
||||
// Set highlighterData for the initial listSelector selection
|
||||
setHighlighterData(data);
|
||||
}
|
||||
} else {
|
||||
// for non-list steps
|
||||
}
|
||||
} else {
|
||||
// For non-list steps
|
||||
setHighlighterData(data);
|
||||
}
|
||||
}, [highlighterData, getList, socket, listSelector, paginationMode, paginationType]);
|
||||
}
|
||||
}, [highlighterData, getList, socket, listSelector, paginationMode, paginationType, captureStage]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
@@ -156,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();
|
||||
@@ -183,6 +240,7 @@ export const BrowserWindow = () => {
|
||||
addTextStep('', data, {
|
||||
selector: highlighterData.selector,
|
||||
tag: highlighterData.elementInfo?.tagName,
|
||||
shadow: highlighterData.elementInfo?.isShadowRoot,
|
||||
attribute
|
||||
});
|
||||
} else {
|
||||
@@ -190,7 +248,7 @@ export const BrowserWindow = () => {
|
||||
setAttributeOptions(options);
|
||||
setSelectedElement({
|
||||
selector: highlighterData.selector,
|
||||
info: highlighterData.elementInfo
|
||||
info: highlighterData.elementInfo,
|
||||
});
|
||||
setShowAttributeModal(true);
|
||||
}
|
||||
@@ -200,7 +258,7 @@ export const BrowserWindow = () => {
|
||||
// Only allow selection in pagination mode if type is not empty, 'scrollDown', or 'scrollUp'
|
||||
if (paginationType !== '' && paginationType !== 'scrollDown' && paginationType !== 'scrollUp' && paginationType !== 'none') {
|
||||
setPaginationSelector(highlighterData.selector);
|
||||
notify(`info`, `Pagination element selected successfully.`);
|
||||
notify(`info`, t('browser_window.attribute_modal.notifications.pagination_select_success'));
|
||||
addListStep(listSelector!, fields, currentListId || 0, { type: paginationType, selector: highlighterData.selector });
|
||||
}
|
||||
return;
|
||||
@@ -208,7 +266,7 @@ export const BrowserWindow = () => {
|
||||
|
||||
if (getList === true && !listSelector) {
|
||||
setListSelector(highlighterData.selector);
|
||||
notify(`info`, `List selected succesfully. Select the text data for extraction.`)
|
||||
notify(`info`, t('browser_window.attribute_modal.notifications.list_select_success'));
|
||||
setCurrentListId(Date.now());
|
||||
setFields({});
|
||||
} else if (getList === true && listSelector && currentListId) {
|
||||
@@ -227,6 +285,7 @@ export const BrowserWindow = () => {
|
||||
selectorObj: {
|
||||
selector: highlighterData.selector,
|
||||
tag: highlighterData.elementInfo?.tagName,
|
||||
shadow: highlighterData.elementInfo?.isShadowRoot,
|
||||
attribute
|
||||
}
|
||||
};
|
||||
@@ -274,6 +333,7 @@ export const BrowserWindow = () => {
|
||||
addTextStep('', data, {
|
||||
selector: selectedElement.selector,
|
||||
tag: selectedElement.info?.tagName,
|
||||
shadow: selectedElement.info?.isShadowRoot,
|
||||
attribute: attribute
|
||||
});
|
||||
}
|
||||
@@ -286,6 +346,7 @@ export const BrowserWindow = () => {
|
||||
selectorObj: {
|
||||
selector: selectedElement.selector,
|
||||
tag: selectedElement.info?.tagName,
|
||||
shadow: selectedElement.info?.isShadowRoot,
|
||||
attribute: attribute
|
||||
}
|
||||
};
|
||||
@@ -317,7 +378,6 @@ export const BrowserWindow = () => {
|
||||
}
|
||||
}, [paginationMode, resetPaginationSelector]);
|
||||
|
||||
|
||||
return (
|
||||
<div onClick={handleClick} style={{ width: '900px', height: "400px" , borderRadius: '8px 8px 0px 0px '}} id="browser-window">
|
||||
{
|
||||
|
||||
@@ -8,6 +8,9 @@ import { AutoAwesome, FormatListBulleted, VpnKey, Usb, Article, CloudQueue,Code,
|
||||
|
||||
|
||||
import { apiUrl } from "../../apiConfig";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '../../i18n';
|
||||
|
||||
|
||||
|
||||
interface MainMenuProps {
|
||||
@@ -17,6 +20,7 @@ interface MainMenuProps {
|
||||
|
||||
export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenuProps) => {
|
||||
const theme = useTheme();
|
||||
const {t} = useTranslation();
|
||||
|
||||
const handleChange = (event: React.SyntheticEvent, newValue: string) => {
|
||||
handleChangeContent(newValue);
|
||||
@@ -70,28 +74,28 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
|
||||
>
|
||||
<Tab
|
||||
value="recordings"
|
||||
label="Robots"
|
||||
label={t('mainmenu.recordings')}
|
||||
icon={<AutoAwesome />}
|
||||
iconPosition="start"
|
||||
|
||||
/>
|
||||
<Tab
|
||||
value="runs"
|
||||
label="Runs"
|
||||
label={t('mainmenu.runs')}
|
||||
icon={<FormatListBulleted />}
|
||||
iconPosition="start"
|
||||
|
||||
/>
|
||||
<Tab
|
||||
value="proxy"
|
||||
label="Proxy"
|
||||
label={t('mainmenu.proxy')}
|
||||
icon={<Usb />}
|
||||
iconPosition="start"
|
||||
|
||||
/>
|
||||
<Tab
|
||||
value="apikey"
|
||||
label="API Key"
|
||||
label={t('mainmenu.apikey')}
|
||||
icon={<VpnKey />}
|
||||
iconPosition="start"
|
||||
|
||||
@@ -99,13 +103,11 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
|
||||
</Tabs>
|
||||
<hr />
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1rem', textAlign: 'left' }}>
|
||||
|
||||
<Button href="/api-docs" target="_blank" rel="noopener noreferrer" sx={buttonStyles} startIcon={<Article />}>
|
||||
API Docs
|
||||
|
||||
<Button href={`${apiUrl}/api-docs/`} target="_blank" rel="noopener noreferrer" sx={buttonStyles} startIcon={<Code />}>
|
||||
{t('mainmenu.apidocs')}
|
||||
</Button>
|
||||
<Button href="https://forms.gle/hXjgqDvkEhPcaBW76" target="_blank" rel="noopener noreferrer" sx={buttonStyles} startIcon={<CloudQueue />}>
|
||||
Join Maxun Cloud
|
||||
{t('mainmenu.feedback')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { sendProxyConfig, getProxyConfig, testProxyConfig, deleteProxyConfig } from '../../api/proxy';
|
||||
import { useGlobalInfoStore } from '../../context/globalInfo';
|
||||
import { useThemeMode } from '../../context/theme-provider';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// Custom styled Tabs component
|
||||
const CustomTabs = styled(Tabs)(({ theme }) => ({
|
||||
@@ -44,6 +45,7 @@ const CustomTab = styled(Tab)(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
const FormContainer = styled(Box)({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@@ -56,6 +58,7 @@ const FormControl = styled(Box)({
|
||||
});
|
||||
|
||||
const ProxyForm: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [proxyConfigForm, setProxyConfigForm] = useState({
|
||||
server_url: '',
|
||||
username: '',
|
||||
@@ -119,13 +122,13 @@ const ProxyForm: React.FC = () => {
|
||||
try {
|
||||
const response = await sendProxyConfig(proxyConfigForm);
|
||||
if (response) {
|
||||
notify('success', 'Proxy configuration submitted successfully');
|
||||
notify('success', t('proxy.notifications.config_success'));
|
||||
} else {
|
||||
notify('error', `Failed to submit proxy configuration. Try again. ${response}`);
|
||||
console.log(`Failed to submit proxy configuration. Try again. ${response}`)
|
||||
notify('error', t('proxy.notifications.config_error'));
|
||||
console.log(`${t('proxy.notifications.config_error')} ${response}`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
notify('error', `${error} : Failed to submit proxy configuration`);
|
||||
notify('error', `${error} : ${t('proxy.notifications.config_error')}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -136,9 +139,9 @@ const ProxyForm: React.FC = () => {
|
||||
const testProxy = async () => {
|
||||
await testProxyConfig().then((response) => {
|
||||
if (response.success) {
|
||||
notify('success', 'Proxy configuration is working');
|
||||
notify('success', t('proxy.notifications.test_success'));
|
||||
} else {
|
||||
notify('error', 'Failed to test proxy configuration. Try again.');
|
||||
notify('error', t('proxy.notifications.test_error'));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -149,7 +152,7 @@ const ProxyForm: React.FC = () => {
|
||||
if (response.proxy_url) {
|
||||
setIsProxyConfigured(true);
|
||||
setProxy(response);
|
||||
notify('success', 'Proxy configuration fetched successfully');
|
||||
notify('success', t('proxy.notifications.fetch_success'));
|
||||
}
|
||||
} catch (error: any) {
|
||||
notify('error', error);
|
||||
@@ -159,11 +162,11 @@ const ProxyForm: React.FC = () => {
|
||||
const removeProxy = async () => {
|
||||
await deleteProxyConfig().then((response) => {
|
||||
if (response) {
|
||||
notify('success', 'Proxy configuration removed successfully');
|
||||
notify('success', t('proxy.notifications.remove_success'));
|
||||
setIsProxyConfigured(false);
|
||||
setProxy({ proxy_url: '', auth: false });
|
||||
} else {
|
||||
notify('error', 'Failed to remove proxy configuration. Try again.');
|
||||
notify('error', t('proxy.notifications.remove_error'));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -179,7 +182,7 @@ const ProxyForm: React.FC = () => {
|
||||
<>
|
||||
<FormContainer>
|
||||
<Typography variant="h6" gutterBottom component="div" style={{ marginTop: '20px' }}>
|
||||
Proxy Configuration
|
||||
{t('proxy.title')}
|
||||
</Typography>
|
||||
<CustomTabs
|
||||
value={tabIndex}
|
||||
@@ -191,18 +194,24 @@ const ProxyForm: React.FC = () => {
|
||||
}}
|
||||
>
|
||||
<CustomTab
|
||||
label="Standard Proxy"
|
||||
label={t('proxy.tab_standard')}
|
||||
style={{
|
||||
color: tabIndex === 0 ? '#FF69B4' : (isDarkMode ? 'white' : 'black')
|
||||
}}
|
||||
/>
|
||||
<CustomTab
|
||||
label="Automatic Proxy Rotation"
|
||||
label={t('proxy.tab_rotation')}
|
||||
style={{
|
||||
color: tabIndex === 1 ? '#FF69B4' : (isDarkMode ? 'white' : 'black')
|
||||
}}
|
||||
/>
|
||||
</CustomTabs>
|
||||
|
||||
<!-- <Tabs value={tabIndex} onChange={handleTabChange}>
|
||||
<Tab label={t('proxy.tab_standard')} />
|
||||
<Tab label={t('proxy.tab_rotation')} />
|
||||
</Tabs> -->
|
||||
|
||||
{tabIndex === 0 && (
|
||||
isProxyConfigured ? (
|
||||
<Box sx={{ maxWidth: 600, width: '100%', marginTop: '5px' }}>
|
||||
@@ -210,8 +219,8 @@ const ProxyForm: React.FC = () => {
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell><strong>Proxy URL</strong></TableCell>
|
||||
<TableCell><strong>Requires Authentication</strong></TableCell>
|
||||
<TableCell><strong>{t('proxy.table.proxy_url')}</strong></TableCell>
|
||||
<TableCell><strong>{t('proxy.table.requires_auth')}</strong></TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -223,39 +232,37 @@ const ProxyForm: React.FC = () => {
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Button variant="outlined" color="primary" onClick={testProxy}>
|
||||
Test Proxy
|
||||
{t('proxy.test_proxy')}
|
||||
</Button>
|
||||
<Button variant="outlined" color="error" onClick={removeProxy} sx={{ marginLeft: '10px' }}>
|
||||
Remove Proxy
|
||||
{t('proxy.remove_proxy')}
|
||||
</Button>
|
||||
</Box>
|
||||
) : (
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ maxWidth: 400, width: '100%' }}>
|
||||
<FormControl>
|
||||
<TextField
|
||||
label="Proxy Server URL"
|
||||
label={t('proxy.server_url')}
|
||||
name="server_url"
|
||||
value={proxyConfigForm.server_url}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
required
|
||||
error={!!errors.server_url}
|
||||
helperText={errors.server_url || `Proxy to be used for all robots. HTTP and SOCKS proxies are supported.
|
||||
Example http://myproxy.com:3128 or socks5://myproxy.com:3128.
|
||||
Short form myproxy.com:3128 is considered an HTTP proxy.`}
|
||||
helperText={errors.server_url || t('proxy.server_url_helper')}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={<Switch checked={requiresAuth} onChange={handleAuthToggle} />}
|
||||
label="Requires Authentication?"
|
||||
label={t('proxy.requires_auth')}
|
||||
/>
|
||||
</FormControl>
|
||||
{requiresAuth && (
|
||||
<>
|
||||
<FormControl>
|
||||
<TextField
|
||||
label="Username"
|
||||
label={t('proxy.username')}
|
||||
name="username"
|
||||
value={proxyConfigForm.username}
|
||||
onChange={handleChange}
|
||||
@@ -267,7 +274,7 @@ const ProxyForm: React.FC = () => {
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<TextField
|
||||
label="Password"
|
||||
label={t('proxy.password')}
|
||||
name="password"
|
||||
value={proxyConfigForm.password}
|
||||
onChange={handleChange}
|
||||
@@ -287,7 +294,7 @@ const ProxyForm: React.FC = () => {
|
||||
fullWidth
|
||||
disabled={!proxyConfigForm.server_url || (requiresAuth && (!proxyConfigForm.username || !proxyConfigForm.password))}
|
||||
>
|
||||
Add Proxy
|
||||
{t('proxy.add_proxy')}
|
||||
</Button>
|
||||
</Box>
|
||||
))}
|
||||
@@ -295,31 +302,37 @@ const ProxyForm: React.FC = () => {
|
||||
<Box sx={{ maxWidth: 400, width: '100%', textAlign: 'center', marginTop: '20px' }}>
|
||||
<>
|
||||
<Typography variant="body1" gutterBottom component="div">
|
||||
Coming Soon - In Open Source (Basic Rotation) & Cloud (Advanced Rotation). If you don't want to manage the infrastructure, join our cloud waitlist to get early access.
|
||||
{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',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', bgcolor: isDarkMode ? '#3b002d' : '#ffc4f1', color: isDarkMode ? 'white' : 'black' }}>
|
||||
<AlertTitle>If your proxy requires a username and password, always provide them separately from the proxy URL. </AlertTitle>
|
||||
|
||||
<Alert severity="info" sx={{ marginTop: '80px', marginLeft: '50px', height: '230px', width: '450px', border: '1px solid #ff00c3' }}>
|
||||
<AlertTitle>{t('proxy.alert.title')}</AlertTitle>
|
||||
<br />
|
||||
<b>The right way</b>
|
||||
<b>{t('proxy.alert.right_way')}</b>
|
||||
<br />
|
||||
Proxy URL: http://proxy.com:1337
|
||||
{t('proxy.alert.proxy_url')} http://proxy.com:1337
|
||||
<br />
|
||||
Username: myusername
|
||||
{t('proxy.alert.username')} myusername
|
||||
<br />
|
||||
Password: mypassword
|
||||
{t('proxy.alert.password')} mypassword
|
||||
<br />
|
||||
<br />
|
||||
<b>The wrong way</b>
|
||||
<br />
|
||||
Proxy URL: http://myusername:mypassword@proxy.com:1337
|
||||
<b>{t('proxy.alert.wrong_way')}</b>
|
||||
<br />
|
||||
|
||||
{t('proxy.alert.proxy_url')} http://myusername:mypassword@proxy.com:1337
|
||||
</Alert>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -23,6 +23,7 @@ import { getActiveWorkflow } from "../../api/workflow";
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import ActionDescriptionBox from '../molecules/ActionDescriptionBox';
|
||||
import { useThemeMode } from '../../context/theme-provider';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const fetchWorkflow = (id: string, callback: (response: WorkflowFile) => void) => {
|
||||
getActiveWorkflow(id).then(
|
||||
@@ -56,11 +57,14 @@ 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 } = useGlobalInfoStore();
|
||||
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();
|
||||
const { browserSteps, updateBrowserTextStepLabel, deleteBrowserStep, addScreenshotStep, updateListTextFieldLabel, removeListTextField } = useBrowserSteps();
|
||||
const { id, socket } = useSocketStore();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const workflowHandler = useCallback((data: WorkflowFile) => {
|
||||
setWorkflow(data);
|
||||
@@ -129,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
|
||||
@@ -140,7 +154,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
setTextLabels(prevLabels => ({ ...prevLabels, [id]: label }));
|
||||
}
|
||||
if (!label.trim()) {
|
||||
setErrors(prevErrors => ({ ...prevErrors, [id]: 'Label cannot be empty' }));
|
||||
setErrors(prevErrors => ({ ...prevErrors, [id]: t('right_panel.errors.label_required') }));
|
||||
} else {
|
||||
setErrors(prevErrors => ({ ...prevErrors, [id]: '' }));
|
||||
}
|
||||
@@ -152,7 +166,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
updateBrowserTextStepLabel(id, label);
|
||||
setConfirmedTextSteps(prev => ({ ...prev, [id]: true }));
|
||||
} else {
|
||||
setErrors(prevErrors => ({ ...prevErrors, [id]: 'Label cannot be empty' }));
|
||||
setErrors(prevErrors => ({ ...prevErrors, [id]: t('right_panel.errors.label_required') }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -168,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,
|
||||
@@ -194,6 +224,22 @@ 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 => {
|
||||
@@ -214,18 +260,19 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
const stopCaptureAndEmitGetTextSettings = useCallback(() => {
|
||||
const hasUnconfirmedTextSteps = browserSteps.some(step => step.type === 'text' && !confirmedTextSteps[step.id]);
|
||||
if (hasUnconfirmedTextSteps) {
|
||||
notify('error', 'Please confirm all text fields');
|
||||
notify('error', t('right_panel.errors.confirm_text_fields'));
|
||||
return;
|
||||
}
|
||||
stopGetText();
|
||||
const settings = getTextSettingsObject();
|
||||
console.log("SETTINGS", settings);
|
||||
const hasTextSteps = browserSteps.some(step => step.type === 'text');
|
||||
if (hasTextSteps) {
|
||||
socket?.emit('action', { action: 'scrapeSchema', settings });
|
||||
}
|
||||
setIsCaptureTextConfirmed(true);
|
||||
resetInterpretationLog();
|
||||
onFinishCapture();
|
||||
}, [stopGetText, getTextSettingsObject, socket, browserSteps, confirmedTextSteps]);
|
||||
}, [stopGetText, getTextSettingsObject, socket, browserSteps, confirmedTextSteps, resetInterpretationLog]);
|
||||
|
||||
const getListSettingsObject = useCallback(() => {
|
||||
let settings: {
|
||||
@@ -279,7 +326,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
if (settings) {
|
||||
socket?.emit('action', { action: 'scrapeList', settings });
|
||||
} else {
|
||||
notify('error', 'Unable to create list settings. Make sure you have defined a field for the list.');
|
||||
notify('error', t('right_panel.errors.unable_create_settings'));
|
||||
}
|
||||
handleStopGetList();
|
||||
onFinishCapture();
|
||||
@@ -297,13 +344,13 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
|
||||
case 'pagination':
|
||||
if (!paginationType) {
|
||||
notify('error', 'Please select a pagination type.');
|
||||
notify('error', t('right_panel.errors.select_pagination'));
|
||||
return;
|
||||
}
|
||||
const settings = getListSettingsObject();
|
||||
const paginationSelector = settings.pagination?.selector;
|
||||
if (['clickNext', 'clickLoadMore'].includes(paginationType) && !paginationSelector) {
|
||||
notify('error', 'Please select the pagination element first.');
|
||||
notify('error', t('right_panel.errors.select_pagination_element'));
|
||||
return;
|
||||
}
|
||||
stopPaginationMode();
|
||||
@@ -315,16 +362,17 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
|
||||
case 'limit':
|
||||
if (!limitType || (limitType === 'custom' && !customLimit)) {
|
||||
notify('error', 'Please select a limit or enter a custom limit.');
|
||||
notify('error', t('right_panel.errors.select_limit'));
|
||||
return;
|
||||
}
|
||||
const limit = limitType === 'custom' ? parseInt(customLimit) : parseInt(limitType);
|
||||
if (isNaN(limit) || limit <= 0) {
|
||||
notify('error', 'Please enter a valid limit.');
|
||||
notify('error', t('right_panel.errors.invalid_limit'));
|
||||
return;
|
||||
}
|
||||
stopLimitMode();
|
||||
setShowLimitOptions(false);
|
||||
setIsCaptureListConfirmed(true);
|
||||
stopCaptureAndEmitGetListSettings();
|
||||
setCaptureStage('complete');
|
||||
break;
|
||||
@@ -335,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);
|
||||
};
|
||||
@@ -349,7 +414,8 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
setTextLabels({});
|
||||
setErrors({});
|
||||
setConfirmedTextSteps({});
|
||||
notify('error', 'Capture Text Discarded');
|
||||
setIsCaptureTextConfirmed(false);
|
||||
notify('error', t('right_panel.errors.capture_text_discarded'));
|
||||
}, [browserSteps, stopGetText, deleteBrowserStep]);
|
||||
|
||||
const discardGetList = useCallback(() => {
|
||||
@@ -364,7 +430,8 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
setShowLimitOptions(false);
|
||||
setCaptureStage('initial');
|
||||
setConfirmedListTextFields({});
|
||||
notify('error', 'Capture List Discarded');
|
||||
setIsCaptureListConfirmed(false);
|
||||
notify('error', t('right_panel.errors.capture_list_discarded'));
|
||||
}, [browserSteps, stopGetList, deleteBrowserStep, resetListState]);
|
||||
|
||||
|
||||
@@ -420,37 +487,54 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
</SimpleBox> */}
|
||||
<ActionDescriptionBox isDarkMode={isDarkMode} />
|
||||
<Box display="flex" flexDirection="column" gap={2} style={{ margin: '13px',background: isDarkMode?'#1E2124': 'inherit',color: isDarkMode ? 'white' : 'inherit' }}>
|
||||
{!getText && !getScreenshot && !getList && showCaptureList && <Button variant="contained" sx={{backgroundColor:"#ff00c3",color:`${isDarkMode?'white':'black'}`}} onClick={startGetList}>Capture List</Button>}
|
||||
{!getText && !getScreenshot && !getList && showCaptureList && <Button variant="contained" sx={{backgroundColor:"#ff00c3",color:`${isDarkMode?'white':'black'}`}} onClick={startGetList}>{t('right_panel.buttons.capture_list')}</Button>}
|
||||
<!-- <ActionDescriptionBox />
|
||||
<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}
|
||||
>
|
||||
{t('right_panel.buttons.back')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleConfirmListCapture}
|
||||
disabled={captureStage === 'initial' ? isConfirmCaptureDisabled : hasUnconfirmedListTextFields}
|
||||
>
|
||||
{captureStage === 'initial' ? 'Confirm Capture' :
|
||||
captureStage === 'pagination' ? 'Confirm Pagination' :
|
||||
captureStage === 'limit' ? 'Confirm Limit' : 'Finish Capture'}
|
||||
{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')}
|
||||
</Button>
|
||||
<Button variant="outlined" color="error" onClick={discardGetList}>
|
||||
{t('right_panel.buttons.discard')}
|
||||
</Button>
|
||||
<Button variant="outlined" color="error" onClick={discardGetList}>Discard</Button>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{showPaginationOptions && (
|
||||
<Box display="flex" flexDirection="column" gap={2} style={{ margin: '13px' }}>
|
||||
<Typography>How can we find the next list item on the page?</Typography>
|
||||
<Button variant={paginationType === 'clickNext' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('clickNext')}>Click on next to navigate to the next page</Button>
|
||||
<Button variant={paginationType === 'clickLoadMore' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('clickLoadMore')}>Click on load more to load more items</Button>
|
||||
<Button variant={paginationType === 'scrollDown' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('scrollDown')}>Scroll down to load more items</Button>
|
||||
<Button variant={paginationType === 'scrollUp' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('scrollUp')}>Scroll up to load more items</Button>
|
||||
<Button variant={paginationType === 'none' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('none')}>No more items to load</Button>
|
||||
<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>
|
||||
</Box>
|
||||
)}
|
||||
{showLimitOptions && (
|
||||
<FormControl>
|
||||
<FormLabel style={{ marginBottom: '10px', background: isDarkMode ? "#1E2124" : 'white', color: isDarkMode ? "white" : 'black' }}>
|
||||
<h4>What is the maximum number of rows you want to extract?</h4>
|
||||
<h4>{t('right_panel.limit.title')}</h4>
|
||||
<!-- <FormLabel>
|
||||
<h4>{t('right_panel.limit.title')}</h4> -->
|
||||
</FormLabel>
|
||||
<RadioGroup
|
||||
value={limitType}
|
||||
@@ -464,13 +548,28 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
<FormControlLabel value="10" control={<Radio />} label="10" />
|
||||
<FormControlLabel value="100" control={<Radio />} label="100" />
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<FormControlLabel value="custom" control={<Radio />} label="Custom" />
|
||||
<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="Enter number"
|
||||
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': {
|
||||
@@ -487,21 +586,24 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
)}
|
||||
{!getText && !getScreenshot && !getList && showCaptureText && <Button variant="contained" sx={{backgroundColor:"#ff00c3",color:`${isDarkMode?'white':'black'}`}} onClick={startGetText}>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} >Confirm</Button>
|
||||
<Button variant="outlined" color="error" onClick={discardGetText} >Discard</Button>
|
||||
<Button variant="outlined" onClick={stopCaptureAndEmitGetTextSettings} >{t('right_panel.buttons.confirm')}</Button>
|
||||
<Button variant="outlined" color="error" onClick={discardGetText} >{t('right_panel.buttons.discard')}</Button>
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
{!getText && !getScreenshot && !getList && showCaptureScreenshot && <Button variant="contained" sx={{backgroundColor:"#ff00c3",color:`${isDarkMode?'white':'black'}`}} onClick={startGetScreenshot}>Capture Screenshot</Button>}
|
||||
{!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)}>Capture Fullpage</Button>
|
||||
<Button variant="contained" onClick={() => captureScreenshot(false)}>Capture Visible Part</Button>
|
||||
<Button variant="outlined" color="error" onClick={stopGetScreenshot}>Discard</Button>
|
||||
<Button variant="contained" onClick={() => captureScreenshot(true)}>{t('right_panel.screenshot.capture_fullpage')}</Button>
|
||||
<Button variant="contained" onClick={() => captureScreenshot(false)}>{t('right_panel.screenshot.capture_visible')}</Button>
|
||||
<Button variant="outlined" color="error" onClick={stopGetScreenshot}>{t('right_panel.buttons.discard')}</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
@@ -512,7 +614,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
step.type === 'text' && (
|
||||
<>
|
||||
<TextField
|
||||
label="Label"
|
||||
label={t('right_panel.fields.label')}
|
||||
value={textLabels[step.id] || step.label || ''}
|
||||
onChange={(e) => handleTextLabelChange(step.id, e.target.value)}
|
||||
fullWidth
|
||||
@@ -531,7 +633,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
sx={{ background: isDarkMode ? "#1E2124" : 'white', color: isDarkMode ? "white" : 'black' }}
|
||||
/>
|
||||
<TextField
|
||||
label="Data"
|
||||
label={t('right_panel.fields.data')}
|
||||
value={step.data}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
@@ -545,10 +647,20 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
}}
|
||||
|
||||
/>
|
||||
{!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()}>Confirm</Button>
|
||||
<Button variant="contained" color="error" onClick={() => handleTextStepDiscard(step.id)}>Discard</Button>
|
||||
<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>
|
||||
)}
|
||||
</>
|
||||
@@ -557,17 +669,19 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
<Box display="flex" alignItems="center">
|
||||
<DocumentScannerIcon sx={{ mr: 1 }} />
|
||||
<Typography>
|
||||
{`Take ${step.fullPage ? 'Fullpage' : 'Visible Part'} Screenshot`}
|
||||
{step.fullPage ?
|
||||
t('right_panel.screenshot.display_fullpage') :
|
||||
t('right_panel.screenshot.display_visible')}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{step.type === 'list' && (
|
||||
<>
|
||||
<Typography>List Selected Successfully</Typography>
|
||||
<Typography>{t('right_panel.messages.list_selected')}</Typography>
|
||||
{Object.entries(step.fields).map(([key, field]) => (
|
||||
<Box key={key} sx={{ padding: '10px', margin: '11px', borderRadius: '5px', position: 'relative', background: `${isDarkMode ? "#1E2124" : 'white'}` }}>
|
||||
<TextField
|
||||
label="Field Label"
|
||||
label={t('right_panel.fields.field_label')}
|
||||
value={field.label || ''}
|
||||
onChange={(e) => handleTextLabelChange(field.id, e.target.value, step.id, key)}
|
||||
fullWidth
|
||||
@@ -584,7 +698,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
|
||||
/>
|
||||
<TextField
|
||||
label="Field Data"
|
||||
label={t('right_panel.fields.field_data')}
|
||||
value={field.data || ''}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
@@ -598,21 +712,31 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
}}
|
||||
|
||||
/>
|
||||
{!confirmedListTextFields[step.id]?.[key] && (
|
||||
{!confirmedListTextFields[step.id]?.[key] ? (
|
||||
<Box display="flex" justifyContent="space-between" gap={2}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => handleListTextFieldConfirm(step.id, key)}
|
||||
disabled={!field.label?.trim()}
|
||||
>
|
||||
Confirm
|
||||
{t('right_panel.buttons.confirm')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => handleListTextFieldDiscard(step.id, key)}
|
||||
>
|
||||
Discard
|
||||
{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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user