From 2009089e2d2f7e858957f1b087585be734620daf Mon Sep 17 00:00:00 2001 From: AmitChauhan63390 Date: Sun, 26 Jan 2025 14:22:36 +0530 Subject: [PATCH 001/197] airtable Oauth --- .gitignore | 2 + package.json | 8 + server/config/config.json | 26 ++ ...125120216-add-airtable-columns-to-robot.js | 16 + ...0125182726-update-airtable-token-length.js | 22 + server/src/models/Robot.ts | 28 +- server/src/routes/auth.ts | 243 ++++++++++ server/src/server.ts | 11 + .../integration/IntegrationSettings.tsx | 422 +++++++++++------- 9 files changed, 617 insertions(+), 161 deletions(-) create mode 100644 server/config/config.json create mode 100644 server/migrations/20250125120216-add-airtable-columns-to-robot.js create mode 100644 server/migrations/20250125182726-update-airtable-token-length.js diff --git a/.gitignore b/.gitignore index 74279fc9..a2a80766 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ /build package-lock.json + +docker-compose.yml \ No newline at end of file diff --git a/package.json b/package.json index 5dae78a2..974dfc06 100644 --- a/package.json +++ b/package.json @@ -16,16 +16,19 @@ "@types/bcrypt": "^5.0.2", "@types/body-parser": "^1.19.5", "@types/csurf": "^1.11.5", + "@types/express-session": "^1.18.1", "@types/jsonwebtoken": "^9.0.7", "@types/node": "22.7.9", "@types/react": "^18.0.5", "@types/react-dom": "^18.0.1", "@types/uuid": "^8.3.4", + "airtable": "^0.12.2", "axios": "^0.26.0", "bcrypt": "^5.1.1", "body-parser": "^1.20.3", "buffer": "^6.0.3", "bullmq": "^5.12.15", + "connect-redis": "^8.0.1", "cookie-parser": "^1.4.6", "cors": "^2.8.5", "cron-parser": "^4.9.0", @@ -33,6 +36,7 @@ "csurf": "^1.11.0", "dotenv": "^16.0.0", "express": "^4.17.2", + "express-session": "^1.18.1", "fortawesome": "^0.0.1-security", "google-auth-library": "^9.14.1", "googleapis": "^144.0.0", @@ -52,6 +56,7 @@ "moment-timezone": "^0.5.45", "node-cron": "^3.0.3", "pg": "^8.13.0", + "pkce-challenge": "^4.1.0", "playwright": "^1.48.2", "playwright-extra": "^4.3.6", "posthog-node": "^4.2.1", @@ -65,6 +70,7 @@ "react-router-dom": "^6.26.1", "react-simple-code-editor": "^0.11.2", "react-transition-group": "^4.4.2", + "redis": "^4.7.0", "sequelize": "^6.37.3", "sequelize-typescript": "^2.1.6", "sharp": "^0.33.5", @@ -106,6 +112,7 @@ "@types/prismjs": "^1.26.0", "@types/react-highlight": "^0.12.5", "@types/react-transition-group": "^4.4.4", + "@types/redis": "^4.0.11", "@types/styled-components": "^5.1.23", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.6", @@ -115,6 +122,7 @@ "cross-env": "^7.0.3", "js-cookie": "^3.0.5", "nodemon": "^2.0.15", + "sequelize-cli": "^6.6.2", "ts-node": "^10.4.0", "vite": "^5.4.10" } diff --git a/server/config/config.json b/server/config/config.json new file mode 100644 index 00000000..395914f9 --- /dev/null +++ b/server/config/config.json @@ -0,0 +1,26 @@ +{ + "development": { + "username": "postgres", + "password": "postgres", + "database": "maxun", + "host": "localhost", + "port": 5432, + "dialect": "postgres" + }, + "test": { + "username": "postgres", + "password": "postgres", + "database": "maxun_test", + "host": "localhost", + "port": 5432, + "dialect": "postgres" + }, + "production": { + "username": "postgres", + "password": "postgres", + "database": "maxun_prod", + "host": "localhost", + "port": 5432, + "dialect": "postgres" + } + } \ No newline at end of file diff --git a/server/migrations/20250125120216-add-airtable-columns-to-robot.js b/server/migrations/20250125120216-add-airtable-columns-to-robot.js new file mode 100644 index 00000000..ac2c852b --- /dev/null +++ b/server/migrations/20250125120216-add-airtable-columns-to-robot.js @@ -0,0 +1,16 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + + await queryInterface.addColumn('robot', 'airtable_refresh_token', { + type: Sequelize.STRING, + allowNull: true, + }); + }, + + down: async (queryInterface, Sequelize) => { + + await queryInterface.removeColumn('robot', 'airtable_refresh_token'); + }, +}; \ No newline at end of file diff --git a/server/migrations/20250125182726-update-airtable-token-length.js b/server/migrations/20250125182726-update-airtable-token-length.js new file mode 100644 index 00000000..b75d788c --- /dev/null +++ b/server/migrations/20250125182726-update-airtable-token-length.js @@ -0,0 +1,22 @@ +// migrations/XXXXXXXXXXXXXX-update-airtable-token-length.js +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.changeColumn('robot', 'airtable_access_token', { + type: Sequelize.TEXT + }); + + await queryInterface.changeColumn('robot', 'airtable_refresh_token', { + type: Sequelize.TEXT + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.changeColumn('robot', 'airtable_access_token', { + type: Sequelize.STRING(255) + }); + + await queryInterface.changeColumn('robot', 'airtable_refresh_token', { + type: Sequelize.STRING(255) + }); + } +}; \ No newline at end of file diff --git a/server/src/models/Robot.ts b/server/src/models/Robot.ts index 3b2717d6..ffd4746d 100644 --- a/server/src/models/Robot.ts +++ b/server/src/models/Robot.ts @@ -25,6 +25,10 @@ interface RobotAttributes { google_sheet_id?: string | null; google_access_token?: string | null; google_refresh_token?: string | null; + airtable_base_id?: string | null; // New field for Airtable base ID + airtable_table_name?: string | null; // New field for Airtable table name + airtable_access_token?: string | null; // New field for Airtable access token + airtable_refresh_token?: string | null; // New field for Airtable refresh token schedule?: ScheduleConfig | null; } @@ -49,10 +53,14 @@ class Robot extends Model implements R public recording_meta!: RobotMeta; public recording!: RobotWorkflow; public google_sheet_email!: string | null; - public google_sheet_name?: string | null; - public google_sheet_id?: string | null; + public google_sheet_name!: string | null; + public google_sheet_id!: string | null; public google_access_token!: string | null; public google_refresh_token!: string | null; + public airtable_base_id!: string | null; // New field for Airtable base ID + public airtable_table_name!: string | null; // New field for Airtable table name + public airtable_access_token!: string | null; // New field for Airtable access token + public airtable_refresh_token!: string | null; // New field for Airtable refresh token public schedule!: ScheduleConfig | null; } @@ -95,6 +103,22 @@ Robot.init( type: DataTypes.STRING, allowNull: true, }, + airtable_base_id: { + type: DataTypes.STRING, + allowNull: true, + }, + airtable_table_name: { + type: DataTypes.STRING, + allowNull: true, + }, + airtable_access_token: { + type: DataTypes.TEXT, + allowNull: true, + }, + airtable_refresh_token: { + type: DataTypes.TEXT, + allowNull: true, + }, schedule: { type: DataTypes.JSONB, allowNull: true, diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 837fcd8a..574e2176 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -1,4 +1,6 @@ import { Router, Request, Response } from "express"; +import Airtable from "airtable"; + import User from "../models/User"; import Robot from "../models/Robot"; import jwt from "jsonwebtoken"; @@ -7,6 +9,16 @@ import { requireSignIn } from "../middlewares/auth"; import { genAPIKey } from "../utils/api"; import { google } from "googleapis"; import { capture } from "../utils/analytics"; + + + +declare module "express-session" { + interface SessionData { + code_verifier: string; + robotId: string; + } +} + export const router = Router(); interface AuthenticatedRequest extends Request { @@ -555,3 +567,234 @@ router.post( } } ); + + + + +import crypto from 'crypto'; + +// Add these environment variables to your .env file +// AIRTABLE_CLIENT_ID=your_client_id +// AIRTABLE_CLIENT_SECRET=your_client_secret +// AIRTABLE_REDIRECT_URI=http://localhost:8080/auth/airtable/callback + +// Airtable OAuth Routes +router.get("/airtable", (req, res) => { + const { robotId } = req.query; + if (!robotId) { + return res.status(400).json({ message: "Robot ID is required" }); + } + + // Generate PKCE codes + const code_verifier = crypto.randomBytes(64).toString('base64url'); + const code_challenge = crypto.createHash('sha256') + .update(code_verifier) + .digest('base64url'); + + // Store in session + req.session.code_verifier = code_verifier; + req.session.robotId = robotId.toString(); + + const params = new URLSearchParams({ + client_id: process.env.AIRTABLE_CLIENT_ID!, + redirect_uri: process.env.AIRTABLE_REDIRECT_URI!, + + response_type: 'code', + state: robotId.toString(), + scope: 'data.records:read data.records:write schema.bases:read', + code_challenge: code_challenge, + code_challenge_method: 'S256' + }); + + res.redirect(`https://airtable.com/oauth2/v1/authorize?${params}`); +}); + +router.get("/airtable/callback", async (req, res) => { + try { + const { code, state, error } = req.query; + + if (error) { + return res.redirect( + `${process.env.PUBLIC_URL}/robots/${state}/integrate?error=${encodeURIComponent(error.toString())}` + ); + } + + if (!code || !state) { + return res.status(400).json({ message: "Missing authorization code or state" }); + } + + // Verify session data + if (!req.session?.code_verifier || req.session.robotId !== state.toString()) { + return res.status(400).json({ + message: "Session expired - please restart the OAuth flow" + }); + } + + // Exchange code for tokens + const tokenResponse = await fetch("https://airtable.com/oauth2/v1/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + grant_type: "authorization_code", + code: code.toString(), + client_id: process.env.AIRTABLE_CLIENT_ID!, + + redirect_uri: process.env.AIRTABLE_REDIRECT_URI!, + code_verifier: req.session.code_verifier + }), + }); + + if (!tokenResponse.ok) { + const errorData = await tokenResponse.json(); + console.error('Token exchange failed:', errorData); + return res.redirect( + `${process.env.PUBLIC_URL}/robots/${state}/integrate?error=${encodeURIComponent(errorData.error_description || 'Authentication failed')}` + ); + } + + const tokens = await tokenResponse.json(); + + // Update robot with credentials + const robot = await Robot.findOne({ + where: { "recording_meta.id": req.session.robotId } + }); + + if (!robot) { + return res.status(404).json({ message: "Robot not found" }); + } + + await robot.update({ + airtable_access_token: tokens.access_token, + airtable_refresh_token: tokens.refresh_token, + + }); + + // Clear session data + req.session.destroy((err) => { + if (err) console.error('Session cleanup error:', err); + }); + + res.redirect( + `${process.env.PUBLIC_URL}/robots/${state}/integrate?success=true` + ); + + } catch (error: any) { + console.error('Airtable callback error:', error); + res.redirect( + `${process.env.PUBLIC_URL}/robots/${req.session.robotId}/integrate?error=${encodeURIComponent(error.message)}` + ); + } +}); + +// Get Airtable bases +router.get("/airtable/bases", requireSignIn, async (req: AuthenticatedRequest, res) => { + try { + const { robotId } = req.query; + if (!robotId) { + return res.status(400).json({ message: "Robot ID is required" }); + } + + const robot = await Robot.findOne({ + where: { "recording_meta.id": robotId }, + raw: true, + }); + + if (!robot?.airtable_access_token) { + return res.status(400).json({ message: "Robot not authenticated with Airtable" }); + } + + const response = await fetch('https://api.airtable.com/v0/meta/bases', { + headers: { + 'Authorization': `Bearer ${robot.airtable_access_token}` + } + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error.message || 'Failed to fetch bases'); + } + + const data = await response.json(); + res.json(data.bases.map((base: any) => ({ + id: base.id, + name: base.name + }))); + + } catch (error: any) { + res.status(500).json({ message: error.message }); + } +}); + +// Update robot with selected base +router.post("/airtable/update", requireSignIn, async (req: AuthenticatedRequest, res) => { + const { baseId, baseName, robotId } = req.body; + + if (!baseId || !robotId) { + return res.status(400).json({ message: "Base ID and Robot ID are required" }); + } + + try { + const robot = await Robot.findOne({ + where: { "recording_meta.id": robotId } + }); + + if (!robot) { + return res.status(404).json({ message: "Robot not found" }); + } + + await robot.update({ + airtable_base_id: baseId, + airtable_table_name: baseName, + }); + + capture("maxun-oss-airtable-integration-created", { + user_id: req.user?.id, + robot_id: robotId, + created_at: new Date().toISOString(), + }); + + res.json({ message: "Airtable base updated successfully" }); + + } catch (error: any) { + res.status(500).json({ message: error.message }); + } +}); + +// Remove Airtable integration +router.post("/airtable/remove", requireSignIn, async (req: AuthenticatedRequest, res) => { + const { robotId } = req.body; + if (!robotId) { + return res.status(400).json({ message: "Robot ID is required" }); + } + + try { + const robot = await Robot.findOne({ + where: { "recording_meta.id": robotId } + }); + + if (!robot) { + return res.status(404).json({ message: "Robot not found" }); + } + + await robot.update({ + airtable_access_token: null, + airtable_refresh_token: null, + airtable_base_id: null, + + }); + + capture("maxun-oss-airtable-integration-removed", { + user_id: req.user?.id, + robot_id: robotId, + deleted_at: new Date().toISOString(), + }); + + res.json({ message: "Airtable integration removed successfully" }); + + } catch (error: any) { + res.status(500).json({ message: error.message }); + } +}); + diff --git a/server/src/server.ts b/server/src/server.ts index 8c28c2d2..94197cfb 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -18,6 +18,7 @@ import { fork } from 'child_process'; import { capture } from "./utils/analytics"; import swaggerUi from 'swagger-ui-express'; import swaggerSpec from './swagger/config'; +import session from 'express-session'; const app = express(); app.use(cors({ @@ -26,6 +27,16 @@ app.use(cors({ })); app.use(express.json()); + +app.use( + session({ + secret: 'your_secret_key', // Replace with a secure secret key + resave: false, // Do not resave the session if it hasn't changed + saveUninitialized: true, // Save new sessions + cookie: { secure: false }, // Set to true if using HTTPS + }) +); + const server = http.createServer(app); /** diff --git a/src/components/integration/IntegrationSettings.tsx b/src/components/integration/IntegrationSettings.tsx index f9c397ae..9101b9a7 100644 --- a/src/components/integration/IntegrationSettings.tsx +++ b/src/components/integration/IntegrationSettings.tsx @@ -6,7 +6,6 @@ import { CircularProgress, Alert, AlertTitle, - Chip, } from "@mui/material"; import Button from "@mui/material/Button"; import TextField from "@mui/material/TextField"; @@ -14,10 +13,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 Cookies from "js-cookie"; import { useTranslation } from "react-i18next"; - interface IntegrationProps { isOpen: boolean; handleStart: (data: IntegrationSettings) => void; @@ -27,6 +25,8 @@ interface IntegrationProps { export interface IntegrationSettings { spreadsheetId: string; spreadsheetName: string; + airtableBaseId: string; + airtableBaseName: string; data: string; } @@ -35,7 +35,7 @@ const getCookie = (name: string): string | null => { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) { - return parts.pop()?.split(';').shift() || null; + return parts.pop()?.split(";").shift() || null; } return null; }; @@ -53,58 +53,114 @@ export const IntegrationSettingsModal = ({ const [settings, setSettings] = useState({ spreadsheetId: "", spreadsheetName: "", + airtableBaseId: "", + airtableBaseName: "", data: "", }); - const [spreadsheets, setSpreadsheets] = useState< - { id: string; name: string }[] - >([]); + const [spreadsheets, setSpreadsheets] = useState<{ id: string; name: string }[]>([]); + const [airtableBases, setAirtableBases] = useState<{ id: string; name: string }[]>([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const { recordingId, notify } = useGlobalInfoStore(); const [recording, setRecording] = useState(null); + // Authenticate with Google const authenticateWithGoogle = () => { - window.location.href = `${apiUrl}/auth/google?robotId=${recordingId}`; + const redirectUri = `${window.location.origin}/google/callback`; + window.location.href = `${apiUrl}/auth/google?robotId=${recordingId}&redirect_uri=${encodeURIComponent(redirectUri)}`; + }; + + const authenticateWithAirtable = () => { + const redirectUri = `${window.location.origin}/airtable/callback`; + window.location.href = `${apiUrl}/auth/airtable?robotId=${recordingId}&redirect_uri=${encodeURIComponent(redirectUri)}`; }; - const handleOAuthCallback = async () => { + // Handle OAuth callback + const handleGoogleCallback = async () => { try { - const response = await axios.get(`${apiUrl}/auth/google/callback`); - const { google_sheet_email, files } = response.data; + const urlParams = new URLSearchParams(window.location.search); + const code = urlParams.get('code'); + + if (!code) { + setError(t("integration_settings.errors.no_auth_code")); + return; + } + + const response = await axios.get( + `${apiUrl}/auth/google/callback?code=${code}&robotId=${recordingId}` + ); + + if (response.data.accessToken) { + notify("success", t("integration_settings.notifications.google_auth_success")); + await fetchSpreadsheetFiles(); + } + + // Clear URL parameters + window.history.replaceState({}, document.title, window.location.pathname); + } catch (error) { - setError("Error authenticating with Google"); + setError(t("integration_settings.errors.google_auth_error")); } }; + const handleAirtableCallback = async () => { + try { + const urlParams = new URLSearchParams(window.location.search); + const code = urlParams.get('code'); + + if (!code) { + setError(t("integration_settings.errors.no_auth_code")); + return; + } + + const response = await axios.get( + `${apiUrl}/auth/airtable/callback?code=${code}&robotId=${recordingId}` + ); + + if (response.data.accessToken) { + notify("success", t("integration_settings.notifications.airtable_auth_success")); + await fetchAirtableBases(); + } + + // Clear URL parameters + window.history.replaceState({}, document.title, window.location.pathname); + + } catch (error) { + setError(t("integration_settings.errors.airtable_auth_error")); + } + }; + + // Fetch Google Sheets const fetchSpreadsheetFiles = async () => { try { - const response = await axios.get( - `${apiUrl}/auth/gsheets/files?robotId=${recordingId}`, - { - withCredentials: true, - } - ); + const response = await axios.get(`${apiUrl}/auth/gsheets/files?robotId=${recordingId}`, { + withCredentials: true, + }); setSpreadsheets(response.data); } catch (error: any) { - console.error( - "Error fetching spreadsheet files:", - error.response?.data?.message || error.message - ); - notify( - "error", - t('integration_settings.errors.fetch_error', { - message: error.response?.data?.message || error.message - }) - ); + console.error("Error fetching spreadsheet files:", error.response?.data?.message || error.message); + notify("error", t("integration_settings.errors.fetch_error", { message: error.response?.data?.message || error.message })); } }; + // Fetch Airtable Bases + const fetchAirtableBases = async () => { + try { + const response = await axios.get(`${apiUrl}/auth/airtable/bases?robotId=${recordingId}`, { + withCredentials: true, + }); + setAirtableBases(response.data); + } catch (error: any) { + console.error("Error fetching Airtable bases:", error.response?.data?.message || error.message); + notify("error", t("integration_settings.errors.fetch_error", { message: error.response?.data?.message || error.message })); + } + }; + + // Handle Google Sheet selection const handleSpreadsheetSelect = (e: React.ChangeEvent) => { - const selectedSheet = spreadsheets.find( - (sheet) => sheet.id === e.target.value - ); + const selectedSheet = spreadsheets.find((sheet) => sheet.id === e.target.value); if (selectedSheet) { setSettings({ ...settings, @@ -114,6 +170,19 @@ export const IntegrationSettingsModal = ({ } }; + // Handle Airtable Base selection + const handleAirtableBaseSelect = (e: React.ChangeEvent) => { + const selectedBase = airtableBases.find((base) => base.id === e.target.value); + if (selectedBase) { + setSettings({ + ...settings, + airtableBaseId: selectedBase.id, + airtableBaseName: selectedBase.name, + }); + } + }; + + // Update Google Sheet ID const updateGoogleSheetId = async () => { try { const response = await axios.post( @@ -125,16 +194,33 @@ export const IntegrationSettingsModal = ({ }, { withCredentials: true } ); - notify(`success`, t('integration_settings.notifications.sheet_selected')); + notify("success", t("integration_settings.notifications.sheet_selected")); console.log("Google Sheet ID updated:", response.data); } catch (error: any) { - console.error( - "Error updating Google Sheet ID:", - error.response?.data?.message || error.message - ); + console.error("Error updating Google Sheet ID:", error.response?.data?.message || error.message); } }; + // Update Airtable Base ID + const updateAirtableBaseId = async () => { + try { + const response = await axios.post( + `${apiUrl}/auth/airtable/update`, + { + baseId: settings.airtableBaseId, + baseName: settings.airtableBaseName, + robotId: recordingId, + }, + { withCredentials: true } + ); + notify("success", t("integration_settings.notifications.base_selected")); + console.log("Airtable Base ID updated:", response.data); + } catch (error: any) { + console.error("Error updating Airtable Base ID:", error.response?.data?.message || error.message); + } + }; + + // Remove Integration const removeIntegration = async () => { try { await axios.post( @@ -145,98 +231,89 @@ export const IntegrationSettingsModal = ({ setRecording(null); setSpreadsheets([]); - setSettings({ spreadsheetId: "", spreadsheetName: "", data: "" }); + setAirtableBases([]); + setSettings({ spreadsheetId: "", spreadsheetName: "", airtableBaseId: "", airtableBaseName: "", data: "" }); } catch (error: any) { - console.error( - "Error removing Google Sheets integration:", - error.response?.data?.message || error.message - ); + console.error("Error removing integration:", error.response?.data?.message || error.message); } }; useEffect(() => { - // Check if there is a success message in cookies - const status = getCookie("robot_auth_status"); - const message = getCookie("robot_auth_message"); - - if (status === "success" && message) { - notify("success", message); - // Clear the cookies after reading - removeCookie("robot_auth_status"); - removeCookie("robot_auth_message"); - } - - // Check if we're on the callback URL - const urlParams = new URLSearchParams(window.location.search); - const code = urlParams.get("code"); - if (code) { - handleOAuthCallback(); - } - - const fetchRecordingInfo = async () => { - if (!recordingId) return; - const recording = await getStoredRecording(recordingId); - if (recording) { - setRecording(recording); + const checkAuthCallback = () => { + const path = window.location.pathname; + const urlParams = new URLSearchParams(window.location.search); + const code = urlParams.get('code'); + + if (code) { + if (path.includes('/google/callback')) { + handleGoogleCallback(); + } else if (path.includes('/airtable/callback')) { + handleAirtableCallback(); + } } }; - - fetchRecordingInfo(); - }, [recordingId]); + + checkAuthCallback(); + + // Cleanup function + return () => { + window.history.replaceState({}, document.title, window.location.pathname); + }; + }, []); return ( -
- - {t('integration_settings.title')} - +
+ {t("integration_settings.title")} - {recording && recording.google_sheet_id ? ( + {recording && (recording.google_sheet_id || recording.airtable_base_id) ? ( <> - - {t('integration_settings.alerts.success.title')} - {t('integration_settings.alerts.success.content', { sheetName: recording.google_sheet_name })} - - {t('integration_settings.alerts.success.here')} - . -
- {t('integration_settings.alerts.success.note')} {t('integration_settings.alerts.success.sync_limitation')} -
- ) : ( <> - {!recording?.google_sheet_email ? ( + {!recording?.google_sheet_email && !recording?.airtable_email ? ( <> -

{t('integration_settings.descriptions.sync_info')}

- + ) : ( <> - {recording.google_sheet_email && ( + {(recording.google_sheet_email || recording.airtable_email) && ( - {t('integration_settings.descriptions.authenticated_as', { - email: recording.google_sheet_email + {t("integration_settings.descriptions.authenticated_as", { + email: recording.google_sheet_email || recording.airtable_email, })} )} @@ -245,64 +322,91 @@ export const IntegrationSettingsModal = ({ ) : error ? ( {error} - ) : spreadsheets.length === 0 ? ( - <> -
- - -
- ) : ( <> - - {spreadsheets.map((sheet) => ( - - {sheet.name} - - ))} - + {recording.google_sheet_email && ( + <> + + {spreadsheets.map((sheet) => ( + + {sheet.name} + + ))} + - {settings.spreadsheetId && ( - - {t('integration_settings.fields.selected_sheet', { - name: spreadsheets.find((s) => s.id === settings.spreadsheetId)?.name, - id: settings.spreadsheetId - })} - + {settings.spreadsheetId && ( + + {t("integration_settings.fields.selected_sheet", { + name: spreadsheets.find((s) => s.id === settings.spreadsheetId)?.name, + id: settings.spreadsheetId, + })} + + )} + + + )} - + {recording.airtable_email && ( + <> + + {airtableBases.map((base) => ( + + {base.name} + + ))} + + + {settings.airtableBaseId && ( + + {t("integration_settings.fields.selected_base", { + name: airtableBases.find((b) => b.id === settings.airtableBaseId)?.name, + id: settings.airtableBaseId, + })} + + )} + + + + )} )} From 6dfdb496db09ab8f5e0d79057e92867298ad86d8 Mon Sep 17 00:00:00 2001 From: AmitChauhan63390 Date: Sun, 26 Jan 2025 14:33:34 +0530 Subject: [PATCH 002/197] integrationsettings --- .../integration/IntegrationSettings.tsx | 76 ++++++++++++++----- 1 file changed, 55 insertions(+), 21 deletions(-) diff --git a/src/components/integration/IntegrationSettings.tsx b/src/components/integration/IntegrationSettings.tsx index 9101b9a7..874b248d 100644 --- a/src/components/integration/IntegrationSettings.tsx +++ b/src/components/integration/IntegrationSettings.tsx @@ -71,62 +71,62 @@ export const IntegrationSettingsModal = ({ const redirectUri = `${window.location.origin}/google/callback`; window.location.href = `${apiUrl}/auth/google?robotId=${recordingId}&redirect_uri=${encodeURIComponent(redirectUri)}`; }; - + + // Authenticate with Airtable const authenticateWithAirtable = () => { const redirectUri = `${window.location.origin}/airtable/callback`; window.location.href = `${apiUrl}/auth/airtable?robotId=${recordingId}&redirect_uri=${encodeURIComponent(redirectUri)}`; }; - // Handle OAuth callback + // Handle Google OAuth callback const handleGoogleCallback = async () => { try { const urlParams = new URLSearchParams(window.location.search); - const code = urlParams.get('code'); - + const code = urlParams.get("code"); + if (!code) { setError(t("integration_settings.errors.no_auth_code")); return; } - + const response = await axios.get( `${apiUrl}/auth/google/callback?code=${code}&robotId=${recordingId}` ); - + if (response.data.accessToken) { notify("success", t("integration_settings.notifications.google_auth_success")); await fetchSpreadsheetFiles(); } - + // Clear URL parameters window.history.replaceState({}, document.title, window.location.pathname); - } catch (error) { setError(t("integration_settings.errors.google_auth_error")); } }; + // Handle Airtable OAuth callback const handleAirtableCallback = async () => { try { const urlParams = new URLSearchParams(window.location.search); - const code = urlParams.get('code'); - + const code = urlParams.get("code"); + if (!code) { setError(t("integration_settings.errors.no_auth_code")); return; } - + const response = await axios.get( `${apiUrl}/auth/airtable/callback?code=${code}&robotId=${recordingId}` ); - + if (response.data.accessToken) { notify("success", t("integration_settings.notifications.airtable_auth_success")); await fetchAirtableBases(); } - + // Clear URL parameters window.history.replaceState({}, document.title, window.location.pathname); - } catch (error) { setError(t("integration_settings.errors.airtable_auth_error")); } @@ -135,6 +135,7 @@ export const IntegrationSettingsModal = ({ // Fetch Google Sheets const fetchSpreadsheetFiles = async () => { try { + setLoading(true); const response = await axios.get(`${apiUrl}/auth/gsheets/files?robotId=${recordingId}`, { withCredentials: true, }); @@ -142,12 +143,15 @@ export const IntegrationSettingsModal = ({ } catch (error: any) { console.error("Error fetching spreadsheet files:", error.response?.data?.message || error.message); notify("error", t("integration_settings.errors.fetch_error", { message: error.response?.data?.message || error.message })); + } finally { + setLoading(false); } }; // Fetch Airtable Bases const fetchAirtableBases = async () => { try { + setLoading(true); const response = await axios.get(`${apiUrl}/auth/airtable/bases?robotId=${recordingId}`, { withCredentials: true, }); @@ -155,6 +159,8 @@ export const IntegrationSettingsModal = ({ } catch (error: any) { console.error("Error fetching Airtable bases:", error.response?.data?.message || error.message); notify("error", t("integration_settings.errors.fetch_error", { message: error.response?.data?.message || error.message })); + } finally { + setLoading(false); } }; @@ -185,6 +191,7 @@ export const IntegrationSettingsModal = ({ // Update Google Sheet ID const updateGoogleSheetId = async () => { try { + setLoading(true); const response = await axios.post( `${apiUrl}/auth/gsheets/update`, { @@ -198,12 +205,15 @@ export const IntegrationSettingsModal = ({ console.log("Google Sheet ID updated:", response.data); } catch (error: any) { console.error("Error updating Google Sheet ID:", error.response?.data?.message || error.message); + } finally { + setLoading(false); } }; // Update Airtable Base ID const updateAirtableBaseId = async () => { try { + setLoading(true); const response = await axios.post( `${apiUrl}/auth/airtable/update`, { @@ -217,12 +227,15 @@ export const IntegrationSettingsModal = ({ console.log("Airtable Base ID updated:", response.data); } catch (error: any) { console.error("Error updating Airtable Base ID:", error.response?.data?.message || error.message); + } finally { + setLoading(false); } }; // Remove Integration const removeIntegration = async () => { try { + setLoading(true); await axios.post( `${apiUrl}/auth/gsheets/remove`, { robotId: recordingId }, @@ -232,9 +245,18 @@ export const IntegrationSettingsModal = ({ setRecording(null); setSpreadsheets([]); setAirtableBases([]); - setSettings({ spreadsheetId: "", spreadsheetName: "", airtableBaseId: "", airtableBaseName: "", data: "" }); + setSettings({ + spreadsheetId: "", + spreadsheetName: "", + airtableBaseId: "", + airtableBaseName: "", + data: "", + }); + notify("success", t("integration_settings.notifications.integration_removed")); } catch (error: any) { console.error("Error removing integration:", error.response?.data?.message || error.message); + } finally { + setLoading(false); } }; @@ -242,25 +264,37 @@ export const IntegrationSettingsModal = ({ const checkAuthCallback = () => { const path = window.location.pathname; const urlParams = new URLSearchParams(window.location.search); - const code = urlParams.get('code'); - + const code = urlParams.get("code"); + if (code) { - if (path.includes('/google/callback')) { + if (path.includes("/google/callback")) { handleGoogleCallback(); - } else if (path.includes('/airtable/callback')) { + } else if (path.includes("/airtable/callback")) { handleAirtableCallback(); } } }; - + checkAuthCallback(); - + // Cleanup function return () => { window.history.replaceState({}, document.title, window.location.pathname); }; }, []); + useEffect(() => { + const fetchRecordingInfo = async () => { + if (!recordingId) return; + const recording = await getStoredRecording(recordingId); + if (recording) { + setRecording(recording); + } + }; + + fetchRecordingInfo(); + }, [recordingId]); + return (
From e89d64d04feaf33511dc5f245ffcdc8ee0db4c0d Mon Sep 17 00:00:00 2001 From: AmitChauhan63390 Date: Sun, 26 Jan 2025 21:05:08 +0530 Subject: [PATCH 003/197] minor changes --- server/src/routes/auth.ts | 10 +- .../integration/IntegrationSettings.tsx | 616 ++++++++++-------- 2 files changed, 365 insertions(+), 261 deletions(-) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 574e2176..c2994942 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -573,11 +573,6 @@ router.post( import crypto from 'crypto'; -// Add these environment variables to your .env file -// AIRTABLE_CLIENT_ID=your_client_id -// AIRTABLE_CLIENT_SECRET=your_client_secret -// AIRTABLE_REDIRECT_URI=http://localhost:8080/auth/airtable/callback - // Airtable OAuth Routes router.get("/airtable", (req, res) => { const { robotId } = req.query; @@ -668,6 +663,7 @@ router.get("/airtable/callback", async (req, res) => { await robot.update({ airtable_access_token: tokens.access_token, airtable_refresh_token: tokens.refresh_token, + }); @@ -689,7 +685,7 @@ router.get("/airtable/callback", async (req, res) => { }); // Get Airtable bases -router.get("/airtable/bases", requireSignIn, async (req: AuthenticatedRequest, res) => { +router.get("/airtable/bases", async (req: AuthenticatedRequest, res) => { try { const { robotId } = req.query; if (!robotId) { @@ -697,7 +693,7 @@ router.get("/airtable/bases", requireSignIn, async (req: AuthenticatedRequest, r } const robot = await Robot.findOne({ - where: { "recording_meta.id": robotId }, + where: { "recording_meta.id": robotId.toString() }, raw: true, }); diff --git a/src/components/integration/IntegrationSettings.tsx b/src/components/integration/IntegrationSettings.tsx index 874b248d..1d0534d1 100644 --- a/src/components/integration/IntegrationSettings.tsx +++ b/src/components/integration/IntegrationSettings.tsx @@ -6,6 +6,7 @@ import { CircularProgress, Alert, AlertTitle, + Chip, } from "@mui/material"; import Button from "@mui/material/Button"; import TextField from "@mui/material/TextField"; @@ -23,11 +24,12 @@ interface IntegrationProps { } export interface IntegrationSettings { - spreadsheetId: string; - spreadsheetName: string; - airtableBaseId: string; - airtableBaseName: string; + spreadsheetId?: string; + spreadsheetName?: string; + airtableBaseId?: string; + airtableBaseName?: string; data: string; + integrationType: "googleSheets" | "airtable"; } // Helper functions to replace js-cookie functionality @@ -50,123 +52,126 @@ export const IntegrationSettingsModal = ({ handleClose, }: IntegrationProps) => { const { t } = useTranslation(); + const [selectedIntegrationType, setSelectedIntegrationType] = useState< + "googleSheets" | "airtable" | null + >(null); const [settings, setSettings] = useState({ spreadsheetId: "", spreadsheetName: "", airtableBaseId: "", airtableBaseName: "", data: "", + integrationType: "googleSheets", }); - const [spreadsheets, setSpreadsheets] = useState<{ id: string; name: string }[]>([]); - const [airtableBases, setAirtableBases] = useState<{ id: string; name: string }[]>([]); + const [airtableTables, setAirtableTables] = useState<{ id: string; name: string }[]>([]); + const [spreadsheets, setSpreadsheets] = useState< + { id: string; name: string }[] + >([]); + const [airtableBases, setAirtableBases] = useState< + { id: string; name: string }[] + >([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const { recordingId, notify } = useGlobalInfoStore(); const [recording, setRecording] = useState(null); - // Authenticate with Google const authenticateWithGoogle = () => { - const redirectUri = `${window.location.origin}/google/callback`; - window.location.href = `${apiUrl}/auth/google?robotId=${recordingId}&redirect_uri=${encodeURIComponent(redirectUri)}`; + window.location.href = `${apiUrl}/auth/google?robotId=${recordingId}`; }; - // Authenticate with Airtable const authenticateWithAirtable = () => { - const redirectUri = `${window.location.origin}/airtable/callback`; - window.location.href = `${apiUrl}/auth/airtable?robotId=${recordingId}&redirect_uri=${encodeURIComponent(redirectUri)}`; + window.location.href = `${apiUrl}/auth/airtable?robotId=${recordingId}`; }; - // Handle Google OAuth callback - const handleGoogleCallback = async () => { + const handleIntegrationType = (type: "googleSheets" | "airtable") => { + setSelectedIntegrationType(type); + setSettings({ + ...settings, + integrationType: type, + }); + }; + + const fetchAirtableTables = async (baseId: string) => { try { - const urlParams = new URLSearchParams(window.location.search); - const code = urlParams.get("code"); - - if (!code) { - setError(t("integration_settings.errors.no_auth_code")); - return; - } - const response = await axios.get( - `${apiUrl}/auth/google/callback?code=${code}&robotId=${recordingId}` + `${apiUrl}/auth/airtable/tables?baseId=${baseId}&robotId=${recordingId}`, + { + withCredentials: true, + } + ); + setAirtableTables(response.data); + } catch (error: any) { + console.error( + "Error fetching Airtable tables:", + error.response?.data?.message || error.message + ); + notify( + "error", + t("integration_settings.errors.fetch_error", { + message: error.response?.data?.message || error.message, + }) ); - - if (response.data.accessToken) { - notify("success", t("integration_settings.notifications.google_auth_success")); - await fetchSpreadsheetFiles(); - } - - // Clear URL parameters - window.history.replaceState({}, document.title, window.location.pathname); - } catch (error) { - setError(t("integration_settings.errors.google_auth_error")); } }; - // Handle Airtable OAuth callback - const handleAirtableCallback = async () => { - try { - const urlParams = new URLSearchParams(window.location.search); - const code = urlParams.get("code"); - - if (!code) { - setError(t("integration_settings.errors.no_auth_code")); - return; - } - - const response = await axios.get( - `${apiUrl}/auth/airtable/callback?code=${code}&robotId=${recordingId}` - ); - - if (response.data.accessToken) { - notify("success", t("integration_settings.notifications.airtable_auth_success")); - await fetchAirtableBases(); - } - - // Clear URL parameters - window.history.replaceState({}, document.title, window.location.pathname); - } catch (error) { - setError(t("integration_settings.errors.airtable_auth_error")); - } - }; - - // Fetch Google Sheets const fetchSpreadsheetFiles = async () => { try { - setLoading(true); - const response = await axios.get(`${apiUrl}/auth/gsheets/files?robotId=${recordingId}`, { - withCredentials: true, - }); + const response = await axios.get( + `${apiUrl}/auth/gsheets/files?robotId=${recordingId}`, + { + withCredentials: true, + } + ); setSpreadsheets(response.data); } catch (error: any) { - console.error("Error fetching spreadsheet files:", error.response?.data?.message || error.message); - notify("error", t("integration_settings.errors.fetch_error", { message: error.response?.data?.message || error.message })); - } finally { - setLoading(false); + console.error( + "Error fetching spreadsheet files:", + error.response?.data?.message || error.message + ); + notify( + "error", + t("integration_settings.errors.fetch_error", { + message: error.response?.data?.message || error.message, + }) + ); } }; - // Fetch Airtable Bases + console.log("recordingId", recordingId); + const fetchAirtableBases = async () => { try { - setLoading(true); - const response = await axios.get(`${apiUrl}/auth/airtable/bases?robotId=${recordingId}`, { - withCredentials: true, - }); + const response = await axios.get( + `${apiUrl}/auth/airtable/bases?robotId=${recordingId}`, + { + withCredentials: true, + } + ); + setAirtableBases(response.data); + + console.log("Airtable bases:", response.data); + } catch (error: any) { - console.error("Error fetching Airtable bases:", error.response?.data?.message || error.message); - notify("error", t("integration_settings.errors.fetch_error", { message: error.response?.data?.message || error.message })); - } finally { - setLoading(false); + console.error( + "Error fetching Airtable bases:", + error.response?.data?.message || error.message + ); + notify( + "error", + t("integration_settings.errors.fetch_error", { + message: error.response?.data?.message || error.message, + }) + ); } }; - // Handle Google Sheet selection const handleSpreadsheetSelect = (e: React.ChangeEvent) => { - const selectedSheet = spreadsheets.find((sheet) => sheet.id === e.target.value); + const selectedSheet = spreadsheets.find( + (sheet) => sheet.id === e.target.value + ); if (selectedSheet) { setSettings({ ...settings, @@ -176,9 +181,10 @@ export const IntegrationSettingsModal = ({ } }; - // Handle Airtable Base selection const handleAirtableBaseSelect = (e: React.ChangeEvent) => { - const selectedBase = airtableBases.find((base) => base.id === e.target.value); + const selectedBase = airtableBases.find( + (base) => base.id === e.target.value + ); if (selectedBase) { setSettings({ ...settings, @@ -188,10 +194,8 @@ export const IntegrationSettingsModal = ({ } }; - // Update Google Sheet ID const updateGoogleSheetId = async () => { try { - setLoading(true); const response = await axios.post( `${apiUrl}/auth/gsheets/update`, { @@ -201,19 +205,18 @@ export const IntegrationSettingsModal = ({ }, { withCredentials: true } ); - notify("success", t("integration_settings.notifications.sheet_selected")); + notify(`success`, t("integration_settings.notifications.sheet_selected")); console.log("Google Sheet ID updated:", response.data); } catch (error: any) { - console.error("Error updating Google Sheet ID:", error.response?.data?.message || error.message); - } finally { - setLoading(false); + console.error( + "Error updating Google Sheet ID:", + error.response?.data?.message || error.message + ); } }; - // Update Airtable Base ID const updateAirtableBaseId = async () => { try { - setLoading(true); const response = await axios.post( `${apiUrl}/auth/airtable/update`, { @@ -223,21 +226,25 @@ export const IntegrationSettingsModal = ({ }, { withCredentials: true } ); - notify("success", t("integration_settings.notifications.base_selected")); + notify(`success`, t("integration_settings.notifications.base_selected")); console.log("Airtable Base ID updated:", response.data); } catch (error: any) { - console.error("Error updating Airtable Base ID:", error.response?.data?.message || error.message); - } finally { - setLoading(false); + console.error( + "Error updating Airtable Base ID:", + error.response?.data?.message || error.message + ); } }; - // Remove Integration const removeIntegration = async () => { try { - setLoading(true); + const endpoint = + selectedIntegrationType === "googleSheets" + ? "/auth/gsheets/remove" + : "/auth/airtable/remove"; + await axios.post( - `${apiUrl}/auth/gsheets/remove`, + `${apiUrl}${endpoint}`, { robotId: recordingId }, { withCredentials: true } ); @@ -245,204 +252,305 @@ export const IntegrationSettingsModal = ({ setRecording(null); setSpreadsheets([]); setAirtableBases([]); + setSelectedIntegrationType(null); setSettings({ spreadsheetId: "", spreadsheetName: "", airtableBaseId: "", airtableBaseName: "", data: "", + integrationType: "googleSheets", }); - notify("success", t("integration_settings.notifications.integration_removed")); } catch (error: any) { - console.error("Error removing integration:", error.response?.data?.message || error.message); - } finally { - setLoading(false); + console.error( + "Error removing integration:", + error.response?.data?.message || error.message + ); } }; useEffect(() => { - const checkAuthCallback = () => { - const path = window.location.pathname; - const urlParams = new URLSearchParams(window.location.search); - const code = urlParams.get("code"); + const status = getCookie("robot_auth_status"); + const message = getCookie("robot_auth_message"); - if (code) { - if (path.includes("/google/callback")) { - handleGoogleCallback(); - } else if (path.includes("/airtable/callback")) { - handleAirtableCallback(); - } - } - }; + if (status === "success" && message) { + notify("success", message); + removeCookie("robot_auth_status"); + removeCookie("robot_auth_message"); + } - checkAuthCallback(); + const urlParams = new URLSearchParams(window.location.search); + const code = urlParams.get("code"); + if (code) { + // Determine which authentication callback to handle + // You'll need to implement similar callback logic for Airtable + } - // Cleanup function - return () => { - window.history.replaceState({}, document.title, window.location.pathname); - }; - }, []); - - useEffect(() => { const fetchRecordingInfo = async () => { if (!recordingId) return; const recording = await getStoredRecording(recordingId); if (recording) { setRecording(recording); + // Determine integration type based on existing integration + if (recording.google_sheet_id) { + setSelectedIntegrationType("googleSheets"); + } else if (recording.airtable_base_id) { + setSelectedIntegrationType("airtable"); + } } }; fetchRecordingInfo(); }, [recordingId]); + // Initial integration type selection + if (!selectedIntegrationType) { + return ( + +
+ + {t("integration_settings.title_select_integration")} + +
+ + +
+
+
+ ); + } + return ( -
- {t("integration_settings.title")} +
+ + {selectedIntegrationType === "googleSheets" + ? t("integration_settings.title_google") + : t("integration_settings.title_airtable")} + - {recording && (recording.google_sheet_id || recording.airtable_base_id) ? ( + {recording && + (recording.google_sheet_id || recording.airtable_base_id ? ( + <> + + + {t("integration_settings.alerts.success.title")} + + {selectedIntegrationType === "googleSheets" ? ( + <> + {t("integration_settings.alerts.success.content", { + sheetName: recording.google_sheet_name, + })} + + {t("integration_settings.alerts.success.here")} + + + ) : ( + <> + {t("integration_settings.alerts.success.content", { + sheetName: recording.airtable_base_name, + })} + + {t("integration_settings.alerts.success.here")} + + + )} +
+ + {t("integration_settings.alerts.success.note")} + {" "} + {t("integration_settings.alerts.success.sync_limitation")} +
+ + + ) : null)} + + {!recording?.[ + selectedIntegrationType === "googleSheets" + ? "google_sheet_email" + : "airtable_email" + ] ? ( <> - {recording.google_sheet_id && ( - - {t("integration_settings.alerts.success.title")} - {t("integration_settings.alerts.success.content", { sheetName: recording.google_sheet_name })} - - {t("integration_settings.alerts.success.here")} - . -
- {t("integration_settings.alerts.success.note")} {t("integration_settings.alerts.success.sync_limitation")} -
- )} - - {recording.airtable_base_id && ( - - {t("integration_settings.alerts.success.title")} - {t("integration_settings.alerts.success.content", { sheetName: recording.airtable_base_name })} - - {t("integration_settings.alerts.success.here")} - . -
- {t("integration_settings.alerts.success.note")} {t("integration_settings.alerts.success.sync_limitation")} -
- )} - - ) : ( <> - {!recording?.google_sheet_email && !recording?.airtable_email ? ( + {recording[ + selectedIntegrationType === "googleSheets" + ? "google_sheet_email" + : "airtable_email" + ] && ( + + {t("integration_settings.descriptions.authenticated_as", { + email: + recording[ + selectedIntegrationType === "googleSheets" + ? "google_sheet_email" + : "airtable_email" + ], + })} + + )} + + {loading ? ( + + ) : error ? ( + {error} + ) : (selectedIntegrationType === "googleSheets" + ? spreadsheets + : airtableBases + ).length === 0 ? ( <> -

{t("integration_settings.descriptions.sync_info")}

- - +
+ + +
) : ( <> - {(recording.google_sheet_email || recording.airtable_email) && ( - - {t("integration_settings.descriptions.authenticated_as", { - email: recording.google_sheet_email || recording.airtable_email, + + {(selectedIntegrationType === "googleSheets" + ? spreadsheets + : airtableBases + ).map((item) => ( + + {item.name} + + ))} + + + {(selectedIntegrationType === "googleSheets" + ? settings.spreadsheetId + : settings.airtableBaseId) && ( + + {t("integration_settings.fields.selected_sheet", { + name: + selectedIntegrationType === "googleSheets" + ? spreadsheets.find( + (s) => s.id === settings.spreadsheetId + )?.name + : airtableBases.find( + (b) => b.id === settings.airtableBaseId + )?.name, + id: + selectedIntegrationType === "googleSheets" + ? settings.spreadsheetId + : settings.airtableBaseId, })} )} - {loading ? ( - - ) : error ? ( - {error} - ) : ( - <> - {recording.google_sheet_email && ( - <> - - {spreadsheets.map((sheet) => ( - - {sheet.name} - - ))} - - - {settings.spreadsheetId && ( - - {t("integration_settings.fields.selected_sheet", { - name: spreadsheets.find((s) => s.id === settings.spreadsheetId)?.name, - id: settings.spreadsheetId, - })} - - )} - - - - )} - - {recording.airtable_email && ( - <> - - {airtableBases.map((base) => ( - - {base.name} - - ))} - - - {settings.airtableBaseId && ( - - {t("integration_settings.fields.selected_base", { - name: airtableBases.find((b) => b.id === settings.airtableBaseId)?.name, - id: settings.airtableBaseId, - })} - - )} - - - - )} - - )} + )} @@ -462,4 +570,4 @@ export const modalStyle = { height: "fit-content", display: "block", padding: "20px", -}; \ No newline at end of file +}; From ae700764b9a3926d56c8960163eb7cc0b6be2a76 Mon Sep 17 00:00:00 2001 From: AmitChauhan63390 Date: Mon, 27 Jan 2025 21:29:11 +0530 Subject: [PATCH 004/197] integration modal changed --- server/src/routes/auth.ts | 11 +- .../integration/IntegrationSettings.tsx | 714 +++++++++--------- 2 files changed, 353 insertions(+), 372 deletions(-) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index c2994942..268134f4 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -672,8 +672,17 @@ router.get("/airtable/callback", async (req, res) => { if (err) console.error('Session cleanup error:', err); }); + res.cookie("airtable_auth_status", "success", { + httpOnly: false, + maxAge: 60000, + }); // 1-minute expiration + res.cookie("airtable_auth_message", "Robot successfully authenticated", { + httpOnly: false, + maxAge: 60000, + }); + res.redirect( - `${process.env.PUBLIC_URL}/robots/${state}/integrate?success=true` + `${process.env.PUBLIC_URL}/robots/${state}/integrate || http://localhost:5173/robots/${state}/integrate` ); } catch (error: any) { diff --git a/src/components/integration/IntegrationSettings.tsx b/src/components/integration/IntegrationSettings.tsx index 1d0534d1..9dd3bdc9 100644 --- a/src/components/integration/IntegrationSettings.tsx +++ b/src/components/integration/IntegrationSettings.tsx @@ -6,10 +6,9 @@ import { CircularProgress, Alert, AlertTitle, - Chip, + Button, + TextField, } from "@mui/material"; -import Button from "@mui/material/Button"; -import TextField from "@mui/material/TextField"; import axios from "axios"; import { useGlobalInfoStore } from "../../context/globalInfo"; import { getStoredRecording } from "../../api/storage"; @@ -52,9 +51,6 @@ export const IntegrationSettingsModal = ({ handleClose, }: IntegrationProps) => { const { t } = useTranslation(); - const [selectedIntegrationType, setSelectedIntegrationType] = useState< - "googleSheets" | "airtable" | null - >(null); const [settings, setSettings] = useState({ spreadsheetId: "", spreadsheetName: "", @@ -64,114 +60,61 @@ export const IntegrationSettingsModal = ({ integrationType: "googleSheets", }); - const [airtableTables, setAirtableTables] = useState<{ id: string; name: string }[]>([]); - const [spreadsheets, setSpreadsheets] = useState< - { id: string; name: string }[] - >([]); - const [airtableBases, setAirtableBases] = useState< - { id: string; name: string }[] - >([]); + const [spreadsheets, setSpreadsheets] = useState<{ id: string; name: string }[]>([]); + const [airtableBases, setAirtableBases] = useState<{ id: string; name: string }[]>([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const { recordingId, notify } = useGlobalInfoStore(); const [recording, setRecording] = useState(null); + const [airtableAuthStatus, setAirtableAuthStatus] = useState(null); + + // Authenticate with Google Sheets const authenticateWithGoogle = () => { window.location.href = `${apiUrl}/auth/google?robotId=${recordingId}`; }; + // Authenticate with Airtable const authenticateWithAirtable = () => { window.location.href = `${apiUrl}/auth/airtable?robotId=${recordingId}`; }; - const handleIntegrationType = (type: "googleSheets" | "airtable") => { - setSelectedIntegrationType(type); - setSettings({ - ...settings, - integrationType: type, - }); - }; - - const fetchAirtableTables = async (baseId: string) => { - try { - const response = await axios.get( - `${apiUrl}/auth/airtable/tables?baseId=${baseId}&robotId=${recordingId}`, - { - withCredentials: true, - } - ); - setAirtableTables(response.data); - } catch (error: any) { - console.error( - "Error fetching Airtable tables:", - error.response?.data?.message || error.message - ); - notify( - "error", - t("integration_settings.errors.fetch_error", { - message: error.response?.data?.message || error.message, - }) - ); - } - }; - + // Fetch Google Sheets files const fetchSpreadsheetFiles = async () => { try { const response = await axios.get( `${apiUrl}/auth/gsheets/files?robotId=${recordingId}`, - { - withCredentials: true, - } + { withCredentials: true } ); setSpreadsheets(response.data); } catch (error: any) { - console.error( - "Error fetching spreadsheet files:", - error.response?.data?.message || error.message - ); - notify( - "error", - t("integration_settings.errors.fetch_error", { - message: error.response?.data?.message || error.message, - }) - ); + console.error("Error fetching spreadsheet files:", error); + notify("error", t("integration_settings.errors.fetch_error", { + message: error.response?.data?.message || error.message, + })); } }; - console.log("recordingId", recordingId); - + // Fetch Airtable bases const fetchAirtableBases = async () => { try { const response = await axios.get( `${apiUrl}/auth/airtable/bases?robotId=${recordingId}`, - { - withCredentials: true, - } + { withCredentials: true } ); - setAirtableBases(response.data); - - console.log("Airtable bases:", response.data); - } catch (error: any) { - console.error( - "Error fetching Airtable bases:", - error.response?.data?.message || error.message - ); - notify( - "error", - t("integration_settings.errors.fetch_error", { - message: error.response?.data?.message || error.message, - }) - ); + console.error("Error fetching Airtable bases:", error); + notify("error", t("integration_settings.errors.fetch_error", { + message: error.response?.data?.message || error.message, + })); } }; + // Handle Google Sheets selection const handleSpreadsheetSelect = (e: React.ChangeEvent) => { - const selectedSheet = spreadsheets.find( - (sheet) => sheet.id === e.target.value - ); + const selectedSheet = spreadsheets.find((sheet) => sheet.id === e.target.value); if (selectedSheet) { setSettings({ ...settings, @@ -181,10 +124,9 @@ export const IntegrationSettingsModal = ({ } }; + // Handle Airtable base selection const handleAirtableBaseSelect = (e: React.ChangeEvent) => { - const selectedBase = airtableBases.find( - (base) => base.id === e.target.value - ); + const selectedBase = airtableBases.find((base) => base.id === e.target.value); if (selectedBase) { setSettings({ ...settings, @@ -194,9 +136,10 @@ export const IntegrationSettingsModal = ({ } }; + // Update Google Sheets integration const updateGoogleSheetId = async () => { try { - const response = await axios.post( + await axios.post( `${apiUrl}/auth/gsheets/update`, { spreadsheetId: settings.spreadsheetId, @@ -205,19 +148,19 @@ export const IntegrationSettingsModal = ({ }, { withCredentials: true } ); - notify(`success`, t("integration_settings.notifications.sheet_selected")); - console.log("Google Sheet ID updated:", response.data); + notify("success", t("integration_settings.notifications.sheet_selected")); } catch (error: any) { - console.error( - "Error updating Google Sheet ID:", - error.response?.data?.message || error.message - ); + console.error("Error updating Google Sheet ID:", error); + notify("error", t("integration_settings.errors.update_error", { + message: error.response?.data?.message || error.message, + })); } }; - const updateAirtableBaseId = async () => { + // Update Airtable integration + const updateAirtableBase = async () => { try { - const response = await axios.post( + await axios.post( `${apiUrl}/auth/airtable/update`, { baseId: settings.airtableBaseId, @@ -226,332 +169,361 @@ export const IntegrationSettingsModal = ({ }, { withCredentials: true } ); - notify(`success`, t("integration_settings.notifications.base_selected")); - console.log("Airtable Base ID updated:", response.data); + notify("success", t("integration_settings.notifications.base_selected")); } catch (error: any) { - console.error( - "Error updating Airtable Base ID:", - error.response?.data?.message || error.message - ); + console.error("Error updating Airtable base:", error); + notify("error", t("integration_settings.errors.update_error", { + message: error.response?.data?.message || error.message, + })); } }; - const removeIntegration = async () => { + // Remove Google Sheets integration + const removeGoogleSheetsIntegration = async () => { try { - const endpoint = - selectedIntegrationType === "googleSheets" - ? "/auth/gsheets/remove" - : "/auth/airtable/remove"; - await axios.post( - `${apiUrl}${endpoint}`, + `${apiUrl}/auth/gsheets/remove`, { robotId: recordingId }, { withCredentials: true } ); - - setRecording(null); setSpreadsheets([]); - setAirtableBases([]); - setSelectedIntegrationType(null); - setSettings({ - spreadsheetId: "", - spreadsheetName: "", - airtableBaseId: "", - airtableBaseName: "", - data: "", - integrationType: "googleSheets", - }); + setSettings({ ...settings, spreadsheetId: "", spreadsheetName: "" }); + notify("success", t("integration_settings.notifications.integration_removed")); } catch (error: any) { - console.error( - "Error removing integration:", - error.response?.data?.message || error.message - ); + console.error("Error removing Google Sheets integration:", error); + notify("error", t("integration_settings.errors.remove_error", { + message: error.response?.data?.message || error.message, + })); } }; + // Remove Airtable integration + const removeAirtableIntegration = async () => { + try { + await axios.post( + `${apiUrl}/auth/airtable/remove`, + { robotId: recordingId }, + { withCredentials: true } + ); + setAirtableBases([]); + setSettings({ ...settings, airtableBaseId: "", airtableBaseName: "" }); + notify("success", t("integration_settings.notifications.integration_removed")); + } catch (error: any) { + console.error("Error removing Airtable integration:", error); + notify("error", t("integration_settings.errors.remove_error", { + message: error.response?.data?.message || error.message, + })); + } + }; + + // Handle OAuth callback for Airtable + const handleAirtableOAuthCallback = async () => { + try { + const response = await axios.get(`${apiUrl}/auth/airtable/callback`); + if (response.data.success) { + setAirtableAuthStatus(true); + fetchAirtableBases(); // Fetch bases after successful authentication + } + } catch (error) { + setError("Error authenticating with Airtable"); + } + }; + + // Fetch recording info on component mount useEffect(() => { - const status = getCookie("robot_auth_status"); - const message = getCookie("robot_auth_message"); - - if (status === "success" && message) { - notify("success", message); - removeCookie("robot_auth_status"); - removeCookie("robot_auth_message"); - } - - const urlParams = new URLSearchParams(window.location.search); - const code = urlParams.get("code"); - if (code) { - // Determine which authentication callback to handle - // You'll need to implement similar callback logic for Airtable - } - const fetchRecordingInfo = async () => { if (!recordingId) return; const recording = await getStoredRecording(recordingId); if (recording) { setRecording(recording); - // Determine integration type based on existing integration if (recording.google_sheet_id) { - setSelectedIntegrationType("googleSheets"); + setSettings({ ...settings, integrationType: "googleSheets" }); } else if (recording.airtable_base_id) { - setSelectedIntegrationType("airtable"); + setSettings({ ...settings, integrationType: "airtable" }); } } }; - fetchRecordingInfo(); }, [recordingId]); - // Initial integration type selection - if (!selectedIntegrationType) { - return ( - -
- - {t("integration_settings.title_select_integration")} - -
- - -
+ // Handle Airtable authentication status + useEffect(() => { + const status = getCookie("airtable_auth_status"); + const message = getCookie("airtable_auth_message"); + + if (status === "success" && message) { + notify("success", message); + removeCookie("airtable_auth_status"); + removeCookie("airtable_auth_message"); + setAirtableAuthStatus(true); + fetchAirtableBases(); // Fetch bases after successful authentication + } + + const urlParams = new URLSearchParams(window.location.search); + const code = urlParams.get("code"); + if (code) { + handleAirtableOAuthCallback(); + } + }, [recordingId]); + + console.log(recording) + + + const [selectedIntegrationType, setSelectedIntegrationType] = useState< + "googleSheets" | "airtable" | null +>(null); + +// Add this UI at the top of the modal return statement +if (!selectedIntegrationType) { + return ( + +
+ + {t("integration_settings.title_select_integration")} + +
+ {/* Google Sheets Button */} + + + {/* Airtable Button */} +
- - ); - } +
+
+ ); +} + + return ( -
+
- {selectedIntegrationType === "googleSheets" - ? t("integration_settings.title_google") - : t("integration_settings.title_airtable")} + {t("integration_settings.title")} - {recording && - (recording.google_sheet_id || recording.airtable_base_id ? ( - <> - - - {t("integration_settings.alerts.success.title")} - - {selectedIntegrationType === "googleSheets" ? ( - <> - {t("integration_settings.alerts.success.content", { - sheetName: recording.google_sheet_name, - })} - - {t("integration_settings.alerts.success.here")} - - - ) : ( - <> - {t("integration_settings.alerts.success.content", { - sheetName: recording.airtable_base_name, - })} - - {t("integration_settings.alerts.success.here")} - - - )} -
- - {t("integration_settings.alerts.success.note")} - {" "} - {t("integration_settings.alerts.success.sync_limitation")} -
- - - ) : null)} - - {!recording?.[ - selectedIntegrationType === "googleSheets" - ? "google_sheet_email" - : "airtable_email" - ] ? ( + {/* Google Sheets Integration */} + {settings.integrationType === "googleSheets" && ( <> -

{t("integration_settings.descriptions.sync_info")}

- - - ) : ( - <> - {recording[ - selectedIntegrationType === "googleSheets" - ? "google_sheet_email" - : "airtable_email" - ] && ( - - {t("integration_settings.descriptions.authenticated_as", { - email: - recording[ - selectedIntegrationType === "googleSheets" - ? "google_sheet_email" - : "airtable_email" - ], - })} - - )} - - {loading ? ( - - ) : error ? ( - {error} - ) : (selectedIntegrationType === "googleSheets" - ? spreadsheets - : airtableBases - ).length === 0 ? ( + {recording?.google_sheet_id ? ( <> -
- - -
+ {t("integration_settings.alerts.success.here")} + + + ) : ( <> - - {(selectedIntegrationType === "googleSheets" - ? spreadsheets - : airtableBases - ).map((item) => ( - - {item.name} - - ))} - - - {(selectedIntegrationType === "googleSheets" - ? settings.spreadsheetId - : settings.airtableBaseId) && ( - - {t("integration_settings.fields.selected_sheet", { - name: - selectedIntegrationType === "googleSheets" - ? spreadsheets.find( - (s) => s.id === settings.spreadsheetId - )?.name - : airtableBases.find( - (b) => b.id === settings.airtableBaseId - )?.name, - id: - selectedIntegrationType === "googleSheets" - ? settings.spreadsheetId - : settings.airtableBaseId, - })} - + {!recording?.google_sheet_email ? ( + <> +

{t("integration_settings.descriptions.sync_info")}

+ + + ) : ( + <> + + {t("integration_settings.descriptions.authenticated_as", { + email: recording.google_sheet_email, + })} + + {loading ? ( + + ) : error ? ( + {error} + ) : spreadsheets.length === 0 ? ( + + ) : ( + <> + + {spreadsheets.map((sheet) => ( + + {sheet.name} + + ))} + + + + )} + )} + + )} + + )} + {/* Airtable Integration */} + {settings.integrationType === "airtable" && ( + <> + {recording?.airtable_base_id ? ( + <> + + {t("integration_settings.alerts.airtable_success.title")} + {t("integration_settings.alerts.airtable_success.content", { + baseName: recording.airtable_base_name, + })} + + {t("integration_settings.alerts.airtable_success.here")} + + + ) : ( + <> + {!recording?.airtable_access_token ? ( + <> +

{t("integration_settings.descriptions.airtable_sync_info")}

+ + + ) : ( + <> + + {t("integration_settings.descriptions.authenticated_as", { + email: "hghghg", + })} + + {loading ? ( + + ) : error ? ( + {error} + ) : airtableBases.length === 0 ? ( + + ) : ( + <> + + {airtableBases.map((base) => ( + + {base.name} + + ))} + + + + )} + + )} + )} )} @@ -570,4 +542,4 @@ export const modalStyle = { height: "fit-content", display: "block", padding: "20px", -}; +}; \ No newline at end of file From 6a854f953122a36658db34278ed2683bee42520f Mon Sep 17 00:00:00 2001 From: AmitChauhan63390 Date: Thu, 30 Jan 2025 15:56:00 +0530 Subject: [PATCH 005/197] proper integration but writing data to airtable yet to be fixed --- server/src/routes/auth.ts | 49 ++++- server/src/routes/storage.ts | 10 + .../integrations/airtable.ts | 199 ++++++++++++++++++ .../integration/IntegrationSettings.tsx | 123 ++++++++++- 4 files changed, 371 insertions(+), 10 deletions(-) create mode 100644 server/src/workflow-management/integrations/airtable.ts diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 268134f4..86749313 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -733,8 +733,8 @@ router.get("/airtable/bases", async (req: AuthenticatedRequest, res) => { }); // Update robot with selected base -router.post("/airtable/update", requireSignIn, async (req: AuthenticatedRequest, res) => { - const { baseId, baseName, robotId } = req.body; +router.post("/airtable/update", async (req: AuthenticatedRequest, res) => { + const { baseId, robotId , tableName} = req.body; if (!baseId || !robotId) { return res.status(400).json({ message: "Base ID and Robot ID are required" }); @@ -751,7 +751,8 @@ router.post("/airtable/update", requireSignIn, async (req: AuthenticatedRequest, await robot.update({ airtable_base_id: baseId, - airtable_table_name: baseName, + airtable_table_name: tableName, + }); capture("maxun-oss-airtable-integration-created", { @@ -803,3 +804,45 @@ router.post("/airtable/remove", requireSignIn, async (req: AuthenticatedRequest, } }); + + +// Fetch tables from an Airtable base +router.get("/airtable/tables", async (req: AuthenticatedRequest, res) => { + try { + const { baseId, robotId } = req.query; + + if (!baseId || !robotId) { + return res.status(400).json({ message: "Base ID and Robot ID are required" }); + } + + const robot = await Robot.findOne({ + where: { "recording_meta.id": robotId.toString() }, + raw: true, + }); + + if (!robot?.airtable_access_token) { + return res.status(400).json({ message: "Robot not authenticated with Airtable" }); + } + + const response = await fetch(`https://api.airtable.com/v0/meta/bases/${baseId}/tables`, { + headers: { + 'Authorization': `Bearer ${robot.airtable_access_token}` + } + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error.message || 'Failed to fetch tables'); + } + + const data = await response.json(); + res.json(data.tables.map((table: any) => ({ + id: table.id, + name: table.name, + fields: table.fields + }))); + + } catch (error: any) { + res.status(500).json({ message: error.message }); + } +}); \ No newline at end of file diff --git a/server/src/routes/storage.ts b/server/src/routes/storage.ts index ddadf240..0fdd69ce 100644 --- a/server/src/routes/storage.ts +++ b/server/src/routes/storage.ts @@ -20,6 +20,7 @@ import { capture } from "../utils/analytics"; import { tryCatch } from 'bullmq'; import { WorkflowFile } from 'maxun-core'; import { Page } from 'playwright'; +import { airtableUpdateTasks, processAirtableUpdates } from '../workflow-management/integrations/airtable'; chromium.use(stealthPlugin()); export const router = Router(); @@ -514,6 +515,15 @@ router.post('/runs/run/:id', requireSignIn, async (req: AuthenticatedRequest, re status: 'pending', retries: 5, }; + + airtableUpdateTasks[plainRun.runId] = { + robotId: plainRun.robotMetaId, + runId: plainRun.runId, + status: 'pending', + retries: 5, + }; + + processAirtableUpdates(); processGoogleSheetUpdates(); } catch (err: any) { logger.log('error', `Failed to update Google Sheet for run: ${plainRun.runId}: ${err.message}`); diff --git a/server/src/workflow-management/integrations/airtable.ts b/server/src/workflow-management/integrations/airtable.ts new file mode 100644 index 00000000..9ace046c --- /dev/null +++ b/server/src/workflow-management/integrations/airtable.ts @@ -0,0 +1,199 @@ +import Airtable from "airtable"; +import axios from "axios"; +import logger from "../../logger"; +import Run from "../../models/Run"; +import Robot from "../../models/Robot"; + +interface AirtableUpdateTask { + robotId: string; + runId: string; + status: 'pending' | 'completed' | 'failed'; + retries: number; +} + +const MAX_RETRIES = 5; +const BASE_API_DELAY = 2000; + +export let airtableUpdateTasks: { [runId: string]: AirtableUpdateTask } = {}; + +export async function updateAirtable(robotId: string, runId: string) { + try { + const run = await Run.findOne({ where: { runId } }); + if (!run) throw new Error(`Run not found for runId: ${runId}`); + + const plainRun = run.toJSON(); + if (plainRun.status !== 'success') { + console.log('Run status is not success'); + return; + } + + let data: { [key: string]: any }[] = []; + if (plainRun.serializableOutput?.['item-0']) { + data = plainRun.serializableOutput['item-0'] as { [key: string]: any }[]; + } else if (plainRun.binaryOutput?.['item-0']) { + data = [{ "File URL": plainRun.binaryOutput['item-0'] }]; + } + + const robot = await Robot.findOne({ where: { 'recording_meta.id': robotId } }); + if (!robot) throw new Error(`Robot not found for robotId: ${robotId}`); + + const plainRobot = robot.toJSON(); + if (plainRobot.airtable_base_id && plainRobot.airtable_table_name) { + console.log(`Writing to Airtable base ${plainRobot.airtable_base_id}`); + await writeDataToAirtable( + robotId, + plainRobot.airtable_base_id, + plainRobot.airtable_table_name, + data + ); + console.log(`Data written to Airtable for ${robotId}`); + } + } catch (error: any) { + console.error(`Airtable update failed: ${error.message}`); + throw error; + } +} + +export async function writeDataToAirtable( + robotId: string, + baseId: string, + tableName: string, + data: any[] +) { + try { + const robot = await Robot.findOne({ where: { 'recording_meta.id': robotId } }); + if (!robot) throw new Error('Robot not found'); + + const accessToken = robot.get('airtable_access_token'); + if (!accessToken) throw new Error('Airtable not connected'); + + const airtable = new Airtable({ apiKey: accessToken }); + const base = airtable.base(baseId); + + const existingFields = await getExistingFields(base, tableName); + const dataFields = [...new Set(data.flatMap(row => Object.keys(row)))]; + const missingFields = dataFields.filter(field => !existingFields.includes(field)); + + for (const field of missingFields) { + const sampleValue = data.find(row => row[field])?.[field]; + if (sampleValue) { + await createAirtableField(baseId, tableName, field, sampleValue, accessToken); + } + } + + const batchSize = 10; + for (let i = 0; i < data.length; i += batchSize) { + const batch = data.slice(i, i + batchSize); + await retryableAirtableWrite(base, tableName, batch); + } + + logger.log('info', `Successfully wrote ${data.length} records to Airtable`); + } catch (error: any) { + logger.log('error', `Airtable write failed: ${error.message}`); + throw error; + } +} + +async function getExistingFields(base: Airtable.Base, tableName: string): Promise { + try { + const records = await base(tableName).select({ maxRecords: 1 }).firstPage(); + return records[0] ? Object.keys(records[0].fields) : []; + } catch (error) { + return []; + } +} + +async function createAirtableField( + baseId: string, + tableName: string, + fieldName: string, + sampleValue: any, + accessToken: string, + retries = MAX_RETRIES +): Promise { + try { + let fieldType = inferFieldType(sampleValue); + + // Fallback if field type is unknown + if (!fieldType) { + fieldType = 'singleLineText'; + logger.log('warn', `Unknown field type for ${fieldName}, defaulting to singleLineText`); + } + + console.log(`Creating field: ${fieldName}, Type: ${fieldType}`); + + await axios.post( + `https://api.airtable.com/v0/meta/bases/${baseId}/tables/${tableName}/fields`, + { name: fieldName, type: fieldType }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json' + } + } + ); + + logger.log('info', `Created field: ${fieldName} (${fieldType})`); + } catch (error: any) { + if (retries > 0 && error.response?.status === 429) { + await delay(BASE_API_DELAY * (MAX_RETRIES - retries + 2)); + return createAirtableField(baseId, tableName, fieldName, sampleValue, accessToken, retries - 1); + } + throw new Error(`Field creation failed: ${error.response?.data?.error?.message || 'Unknown error'}`); + } +} + +function inferFieldType(value: any): string { + if (typeof value === 'number') return 'number'; + if (typeof value === 'boolean') return 'checkbox'; + if (value instanceof Date) return 'dateTime'; + if (Array.isArray(value)) return 'multipleSelects'; + return 'singleLineText'; +} + +async function retryableAirtableWrite( + base: Airtable.Base, + tableName: string, + batch: any[], + retries = MAX_RETRIES +): Promise { + try { + await base(tableName).create(batch.map(row => ({ fields: row }))); + } catch (error) { + if (retries > 0) { + await delay(BASE_API_DELAY); + return retryableAirtableWrite(base, tableName, batch, retries - 1); + } + throw error; + } +} + +function delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +export const processAirtableUpdates = async () => { + while (true) { + let hasPendingTasks = false; + + for (const runId in airtableUpdateTasks) { + const task = airtableUpdateTasks[runId]; + if (task.status !== 'pending') continue; + + hasPendingTasks = true; + try { + await updateAirtable(task.robotId, task.runId); + delete airtableUpdateTasks[runId]; + } catch (error: any) { + task.retries += 1; + if (task.retries >= MAX_RETRIES) { + task.status = 'failed'; + logger.log('error', `Permanent failure for run ${runId}`); + } + } + } + + if (!hasPendingTasks) break; + await delay(5000); + } +}; diff --git a/src/components/integration/IntegrationSettings.tsx b/src/components/integration/IntegrationSettings.tsx index 9dd3bdc9..3fc62979 100644 --- a/src/components/integration/IntegrationSettings.tsx +++ b/src/components/integration/IntegrationSettings.tsx @@ -15,6 +15,7 @@ import { getStoredRecording } from "../../api/storage"; import { apiUrl } from "../../apiConfig.js"; import Cookies from "js-cookie"; import { useTranslation } from "react-i18next"; +import { SignalCellularConnectedNoInternet0BarSharp } from "@mui/icons-material"; interface IntegrationProps { isOpen: boolean; @@ -27,8 +28,11 @@ export interface IntegrationSettings { spreadsheetName?: string; airtableBaseId?: string; airtableBaseName?: string; + airtableTableName?: string, + airtableTableId?: string, data: string; integrationType: "googleSheets" | "airtable"; + } // Helper functions to replace js-cookie functionality @@ -56,12 +60,17 @@ export const IntegrationSettingsModal = ({ spreadsheetName: "", airtableBaseId: "", airtableBaseName: "", + airtableTableName: "", + airtableTableId: "", + data: "", integrationType: "googleSheets", + }); const [spreadsheets, setSpreadsheets] = useState<{ id: string; name: string }[]>([]); const [airtableBases, setAirtableBases] = useState<{ id: string; name: string }[]>([]); + const [airtableTables, setAirtableTables] = useState<{ id: string; name: string }[]>([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -112,6 +121,23 @@ export const IntegrationSettingsModal = ({ } }; + + const fetchAirtableTables = async (baseId: string, recordingId: string) => { + try { + const response = await axios.get( + `${apiUrl}/auth/airtable/tables?robotId=${recordingId}&baseId=${baseId}`, + { withCredentials: true } + ); + setAirtableTables(response.data); + } + catch (error: any) { + console.error("Error fetching Airtable tables:", error); + notify("error", t("integration_settings.errors.fetch_error", { + message: error.response?.data?.message || error.message, + })); + } + } + // Handle Google Sheets selection const handleSpreadsheetSelect = (e: React.ChangeEvent) => { const selectedSheet = spreadsheets.find((sheet) => sheet.id === e.target.value); @@ -125,17 +151,74 @@ export const IntegrationSettingsModal = ({ }; // Handle Airtable base selection - const handleAirtableBaseSelect = (e: React.ChangeEvent) => { + const handleAirtableBaseSelect = async (e: React.ChangeEvent) => { const selectedBase = airtableBases.find((base) => base.id === e.target.value); + console.log(selectedBase); + if (selectedBase) { - setSettings({ - ...settings, + // Update local state + setSettings((prevSettings) => ({ + ...prevSettings, airtableBaseId: selectedBase.id, airtableBaseName: selectedBase.name, - }); + })); + + // Fetch tables for the selected base + if (recordingId) { + await fetchAirtableTables(selectedBase.id, recordingId); + } else { + console.error("Recording ID is null"); + } + + + + // try { + // // Ensure recordingId is available + // if (!recordingId) { + // throw new Error("Recording ID is missing"); + // } + + // // Make API call to update the base in the database + // const response = await axios.post( + // `${apiUrl}/auth/airtable/update`, + // { + // baseId: selectedBase.id, + // baseName: selectedBase.name, + // robotId: recordingId, // Use recordingId from the global context + // }, + // { withCredentials: true } + // ); + + // if (response.status !== 200) { + // throw new Error("Failed to update Airtable base in the database"); + // } + + // console.log("Airtable base updated successfully:", response.data); + // } catch (error) { + // console.error("Error updating Airtable base:", error); + // notify("error", t("integration_settings.errors.update_error", { + // message: error instanceof Error ? error.message : "Unknown error", + // })); + // } } }; + const handleAirtabletableSelect = (e: React.ChangeEvent) => { + console.log( e.target.value); + const selectedTable = airtableTables.find((table) => table.id === e.target.value); + if (selectedTable) { + setSettings((prevSettings) => ({ + ...prevSettings, + airtableTableId: e.target.value, + + airtableTableName: selectedTable?.name||"", + + })); + } + }; + + + // Update Google Sheets integration const updateGoogleSheetId = async () => { try { @@ -159,6 +242,10 @@ export const IntegrationSettingsModal = ({ // Update Airtable integration const updateAirtableBase = async () => { + console.log(settings.airtableBaseId); + console.log(settings.airtableTableName); + console.log(recordingId); + console.log(settings.airtableBaseName); try { await axios.post( `${apiUrl}/auth/airtable/update`, @@ -166,6 +253,7 @@ export const IntegrationSettingsModal = ({ baseId: settings.airtableBaseId, baseName: settings.airtableBaseName, robotId: recordingId, + tableName: settings.airtableTableName, }, { withCredentials: true } ); @@ -206,7 +294,7 @@ export const IntegrationSettingsModal = ({ { withCredentials: true } ); setAirtableBases([]); - setSettings({ ...settings, airtableBaseId: "", airtableBaseName: "" }); + setSettings({ ...settings, airtableBaseId: "", airtableBaseName: "", airtableTableName:"" }); notify("success", t("integration_settings.notifications.integration_removed")); } catch (error: any) { console.error("Error removing Airtable integration:", error); @@ -239,7 +327,13 @@ export const IntegrationSettingsModal = ({ if (recording.google_sheet_id) { setSettings({ ...settings, integrationType: "googleSheets" }); } else if (recording.airtable_base_id) { - setSettings({ ...settings, integrationType: "airtable" }); + setSettings(prev => ({ + ...prev, + airtableBaseId: recording.airtable_base_id || "", + airtableBaseName: recording.airtable_base_name || "", + airtableTableName: recording.airtable_table_name || "", + integrationType: recording.airtable_base_id ? "airtable" : "googleSheets" + })); } } }; @@ -475,7 +569,7 @@ if (!selectedIntegrationType) { <> {t("integration_settings.descriptions.authenticated_as", { - email: "hghghg", + email: "amit63390@gmail.com", })} {loading ? ( @@ -507,6 +601,21 @@ if (!selectedIntegrationType) { ))} + + {airtableTables.map((table) => ( + + {table.name} + + ))} + ) : ( <> {!recording?.google_sheet_email ? ( <> -

{t("integration_settings.descriptions.sync_info")}

+

{t("integration_settings.google.descriptions.sync_info")}

) : ( <> - {t("integration_settings.descriptions.authenticated_as", { + {t("integration_settings.google.descriptions.authenticated_as", { email: recording.google_sheet_email, })} @@ -492,14 +453,14 @@ if (!selectedIntegrationType) { color="primary" onClick={fetchSpreadsheetFiles} > - {t("integration_settings.buttons.fetch_sheets")} + {t("integration_settings.google.buttons.fetch_sheets")} ) : ( <> - {t("integration_settings.buttons.submit")} + {t("integration_settings.google.buttons.submit")} )} @@ -535,19 +496,24 @@ if (!selectedIntegrationType) { {/* Airtable Integration */} {settings.integrationType === "airtable" && ( <> + + {t("integration_settings.airtable.title")} + + {recording?.airtable_base_id ? ( <> - - {t("integration_settings.alerts.airtable_success.title")} - {t("integration_settings.alerts.airtable_success.content", { + + {t("integration_settings.airtable.alerts.success.title")} + {t("integration_settings.airtable.alerts.success.content", { baseName: recording.airtable_base_name, + tableName: recording.airtable_table_name })} - {t("integration_settings.alerts.airtable_success.here")} + {t("integration_settings.airtable.alerts.success.here")} ) : ( <> {!recording?.airtable_access_token ? ( <> -

{t("integration_settings.descriptions.airtable_sync_info")}

+

{t("integration_settings.airtable.descriptions.sync_info")}

) : ( <> - {t("integration_settings.descriptions.authenticated_as", { - email: "", - })} + {t("integration_settings.airtable.descriptions.authenticated_as")} {loading ? ( @@ -586,17 +550,17 @@ if (!selectedIntegrationType) { ) : airtableBases.length === 0 ? ( ) : ( <> )} From 2ec9bbd026f511e551cd77562869e40c1126f29b Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 26 Feb 2025 14:47:23 +0530 Subject: [PATCH 024/197] feat: add sign in middleware --- server/src/routes/auth.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 92e7a7e3..09db64ab 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -584,7 +584,7 @@ router.post( // Airtable OAuth Routes -router.get("/airtable", (req: Request, res) => { +router.get("/airtable", requireSignIn, (req: Request, res) => { const authenticatedReq = req as AuthenticatedRequest; const { robotId } = authenticatedReq.query; if (!robotId) { @@ -708,7 +708,7 @@ router.get("/airtable/callback", async (req: Request, res) => { }); // Get Airtable bases -router.get("/airtable/bases", async (req: Request, res) => { +router.get("/airtable/bases", requireSignIn, async (req: Request, res) => { const authenticatedReq = req as AuthenticatedRequest; try { const { robotId } = authenticatedReq.query; @@ -748,7 +748,7 @@ router.get("/airtable/bases", async (req: Request, res) => { }); // Update robot with selected base -router.post("/airtable/update", async (req: Request, res) => { +router.post("/airtable/update", requireSignIn, async (req: Request, res) => { const authenticatedReq = req as AuthenticatedRequest; const { baseId, robotId , baseName, tableName, tableId} = req.body; @@ -827,7 +827,7 @@ router.post("/airtable/remove", requireSignIn, async (req: Request, res) => { // Fetch tables from an Airtable base -router.get("/airtable/tables", async (req: Request, res) => { +router.get("/airtable/tables", requireSignIn,async (req: Request, res) => { const authenticatedReq = req as AuthenticatedRequest; try { const { baseId, robotId } = authenticatedReq.query; From 4b05e7bac66530da3eb5d6529ab72734c4b212b3 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 26 Feb 2025 14:49:00 +0530 Subject: [PATCH 025/197] feat: add translation for integration --- public/locales/de.json | 88 ++++++++++++++++++++++++++------- public/locales/en.json | 107 ++++++++++++++++++++++++++++++----------- public/locales/es.json | 88 ++++++++++++++++++++++++++------- public/locales/ja.json | 90 ++++++++++++++++++++++++++-------- public/locales/zh.json | 90 ++++++++++++++++++++++++++-------- 5 files changed, 362 insertions(+), 101 deletions(-) diff --git a/public/locales/de.json b/public/locales/de.json index 613ddfef..fdc4e667 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -287,29 +287,81 @@ } }, "integration_settings": { - "title": "Mit Google Sheet integrieren", + "title": "Integrationseinstellungen", + "title_select_integration": "Integrationstyp auswählen", "descriptions": { - "sync_info": "Wenn Sie diese Option aktivieren, werden bei jeder erfolgreichen Ausführung dieses Roboters die erfassten Daten in Ihrem Google Sheet ergänzt.", "authenticated_as": "Authentifiziert als: {{email}}" }, - "alerts": { - "success": { - "title": "Google Sheet erfolgreich integriert.", - "content": "Jedes Mal, wenn dieser Roboter eine erfolgreiche Ausführung erstellt, werden die erfassten Daten Ihrem Google Sheet {{sheetName}} hinzugefügt. Sie können die Datenaktualisierungen", - "here": "hier", - "note": "Hinweis:", - "sync_limitation": "Die vor der Integration mit Google Sheets extrahierten Daten werden nicht im Google Sheet synchronisiert. Nur die nach der Integration extrahierten Daten werden synchronisiert." + "buttons": { + "submit": "Absenden", + "remove_integration": "Integration entfernen" + }, + "google": { + "title": "Mit Google Sheet integrieren", + "descriptions": { + "sync_info": "Wenn Sie diese Option aktivieren, werden bei jeder erfolgreichen Ausführung dieses Roboters die erfassten Daten in Ihrem Google Sheet ergänzt.", + "authenticated_as": "Authentifiziert als: {{email}}" + }, + "alerts": { + "success": { + "title": "Google Sheet erfolgreich integriert.", + "content": "Jedes Mal, wenn dieser Roboter eine erfolgreiche Ausführung erstellt, werden die erfassten Daten Ihrem Google Sheet {{sheetName}} hinzugefügt. Sie können die Datenaktualisierungen", + "here": "hier", + "note": "Hinweis:", + "sync_limitation": "Die vor der Integration mit Google Sheets extrahierten Daten werden nicht im Google Sheet synchronisiert. Nur die nach der Integration extrahierten Daten werden synchronisiert." + } + }, + "buttons": { + "authenticate": "Mit Google authentifizieren", + "fetch_sheets": "Google Sheets abrufen", + "remove_integration": "Integration entfernen", + "submit": "Absenden" + }, + "fields": { + "select_sheet": "Google Sheet auswählen", + "selected_sheet": "Ausgewähltes Sheet: {{name}} (ID: {{id}})" } }, - "buttons": { - "authenticate": "Mit Google authentifizieren", - "fetch_sheets": "Google Sheets abrufen", - "remove_integration": "Integration entfernen", - "submit": "Absenden" - }, - "fields": { - "select_sheet": "Google Sheet auswählen", - "selected_sheet": "Ausgewähltes Sheet: {{name}} (ID: {{id}})" + "airtable": { + "title": "Mit Airtable verbinden", + "descriptions": { + "sync_info": "Verbinden Sie Ihren Roboter mit einer Airtable-Basis, um Daten automatisch zu synchronisieren.", + "authenticated_as": "Mit Airtable authentifiziert" + }, + "alerts": { + "success": { + "title": "Mit Airtable verbunden", + "content": "Ihr Roboter ist jetzt mit der Basis: {{baseName}} und Tabelle: {{tableName}} verbunden. Sie können darauf zugreifen ", + "here": "hier", + "note": "Hinweis:", + "sync_limitation": "Nur nach der Integration erfasste Daten werden mit Airtable synchronisiert." + } + }, + "buttons": { + "authenticate": "Mit Airtable verbinden", + "fetch_bases": "Airtable-Basen abrufen", + "fetch_tables": "Airtable-Tabellen abrufen", + "remove_integration": "Airtable-Integration entfernen", + "submit": "Basis und Tabelle auswählen" + }, + "fields": { + "select_base": "Airtable-Basis auswählen", + "select_table": "Airtable-Tabelle auswählen", + "selected_base": "Ausgewählte Basis: {{name}}", + "selected_table": "Ausgewählte Tabelle: {{name}}" + }, + "errors": { + "auth_error": "Fehler bei der Authentifizierung mit Airtable", + "fetch_error": "Fehler beim Abrufen von Airtable-Basen: {{message}}", + "fetch_tables_error": "Fehler beim Abrufen von Airtable-Tabellen: {{message}}", + "update_error": "Fehler beim Aktualisieren der Airtable-Basis: {{message}}", + "remove_error": "Fehler beim Entfernen der Airtable-Integration: {{message}}" + }, + "notifications": { + "base_selected": "Airtable-Basis erfolgreich ausgewählt", + "table_selected": "Airtable-Tabelle erfolgreich ausgewählt", + "integration_removed": "Airtable-Integration erfolgreich entfernt" + } } }, "robot_duplication": { diff --git a/public/locales/en.json b/public/locales/en.json index 85bd3148..19c2ea7e 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -289,38 +289,91 @@ } }, "integration_settings": { - "title": "Integrate with Google Sheet", + "title": "Integration Settings", + "title_select_integration": "Select Integration Type", "descriptions": { - "sync_info": "If you enable this option, every time this robot runs a task successfully, its captured data will be appended to your Google Sheet.", "authenticated_as": "Authenticated as: {{email}}" }, - "alerts": { - "success": { - "title": "Google Sheet Integrated Successfully.", - "content": "Every time this robot creates a successful run, its captured data is appended to your {{sheetName}} Google Sheet. You can check the data updates", - "here": "here", - "note": "Note:", - "sync_limitation": "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." + "buttons": { + "submit": "Submit", + "remove_integration": "Remove Integration" + }, + "google": { + "title": "Integrate with Google Sheet", + "descriptions": { + "sync_info": "If you enable this option, every time this robot runs a task successfully, its captured data will be appended to your Google Sheet.", + "authenticated_as": "Authenticated as: {{email}}" + }, + "alerts": { + "success": { + "title": "Google Sheet Integrated Successfully.", + "content": "Every time this robot creates a successful run, its captured data is appended to your {{sheetName}} Google Sheet. You can check the data updates", + "here": "here", + "note": "Note:", + "sync_limitation": "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." + } + }, + "buttons": { + "authenticate": "Authenticate with Google", + "fetch_sheets": "Fetch Google Spreadsheets", + "remove_integration": "Remove Integration", + "submit": "Submit" + }, + "fields": { + "select_sheet": "Select Google Sheet", + "selected_sheet": "Selected Sheet: {{name}} (ID: {{id}})" + }, + "errors": { + "auth_error": "Error authenticating with Google", + "fetch_error": "Error fetching spreadsheet files: {{message}}", + "update_error": "Error updating Google Sheet ID: {{message}}", + "remove_error": "Error removing Google Sheets integration: {{message}}" + }, + "notifications": { + "sheet_selected": "Google Sheet selected successfully", + "integration_removed": "Google Sheets integration removed successfully" } }, - "buttons": { - "authenticate": "Authenticate with Google", - "fetch_sheets": "Fetch Google Spreadsheets", - "remove_integration": "Remove Integration", - "submit": "Submit" - }, - "fields": { - "select_sheet": "Select Google Sheet", - "selected_sheet": "Selected Sheet: {{name}} (ID: {{id}})" - }, - "errors": { - "auth_error": "Error authenticating with Google", - "fetch_error": "Error fetching spreadsheet files: {{message}}", - "update_error": "Error updating Google Sheet ID: {{message}}", - "remove_error": "Error removing Google Sheets integration: {{message}}" - }, - "notifications": { - "sheet_selected": "Google Sheet selected successfully" + "airtable": { + "title": "Integrate with Airtable", + "descriptions": { + "sync_info": "If you enable this option, every time this robot runs a task successfully, its captured data will be appended to your Airtable.", + "authenticated_as": "Authenticated with Airtable" + }, + "alerts": { + "success": { + "title": "Connected to Airtable", + "content": "Your robot is now connected to the base: {{baseName}} and table: {{tableName}}. You can access it ", + "here": "here", + "note": "Note:", + "sync_limitation": "Only data captured after integration will be synced to Airtable." + } + }, + "buttons": { + "authenticate": "Connect with Airtable", + "fetch_bases": "Fetch Airtable Bases", + "fetch_tables": "Fetch Airtable Tables", + "remove_integration": "Remove Airtable Integration", + "submit": "Select Base and Table" + }, + "fields": { + "select_base": "Select Airtable Base", + "select_table": "Select Airtable Table", + "selected_base": "Selected Base: {{name}}", + "selected_table": "Selected Table: {{name}}" + }, + "errors": { + "auth_error": "Error authenticating with Airtable", + "fetch_error": "Error fetching Airtable bases: {{message}}", + "fetch_tables_error": "Error fetching Airtable tables: {{message}}", + "update_error": "Error updating Airtable base: {{message}}", + "remove_error": "Error removing Airtable integration: {{message}}" + }, + "notifications": { + "base_selected": "Airtable base selected successfully", + "table_selected": "Airtable table selected successfully", + "integration_removed": "Airtable integration removed successfully" + } } }, "robot_duplication": { diff --git a/public/locales/es.json b/public/locales/es.json index 46bded05..4e6aadd4 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -288,29 +288,81 @@ } }, "integration_settings": { - "title": "Integrar con Google Sheet", + "title": "Configuración de integración", + "title_select_integration": "Seleccionar tipo de integración", "descriptions": { - "sync_info": "Si habilitas esta opción, cada vez que este robot ejecute una tarea exitosamente, sus datos capturados se añadirán a tu Google Sheet.", "authenticated_as": "Autenticado como: {{email}}" }, - "alerts": { - "success": { - "title": "Google Sheet integrado exitosamente.", - "content": "Cada vez que este robot crea una ejecución exitosa, sus datos capturados se añaden a tu Google Sheet {{sheetName}}. Puedes verificar las actualizaciones de datos", - "here": "aquí", - "note": "Nota:", - "sync_limitation": "Los datos extraídos antes de la integración con Google Sheets no se sincronizarán en el Google Sheet. Solo se sincronizarán los datos extraídos después de la integración." + "buttons": { + "submit": "Enviar", + "remove_integration": "Eliminar integración" + }, + "google": { + "title": "Integrar con Google Sheet", + "descriptions": { + "sync_info": "Si habilitas esta opción, cada vez que este robot ejecute una tarea exitosamente, sus datos capturados se añadirán a tu Google Sheet.", + "authenticated_as": "Autenticado como: {{email}}" + }, + "alerts": { + "success": { + "title": "Google Sheet integrado exitosamente.", + "content": "Cada vez que este robot crea una ejecución exitosa, sus datos capturados se añaden a tu Google Sheet {{sheetName}}. Puedes verificar las actualizaciones de datos", + "here": "aquí", + "note": "Nota:", + "sync_limitation": "Los datos extraídos antes de la integración con Google Sheets no se sincronizarán en el Google Sheet. Solo se sincronizarán los datos extraídos después de la integración." + } + }, + "buttons": { + "authenticate": "Autenticar con Google", + "fetch_sheets": "Obtener Google Sheets", + "remove_integration": "Eliminar integración", + "submit": "Enviar" + }, + "fields": { + "select_sheet": "Seleccionar Google Sheet", + "selected_sheet": "Hoja seleccionada: {{name}} (ID: {{id}})" } }, - "buttons": { - "authenticate": "Autenticar con Google", - "fetch_sheets": "Obtener Google Sheets", - "remove_integration": "Eliminar integración", - "submit": "Enviar" - }, - "fields": { - "select_sheet": "Seleccionar Google Sheet", - "selected_sheet": "Hoja seleccionada: {{name}} (ID: {{id}})" + "airtable": { + "title": "Integrar con Airtable", + "descriptions": { + "sync_info": "Conecte su robot a una base de Airtable para sincronizar datos automáticamente.", + "authenticated_as": "Autenticado con Airtable" + }, + "alerts": { + "success": { + "title": "Conectado a Airtable", + "content": "Su robot ahora está conectado a la base: {{baseName}} y tabla: {{tableName}}. Puede acceder a ella ", + "here": "aquí", + "note": "Nota:", + "sync_limitation": "Solo los datos capturados después de la integración se sincronizarán con Airtable." + } + }, + "buttons": { + "authenticate": "Conectar con Airtable", + "fetch_bases": "Obtener bases de Airtable", + "fetch_tables": "Obtener tablas de Airtable", + "remove_integration": "Eliminar integración de Airtable", + "submit": "Seleccionar base y tabla" + }, + "fields": { + "select_base": "Seleccionar base de Airtable", + "select_table": "Seleccionar tabla de Airtable", + "selected_base": "Base seleccionada: {{name}}", + "selected_table": "Tabla seleccionada: {{name}}" + }, + "errors": { + "auth_error": "Error al autenticar con Airtable", + "fetch_error": "Error al obtener bases de Airtable: {{message}}", + "fetch_tables_error": "Error al obtener tablas de Airtable: {{message}}", + "update_error": "Error al actualizar la base de Airtable: {{message}}", + "remove_error": "Error al eliminar la integración de Airtable: {{message}}" + }, + "notifications": { + "base_selected": "Base de Airtable seleccionada con éxito", + "table_selected": "Tabla de Airtable seleccionada con éxito", + "integration_removed": "Integración de Airtable eliminada con éxito" + } } }, "robot_duplication": { diff --git a/public/locales/ja.json b/public/locales/ja.json index ad78c8f5..83f2b731 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -288,29 +288,81 @@ } }, "integration_settings": { - "title": "Google Sheetと連携", + "title": "連携設定", + "title_select_integration": "連携タイプを選択", "descriptions": { - "sync_info": "このオプションを有効にすると、このロボットがタスクを正常に実行するたびに、取得したデータがGoogle Sheetに追加されます。", - "authenticated_as": "認証済みユーザー: {{email}}" - }, - "alerts": { - "success": { - "title": "Google Sheetの連携が完了しました。", - "content": "このロボットが正常に実行を完了するたびに、取得したデータはGoogle Sheet {{sheetName}}に追加されます。データの更新は", - "here": "こちら", - "note": "注意:", - "sync_limitation": "Google Sheetsとの連携前に抽出されたデータは同期されません。連携後に抽出されたデータのみが同期されます。" - } + "authenticated_as": "認証済みユーザー:{{email}}" }, "buttons": { - "authenticate": "Googleで認証", - "fetch_sheets": "Google Sheetsを取得", - "remove_integration": "連携を解除", - "submit": "送信" + "submit": "送信", + "remove_integration": "連携を削除" }, - "fields": { - "select_sheet": "Google Sheetを選択", - "selected_sheet": "選択したシート: {{name}} (ID: {{id}})" + "google": { + "title": "Google Sheetと連携", + "descriptions": { + "sync_info": "このオプションを有効にすると、このロボットがタスクを正常に実行するたびに、取得したデータがGoogle Sheetに追加されます。", + "authenticated_as": "認証済みユーザー: {{email}}" + }, + "alerts": { + "success": { + "title": "Google Sheetの連携が完了しました。", + "content": "このロボットが正常に実行を完了するたびに、取得したデータはGoogle Sheet {{sheetName}}に追加されます。データの更新は", + "here": "こちら", + "note": "注意:", + "sync_limitation": "Google Sheetsとの連携前に抽出されたデータは同期されません。連携後に抽出されたデータのみが同期されます。" + } + }, + "buttons": { + "authenticate": "Googleで認証", + "fetch_sheets": "Google Sheetsを取得", + "remove_integration": "連携を解除", + "submit": "送信" + }, + "fields": { + "select_sheet": "Google Sheetを選択", + "selected_sheet": "選択したシート: {{name}} (ID: {{id}})" + } + }, + "airtable": { + "title": "Airtableと連携する", + "descriptions": { + "sync_info": "ロボットをAirtableベースに接続して、データを自動的に同期します。", + "authenticated_as": "Airtableで認証済み" + }, + "alerts": { + "success": { + "title": "Airtableに接続しました", + "content": "ロボットがベース:{{baseName}}、テーブル:{{tableName}}に接続されました。アクセスするには", + "here": "こちら", + "note": "注意:", + "sync_limitation": "統合後に収集されたデータのみがAirtableに同期されます。" + } + }, + "buttons": { + "authenticate": "Airtableと接続", + "fetch_bases": "Airtableベースを取得", + "fetch_tables": "Airtableテーブルを取得", + "remove_integration": "Airtable連携を削除", + "submit": "ベースとテーブルを選択" + }, + "fields": { + "select_base": "Airtableベースを選択", + "select_table": "Airtableテーブルを選択", + "selected_base": "選択されたベース:{{name}}", + "selected_table": "選択されたテーブル:{{name}}" + }, + "errors": { + "auth_error": "Airtableとの認証エラー", + "fetch_error": "Airtableベースの取得エラー:{{message}}", + "fetch_tables_error": "Airtableテーブルの取得エラー:{{message}}", + "update_error": "Airtableベースの更新エラー:{{message}}", + "remove_error": "Airtable連携の削除エラー:{{message}}" + }, + "notifications": { + "base_selected": "Airtableベースが正常に選択されました", + "table_selected": "Airtableテーブルが正常に選択されました", + "integration_removed": "Airtable連携が正常に削除されました" + } } }, "robot_duplication": { diff --git a/public/locales/zh.json b/public/locales/zh.json index cd1a4f9f..dea02a75 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -288,29 +288,81 @@ } }, "integration_settings": { - "title": "与Google Sheet集成", + "title": "集成设置", + "title_select_integration": "选择集成类型", "descriptions": { - "sync_info": "如果启用此选项,每次机器人成功运行任务时,捕获的数据都会追加到您的Google Sheet中。", - "authenticated_as": "已验证身份: {{email}}" - }, - "alerts": { - "success": { - "title": "Google Sheet集成成功。", - "content": "每次此机器人创建成功运行时,捕获的数据都会追加到您的Google Sheet {{sheetName}}中。您可以查看数据更新", - "here": "在此处", - "note": "注意:", - "sync_limitation": "与Google Sheets集成之前提取的数据将不会同步到Google Sheet中。只有集成后提取的数据才会同步。" - } + "authenticated_as": "已验证身份:{{email}}" }, "buttons": { - "authenticate": "使用Google验证", - "fetch_sheets": "获取Google Sheets", - "remove_integration": "移除集成", - "submit": "提交" + "submit": "提交", + "remove_integration": "移除集成" }, - "fields": { - "select_sheet": "选择Google Sheet", - "selected_sheet": "已选择表格: {{name}} (ID: {{id}})" + "google": { + "title": "与Google Sheet集成", + "descriptions": { + "sync_info": "如果启用此选项,每次机器人成功运行任务时,捕获的数据都会追加到您的Google Sheet中。", + "authenticated_as": "已验证身份: {{email}}" + }, + "alerts": { + "success": { + "title": "Google Sheet集成成功。", + "content": "每次此机器人创建成功运行时,捕获的数据都会追加到您的Google Sheet {{sheetName}}中。您可以查看数据更新", + "here": "在此处", + "note": "注意:", + "sync_limitation": "与Google Sheets集成之前提取的数据将不会同步到Google Sheet中。只有集成后提取的数据才会同步。" + } + }, + "buttons": { + "authenticate": "使用Google验证", + "fetch_sheets": "获取Google Sheets", + "remove_integration": "移除集成", + "submit": "提交" + }, + "fields": { + "select_sheet": "选择Google Sheet", + "selected_sheet": "已选择表格: {{name}} (ID: {{id}})" + } + }, + "airtable": { + "title": "与Airtable集成", + "descriptions": { + "sync_info": "将您的机器人连接到Airtable数据库以自动同步数据。", + "authenticated_as": "已通过Airtable认证" + }, + "alerts": { + "success": { + "title": "已连接到Airtable", + "content": "您的机器人现已连接到数据库:{{baseName}}和表格:{{tableName}}。您可以在", + "here": "这里", + "note": "注意:", + "sync_limitation": "只有集成后捕获的数据才会同步到Airtable。" + } + }, + "buttons": { + "authenticate": "连接Airtable", + "fetch_bases": "获取Airtable数据库", + "fetch_tables": "获取Airtable表格", + "remove_integration": "移除Airtable集成", + "submit": "选择数据库和表格" + }, + "fields": { + "select_base": "选择Airtable数据库", + "select_table": "选择Airtable表格", + "selected_base": "已选数据库:{{name}}", + "selected_table": "已选表格:{{name}}" + }, + "errors": { + "auth_error": "Airtable认证错误", + "fetch_error": "获取Airtable数据库错误:{{message}}", + "fetch_tables_error": "获取Airtable表格错误:{{message}}", + "update_error": "更新Airtable数据库错误:{{message}}", + "remove_error": "移除Airtable集成错误:{{message}}" + }, + "notifications": { + "base_selected": "已成功选择Airtable数据库", + "table_selected": "已成功选择Airtable表格", + "integration_removed": "已成功移除Airtable集成" + } } }, "robot_duplication": { From 905c81f2666636f35af4b442b7d76bc1a3b1a830 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 26 Feb 2025 14:50:52 +0530 Subject: [PATCH 026/197] feat: add require sign in middleware --- server/src/routes/auth.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 5c267663..4a435cd6 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -677,7 +677,7 @@ router.get("/airtable", requireSignIn, (req: Request, res) => { res.redirect(`https://airtable.com/oauth2/v1/authorize?${params}`); }); -router.get("/airtable/callback", async (req: Request, res) => { +router.get("/airtable/callback", requireSignIn, async (req: Request, res) => { const authenticatedReq = req as AuthenticatedRequest; const baseUrl = process.env.PUBLIC_URL || "http://localhost:5173"; @@ -890,7 +890,7 @@ router.post("/airtable/remove", requireSignIn, async (req: Request, res) => { // Fetch tables from an Airtable base -router.get("/airtable/tables", requireSignIn,async (req: Request, res) => { +router.get("/airtable/tables", requireSignIn, async (req: Request, res) => { const authenticatedReq = req as AuthenticatedRequest; try { const { baseId, robotId } = authenticatedReq.query; From 6f92f6aae1e5c67445031a9893cd90c82b65f540 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 26 Feb 2025 14:52:47 +0530 Subject: [PATCH 027/197] feat: rm trailing quotes --- src/components/robot/Recordings.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/robot/Recordings.tsx b/src/components/robot/Recordings.tsx index 62923cf5..7b062323 100644 --- a/src/components/robot/Recordings.tsx +++ b/src/components/robot/Recordings.tsx @@ -65,13 +65,13 @@ export const Recordings = ({ if (authStatus === 'success' && robotId) { notify(authStatus, t("recordingtable.notifications.auth_success")); - handleNavigate(`/robots/${robotId}/integrate`, robotId, "", []);'' + handleNavigate(`/robots/${robotId}/integrate`, robotId, "", []); } if (airtableAuthStatus === 'success' && robotId) { notify(airtableAuthStatus, t("recordingtable.notifications.auth_success")); - handleNavigate(`/robots/${robotId}/integrate`, robotId, "", []);'' + handleNavigate(`/robots/${robotId}/integrate`, robotId, "", []); } }, []); From 10ec273fbdbdf83af3810a98ca38c6dc6b40f542 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 26 Feb 2025 15:01:37 +0530 Subject: [PATCH 028/197] feat: add integration settings translations --- public/locales/de.json | 28 +++++++++++------ public/locales/es.json | 56 ++++++++++++++++++++-------------- public/locales/ja.json | 68 ++++++++++++++++++++++++------------------ public/locales/zh.json | 68 ++++++++++++++++++++++++------------------ 4 files changed, 130 insertions(+), 90 deletions(-) diff --git a/public/locales/de.json b/public/locales/de.json index d86773b8..71ba81a1 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -317,13 +317,13 @@ "google": { "title": "Mit Google Sheet integrieren", "descriptions": { - "sync_info": "Wenn Sie diese Option aktivieren, werden bei jeder erfolgreichen Ausführung dieses Roboters die erfassten Daten in Ihrem Google Sheet ergänzt.", + "sync_info": "Wenn Sie diese Option aktivieren, werden bei jeder erfolgreichen Ausführung einer Aufgabe durch diesen Roboter die erfassten Daten an Ihr Google Sheet angehängt.", "authenticated_as": "Authentifiziert als: {{email}}" }, "alerts": { "success": { "title": "Google Sheet erfolgreich integriert.", - "content": "Jedes Mal, wenn dieser Roboter eine erfolgreiche Ausführung erstellt, werden die erfassten Daten Ihrem Google Sheet {{sheetName}} hinzugefügt. Sie können die Datenaktualisierungen", + "content": "Jedes Mal, wenn dieser Roboter einen erfolgreichen Lauf erstellt, werden seine erfassten Daten an Ihr Google Sheet {{sheetName}} angehängt. Sie können die Datenaktualisierungen überprüfen", "here": "hier", "note": "Hinweis:", "sync_limitation": "Die vor der Integration mit Google Sheets extrahierten Daten werden nicht im Google Sheet synchronisiert. Nur die nach der Integration extrahierten Daten werden synchronisiert." @@ -331,19 +331,29 @@ }, "buttons": { "authenticate": "Mit Google authentifizieren", - "fetch_sheets": "Google Sheets abrufen", + "fetch_sheets": "Google Tabellenkalkulationen abrufen", "remove_integration": "Integration entfernen", "submit": "Absenden" }, "fields": { "select_sheet": "Google Sheet auswählen", "selected_sheet": "Ausgewähltes Sheet: {{name}} (ID: {{id}})" + }, + "errors": { + "auth_error": "Fehler bei der Authentifizierung mit Google", + "fetch_error": "Fehler beim Abrufen der Tabellenkalkulationsdateien: {{message}}", + "update_error": "Fehler beim Aktualisieren der Google Sheet-ID: {{message}}", + "remove_error": "Fehler beim Entfernen der Google Sheets-Integration: {{message}}" + }, + "notifications": { + "sheet_selected": "Google Sheet erfolgreich ausgewählt", + "integration_removed": "Google Sheets-Integration erfolgreich entfernt" } }, "airtable": { - "title": "Mit Airtable verbinden", + "title": "Mit Airtable integrieren", "descriptions": { - "sync_info": "Verbinden Sie Ihren Roboter mit einer Airtable-Basis, um Daten automatisch zu synchronisieren.", + "sync_info": "Wenn Sie diese Option aktivieren, werden bei jeder erfolgreichen Ausführung einer Aufgabe durch diesen Roboter die erfassten Daten an Ihre Airtable angehängt.", "authenticated_as": "Mit Airtable authentifiziert" }, "alerts": { @@ -357,14 +367,14 @@ }, "buttons": { "authenticate": "Mit Airtable verbinden", - "fetch_bases": "Airtable-Basen abrufen", - "fetch_tables": "Airtable-Tabellen abrufen", + "fetch_bases": "Airtable Basen abrufen", + "fetch_tables": "Airtable Tabellen abrufen", "remove_integration": "Airtable-Integration entfernen", "submit": "Basis und Tabelle auswählen" }, "fields": { - "select_base": "Airtable-Basis auswählen", - "select_table": "Airtable-Tabelle auswählen", + "select_base": "Airtable Basis auswählen", + "select_table": "Airtable Tabelle auswählen", "selected_base": "Ausgewählte Basis: {{name}}", "selected_table": "Ausgewählte Tabelle: {{name}}" }, diff --git a/public/locales/es.json b/public/locales/es.json index 79270cd3..8cb4c302 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -306,45 +306,55 @@ } }, "integration_settings": { - "title": "Configuración de integración", - "title_select_integration": "Seleccionar tipo de integración", + "title": "Ajustes de Integración", + "title_select_integration": "Seleccionar Tipo de Integración", "descriptions": { "authenticated_as": "Autenticado como: {{email}}" }, "buttons": { "submit": "Enviar", - "remove_integration": "Eliminar integración" + "remove_integration": "Eliminar Integración" }, "google": { "title": "Integrar con Google Sheet", "descriptions": { - "sync_info": "Si habilitas esta opción, cada vez que este robot ejecute una tarea exitosamente, sus datos capturados se añadirán a tu Google Sheet.", + "sync_info": "Si habilita esta opción, cada vez que este robot ejecute una tarea correctamente, sus datos capturados se añadirán a su Google Sheet.", "authenticated_as": "Autenticado como: {{email}}" }, "alerts": { "success": { - "title": "Google Sheet integrado exitosamente.", - "content": "Cada vez que este robot crea una ejecución exitosa, sus datos capturados se añaden a tu Google Sheet {{sheetName}}. Puedes verificar las actualizaciones de datos", + "title": "Google Sheet Integrado Correctamente.", + "content": "Cada vez que este robot crea una ejecución exitosa, sus datos capturados se añaden a su Google Sheet {{sheetName}}. Puede comprobar las actualizaciones de datos", "here": "aquí", "note": "Nota:", - "sync_limitation": "Los datos extraídos antes de la integración con Google Sheets no se sincronizarán en el Google Sheet. Solo se sincronizarán los datos extraídos después de la integración." + "sync_limitation": "Los datos extraídos antes de la integración con Google Sheets no se sincronizarán en la Google Sheet. Solo los datos extraídos después de la integración se sincronizarán." } }, "buttons": { "authenticate": "Autenticar con Google", - "fetch_sheets": "Obtener Google Sheets", - "remove_integration": "Eliminar integración", + "fetch_sheets": "Obtener Hojas de Cálculo de Google", + "remove_integration": "Eliminar Integración", "submit": "Enviar" }, "fields": { "select_sheet": "Seleccionar Google Sheet", - "selected_sheet": "Hoja seleccionada: {{name}} (ID: {{id}})" + "selected_sheet": "Hoja Seleccionada: {{name}} (ID: {{id}})" + }, + "errors": { + "auth_error": "Error al autenticar con Google", + "fetch_error": "Error al obtener archivos de hojas de cálculo: {{message}}", + "update_error": "Error al actualizar ID de Google Sheet: {{message}}", + "remove_error": "Error al eliminar la integración de Google Sheets: {{message}}" + }, + "notifications": { + "sheet_selected": "Google Sheet seleccionado correctamente", + "integration_removed": "Integración de Google Sheets eliminada correctamente" } }, "airtable": { "title": "Integrar con Airtable", "descriptions": { - "sync_info": "Conecte su robot a una base de Airtable para sincronizar datos automáticamente.", + "sync_info": "Si habilita esta opción, cada vez que este robot ejecute una tarea correctamente, sus datos capturados se añadirán a su Airtable.", "authenticated_as": "Autenticado con Airtable" }, "alerts": { @@ -358,28 +368,28 @@ }, "buttons": { "authenticate": "Conectar con Airtable", - "fetch_bases": "Obtener bases de Airtable", - "fetch_tables": "Obtener tablas de Airtable", - "remove_integration": "Eliminar integración de Airtable", - "submit": "Seleccionar base y tabla" + "fetch_bases": "Obtener Bases de Airtable", + "fetch_tables": "Obtener Tablas de Airtable", + "remove_integration": "Eliminar Integración de Airtable", + "submit": "Seleccionar Base y Tabla" }, "fields": { - "select_base": "Seleccionar base de Airtable", - "select_table": "Seleccionar tabla de Airtable", - "selected_base": "Base seleccionada: {{name}}", - "selected_table": "Tabla seleccionada: {{name}}" + "select_base": "Seleccionar Base de Airtable", + "select_table": "Seleccionar Tabla de Airtable", + "selected_base": "Base Seleccionada: {{name}}", + "selected_table": "Tabla Seleccionada: {{name}}" }, "errors": { "auth_error": "Error al autenticar con Airtable", "fetch_error": "Error al obtener bases de Airtable: {{message}}", "fetch_tables_error": "Error al obtener tablas de Airtable: {{message}}", - "update_error": "Error al actualizar la base de Airtable: {{message}}", + "update_error": "Error al actualizar base de Airtable: {{message}}", "remove_error": "Error al eliminar la integración de Airtable: {{message}}" }, "notifications": { - "base_selected": "Base de Airtable seleccionada con éxito", - "table_selected": "Tabla de Airtable seleccionada con éxito", - "integration_removed": "Integración de Airtable eliminada con éxito" + "base_selected": "Base de Airtable seleccionada correctamente", + "table_selected": "Tabla de Airtable seleccionada correctamente", + "integration_removed": "Integración de Airtable eliminada correctamente" } } }, diff --git a/public/locales/ja.json b/public/locales/ja.json index d96942dd..adbad166 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -309,77 +309,87 @@ "title": "連携設定", "title_select_integration": "連携タイプを選択", "descriptions": { - "authenticated_as": "認証済みユーザー:{{email}}" + "authenticated_as": "認証済み: {{email}}" }, "buttons": { "submit": "送信", - "remove_integration": "連携を削除" + "remove_integration": "連携を解除" }, "google": { - "title": "Google Sheetと連携", + "title": "Google シートと連携", "descriptions": { - "sync_info": "このオプションを有効にすると、このロボットがタスクを正常に実行するたびに、取得したデータがGoogle Sheetに追加されます。", - "authenticated_as": "認証済みユーザー: {{email}}" + "sync_info": "このオプションを有効にすると、このロボットがタスクを正常に実行するたびに、取得したデータがGoogle シートに追加されます。", + "authenticated_as": "認証済み: {{email}}" }, "alerts": { "success": { - "title": "Google Sheetの連携が完了しました。", - "content": "このロボットが正常に実行を完了するたびに、取得したデータはGoogle Sheet {{sheetName}}に追加されます。データの更新は", + "title": "Google シートの連携に成功しました。", + "content": "このロボットが正常に実行されるたびに、取得したデータが{{sheetName}}という名前のGoogle シートに追加されます。データの更新を確認できます", "here": "こちら", - "note": "注意:", - "sync_limitation": "Google Sheetsとの連携前に抽出されたデータは同期されません。連携後に抽出されたデータのみが同期されます。" + "note": "注意:", + "sync_limitation": "Google シートとの連携前に抽出されたデータはGoogle シートに同期されません。連携後に抽出されたデータのみが同期されます。" } }, "buttons": { "authenticate": "Googleで認証", - "fetch_sheets": "Google Sheetsを取得", + "fetch_sheets": "Google スプレッドシートを取得", "remove_integration": "連携を解除", "submit": "送信" }, "fields": { - "select_sheet": "Google Sheetを選択", + "select_sheet": "Google シートを選択", "selected_sheet": "選択したシート: {{name}} (ID: {{id}})" + }, + "errors": { + "auth_error": "Googleでの認証エラー", + "fetch_error": "スプレッドシートファイルの取得エラー: {{message}}", + "update_error": "Google シートIDの更新エラー: {{message}}", + "remove_error": "Google シート連携の解除エラー: {{message}}" + }, + "notifications": { + "sheet_selected": "Google シートが正常に選択されました", + "integration_removed": "Google シートの連携が正常に解除されました" } }, "airtable": { - "title": "Airtableと連携する", + "title": "Airtableと連携", "descriptions": { - "sync_info": "ロボットをAirtableベースに接続して、データを自動的に同期します。", + "sync_info": "このオプションを有効にすると、このロボットがタスクを正常に実行するたびに、取得したデータがAirtableに追加されます。", "authenticated_as": "Airtableで認証済み" }, "alerts": { - "success": { - "title": "Airtableに接続しました", - "content": "ロボットがベース:{{baseName}}、テーブル:{{tableName}}に接続されました。アクセスするには", - "here": "こちら", - "note": "注意:", - "sync_limitation": "統合後に収集されたデータのみがAirtableに同期されます。" - } + "success": { + "title": "Airtableに接続しました", + "content": "ロボットはベース: {{baseName}} とテーブル: {{tableName}} に接続されました。アクセスできます ", + "here": "こちら", + "note": "注意:", + "sync_limitation": "連携後に取得されたデータのみがAirtableに同期されます。" + } }, "buttons": { "authenticate": "Airtableと接続", "fetch_bases": "Airtableベースを取得", "fetch_tables": "Airtableテーブルを取得", - "remove_integration": "Airtable連携を削除", + "remove_integration": "Airtable連携を解除", "submit": "ベースとテーブルを選択" }, "fields": { "select_base": "Airtableベースを選択", "select_table": "Airtableテーブルを選択", - "selected_base": "選択されたベース:{{name}}", - "selected_table": "選択されたテーブル:{{name}}" + "selected_base": "選択したベース: {{name}}", + "selected_table": "選択したテーブル: {{name}}" }, "errors": { - "auth_error": "Airtableとの認証エラー", - "fetch_error": "Airtableベースの取得エラー:{{message}}", - "fetch_tables_error": "Airtableテーブルの取得エラー:{{message}}", - "update_error": "Airtableベースの更新エラー:{{message}}", - "remove_error": "Airtable連携の削除エラー:{{message}}" + "auth_error": "Airtableでの認証エラー", + "fetch_error": "Airtableベースの取得エラー: {{message}}", + "fetch_tables_error": "Airtableテーブルの取得エラー: {{message}}", + "update_error": "Airtableベースの更新エラー: {{message}}", + "remove_error": "Airtable連携の解除エラー: {{message}}" }, "notifications": { "base_selected": "Airtableベースが正常に選択されました", "table_selected": "Airtableテーブルが正常に選択されました", - "integration_removed": "Airtable連携が正常に削除されました" + "integration_removed": "Airtable連携が正常に解除されました" } } }, diff --git a/public/locales/zh.json b/public/locales/zh.json index 4c12871c..e5fd6152 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -309,77 +309,87 @@ "title": "集成设置", "title_select_integration": "选择集成类型", "descriptions": { - "authenticated_as": "已验证身份:{{email}}" + "authenticated_as": "已认证为:{{email}}" }, "buttons": { "submit": "提交", "remove_integration": "移除集成" }, "google": { - "title": "与Google Sheet集成", + "title": "与Google表格集成", "descriptions": { - "sync_info": "如果启用此选项,每次机器人成功运行任务时,捕获的数据都会追加到您的Google Sheet中。", - "authenticated_as": "已验证身份: {{email}}" + "sync_info": "如果启用此选项,每次机器人成功运行任务时,其捕获的数据将附加到您的Google表格中。", + "authenticated_as": "已认证为:{{email}}" }, "alerts": { "success": { - "title": "Google Sheet集成成功。", - "content": "每次此机器人创建成功运行时,捕获的数据都会追加到您的Google Sheet {{sheetName}}中。您可以查看数据更新", - "here": "在此处", + "title": "Google表格集成成功。", + "content": "每次此机器人创建成功运行时,其捕获的数据将附加到您的{{sheetName}} Google表格中。您可以检查数据更新", + "here": "在这里", "note": "注意:", - "sync_limitation": "与Google Sheets集成之前提取的数据将不会同步到Google Sheet中。只有集成后提取的数据才会同步。" + "sync_limitation": "在与Google表格集成之前提取的数据将不会在Google表格中同步。只有集成后提取的数据才会同步。" } }, "buttons": { - "authenticate": "使用Google验证", - "fetch_sheets": "获取Google Sheets", + "authenticate": "使用Google认证", + "fetch_sheets": "获取Google电子表格", "remove_integration": "移除集成", "submit": "提交" }, "fields": { - "select_sheet": "选择Google Sheet", - "selected_sheet": "已选择表格: {{name}} (ID: {{id}})" + "select_sheet": "选择Google表格", + "selected_sheet": "已选择表格:{{name}}(ID:{{id}})" + }, + "errors": { + "auth_error": "使用Google认证时出错", + "fetch_error": "获取电子表格文件时出错:{{message}}", + "update_error": "更新Google表格ID时出错:{{message}}", + "remove_error": "移除Google表格集成时出错:{{message}}" + }, + "notifications": { + "sheet_selected": "Google表格选择成功", + "integration_removed": "Google表格集成已成功移除" } }, "airtable": { "title": "与Airtable集成", "descriptions": { - "sync_info": "将您的机器人连接到Airtable数据库以自动同步数据。", - "authenticated_as": "已通过Airtable认证" + "sync_info": "如果启用此选项,每次机器人成功运行任务时,其捕获的数据将附加到您的Airtable中。", + "authenticated_as": "已使用Airtable认证" }, "alerts": { "success": { "title": "已连接到Airtable", - "content": "您的机器人现已连接到数据库:{{baseName}}和表格:{{tableName}}。您可以在", - "here": "这里", + "content": "您的机器人现已连接到基础:{{baseName}}和表格:{{tableName}}。您可以访问它", + "here": "在这里", "note": "注意:", "sync_limitation": "只有集成后捕获的数据才会同步到Airtable。" } }, "buttons": { "authenticate": "连接Airtable", - "fetch_bases": "获取Airtable数据库", + "fetch_bases": "获取Airtable基础", "fetch_tables": "获取Airtable表格", "remove_integration": "移除Airtable集成", - "submit": "选择数据库和表格" + "submit": "选择基础和表格" }, "fields": { - "select_base": "选择Airtable数据库", + "select_base": "选择Airtable基础", "select_table": "选择Airtable表格", - "selected_base": "已选数据库:{{name}}", - "selected_table": "已选表格:{{name}}" + "selected_base": "已选择基础:{{name}}", + "selected_table": "已选择表格:{{name}}" }, "errors": { - "auth_error": "Airtable认证错误", - "fetch_error": "获取Airtable数据库错误:{{message}}", - "fetch_tables_error": "获取Airtable表格错误:{{message}}", - "update_error": "更新Airtable数据库错误:{{message}}", - "remove_error": "移除Airtable集成错误:{{message}}" + "auth_error": "使用Airtable认证时出错", + "fetch_error": "获取Airtable基础时出错:{{message}}", + "fetch_tables_error": "获取Airtable表格时出错:{{message}}", + "update_error": "更新Airtable基础时出错:{{message}}", + "remove_error": "移除Airtable集成时出错:{{message}}" }, "notifications": { - "base_selected": "已成功选择Airtable数据库", - "table_selected": "已成功选择Airtable表格", - "integration_removed": "已成功移除Airtable集成" + "base_selected": "Airtable基础选择成功", + "table_selected": "Airtable表格选择成功", + "integration_removed": "Airtable集成已成功移除" } } }, From 8a74c8824470a0eaa7eca8ff8acee2b9db0b84ca Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 26 Feb 2025 22:28:03 +0530 Subject: [PATCH 029/197] feat: rm infer type and regex logic --- .../integrations/airtable.ts | 48 ++++--------------- 1 file changed, 9 insertions(+), 39 deletions(-) diff --git a/server/src/workflow-management/integrations/airtable.ts b/server/src/workflow-management/integrations/airtable.ts index 667fef6f..5b09c608 100644 --- a/server/src/workflow-management/integrations/airtable.ts +++ b/server/src/workflow-management/integrations/airtable.ts @@ -130,9 +130,14 @@ export async function writeDataToAirtable( const airtable = new Airtable({ apiKey: accessToken }); const base = airtable.base(baseId); - const existingFields = await getExistingFields(base, tableName); - const dataFields = [...new Set(data.flatMap(row => Object.keys(row)))]; + const existingFields = await getExistingFields(base, tableName); + console.log(`Found ${existingFields.length} existing fields in Airtable`); + + const dataFields = [...new Set(data.flatMap(row => Object.keys(row)))]; + console.log(`Found ${dataFields.length} fields in data: ${dataFields.join(', ')}`); + const missingFields = dataFields.filter(field => !existingFields.includes(field)); + console.log(`Found ${missingFields.length} missing fields: ${missingFields.join(', ')}`); for (const field of missingFields) { const sampleRow = data.find(row => field in row); @@ -237,19 +242,13 @@ async function createAirtableField( tableId: string, retries = MAX_RETRIES ): Promise { - try { - const sanitizedFieldName = sanitizeFieldName(fieldName); - const fieldType = inferFieldType(sampleValue); - - console.log(`Creating field ${sanitizedFieldName} with type ${fieldType}`); - + try { const response = await axios.post( `https://api.airtable.com/v0/meta/bases/${baseId}/tables/${tableId}/fields`, - { name: sanitizedFieldName, type: fieldType }, + { name: fieldName }, { headers: { Authorization: `Bearer ${accessToken}` } } ); - logger.log('info', `Created field: ${sanitizedFieldName} (${fieldType})`); return response.data; } catch (error: any) { if (retries > 0 && error.response?.status === 429) { @@ -268,35 +267,6 @@ async function createAirtableField( } } -function sanitizeFieldName(fieldName: string): string { - return fieldName - .trim() - .replace(/^[^a-zA-Z]+/, '') - .replace(/[^\w\s]/gi, ' ') - .substring(0, 50); -} - -function inferFieldType(value: any): string { - if (value === null || value === undefined) return 'singleLineText'; - if (typeof value === 'number') return 'number'; - if (typeof value === 'boolean') return 'checkbox'; - if (value instanceof Date) return 'dateTime'; - if (Array.isArray(value)) { - return value.length > 0 && typeof value[0] === 'object' ? 'multipleRecordLinks' : 'multipleSelects'; - } - if (typeof value === 'string' && isValidUrl(value)) return 'url'; - return 'singleLineText'; -} - -function isValidUrl(str: string): boolean { - try { - new URL(str); - return true; - } catch (_) { - return false; - } -} - export const processAirtableUpdates = async () => { while (true) { let hasPendingTasks = false; From 088fc1a109267136ebd99d6bae6728d8d47ccb5a Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 26 Feb 2025 23:52:28 +0530 Subject: [PATCH 030/197] feat: add infer field types --- .../integrations/airtable.ts | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/server/src/workflow-management/integrations/airtable.ts b/server/src/workflow-management/integrations/airtable.ts index 5b09c608..93474721 100644 --- a/server/src/workflow-management/integrations/airtable.ts +++ b/server/src/workflow-management/integrations/airtable.ts @@ -242,13 +242,18 @@ async function createAirtableField( tableId: string, retries = MAX_RETRIES ): Promise { - try { + try { + const fieldType = inferFieldType(sampleValue); + + console.log(`Creating field ${fieldName} with type ${fieldType}`); + const response = await axios.post( `https://api.airtable.com/v0/meta/bases/${baseId}/tables/${tableId}/fields`, - { name: fieldName }, + { name: fieldName, type: fieldType }, { headers: { Authorization: `Bearer ${accessToken}` } } ); + logger.log('info', `Created field: ${fieldName} (${fieldType})`); return response.data; } catch (error: any) { if (retries > 0 && error.response?.status === 429) { @@ -267,6 +272,27 @@ async function createAirtableField( } } +function inferFieldType(value: any): string { + if (value === null || value === undefined) return 'singleLineText'; + if (typeof value === 'number') return 'number'; + if (typeof value === 'boolean') return 'checkbox'; + if (value instanceof Date) return 'dateTime'; + if (Array.isArray(value)) { + return value.length > 0 && typeof value[0] === 'object' ? 'multipleRecordLinks' : 'multipleSelects'; + } + if (typeof value === 'string' && isValidUrl(value)) return 'url'; + return 'singleLineText'; +} + +function isValidUrl(str: string): boolean { + try { + new URL(str); + return true; + } catch (_) { + return false; + } +} + export const processAirtableUpdates = async () => { while (true) { let hasPendingTasks = false; From e6d1476383210b408dabbb9c6ac3c9cf437721d6 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 26 Feb 2025 23:52:58 +0530 Subject: [PATCH 031/197] feat: navigate based on auth status --- src/components/robot/Recordings.tsx | 33 ++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/components/robot/Recordings.tsx b/src/components/robot/Recordings.tsx index 7b062323..780d01cf 100644 --- a/src/components/robot/Recordings.tsx +++ b/src/components/robot/Recordings.tsx @@ -62,16 +62,15 @@ export const Recordings = ({ const airtableAuthStatus = getAndClearCookie('airtable_auth_status'); const robotId = getAndClearCookie('robot_auth_robotId'); - if (authStatus === 'success' && robotId) { - notify(authStatus, t("recordingtable.notifications.auth_success")); - - handleNavigate(`/robots/${robotId}/integrate`, robotId, "", []); - } - if (airtableAuthStatus === 'success' && robotId) { + console.log("Airtable Auth Status:", airtableAuthStatus); notify(airtableAuthStatus, t("recordingtable.notifications.auth_success")); - - handleNavigate(`/robots/${robotId}/integrate`, robotId, "", []); + handleNavigate(`/robots/${robotId}/integrate/airtable`, robotId, "", []); + } + else if (authStatus === 'success' && robotId) { + console.log("Google Auth Status:", authStatus); + notify(authStatus, t("recordingtable.notifications.auth_success")); + handleNavigate(`/robots/${robotId}/integrate/google`, robotId, "", []); } }, []); @@ -97,6 +96,24 @@ export const Recordings = ({ handleStart={handleScheduleRecording} /> ); + } else if (currentPath.endsWith("/integrate/google")) { + return ( + {}} + preSelectedIntegrationType="googleSheets" + /> + ); + } else if (currentPath.endsWith("/integrate/airtable")) { + return ( + {}} + preSelectedIntegrationType="airtable" + /> + ); } else if (currentPath.endsWith("/integrate")) { return ( Date: Wed, 26 Feb 2025 23:53:51 +0530 Subject: [PATCH 032/197] feat: rt rendering --- .../integration/IntegrationSettings.tsx | 234 +++++++++++------- 1 file changed, 138 insertions(+), 96 deletions(-) diff --git a/src/components/integration/IntegrationSettings.tsx b/src/components/integration/IntegrationSettings.tsx index 37e04a66..30e14f98 100644 --- a/src/components/integration/IntegrationSettings.tsx +++ b/src/components/integration/IntegrationSettings.tsx @@ -17,11 +17,13 @@ import { apiUrl } from "../../apiConfig.js"; import Cookies from "js-cookie"; import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; interface IntegrationProps { isOpen: boolean; handleStart: (data: IntegrationSettings) => void; handleClose: () => void; + preSelectedIntegrationType?: "googleSheets" | "airtable" | null; } export interface IntegrationSettings { @@ -33,10 +35,8 @@ export interface IntegrationSettings { airtableTableId?: string, data: string; integrationType: "googleSheets" | "airtable"; - } - const getCookie = (name: string): string | null => { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); @@ -50,11 +50,11 @@ const removeCookie = (name: string): void => { document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`; }; - export const IntegrationSettingsModal = ({ isOpen, handleStart, handleClose, + preSelectedIntegrationType = null, }: IntegrationProps) => { const { t } = useTranslation(); const [settings, setSettings] = useState({ @@ -64,10 +64,8 @@ export const IntegrationSettingsModal = ({ airtableBaseName: "", airtableTableName: "", airtableTableId: "", - data: "", - integrationType: "googleSheets", - + integrationType: preSelectedIntegrationType || "googleSheets", }); const [spreadsheets, setSpreadsheets] = useState<{ id: string; name: string }[]>([]); @@ -76,12 +74,19 @@ export const IntegrationSettingsModal = ({ const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const { recordingId, notify } = useGlobalInfoStore(); + const { + recordingId, + notify, + setRerenderRobots + } = useGlobalInfoStore(); + const [recording, setRecording] = useState(null); + const navigate = useNavigate(); - const [airtableAuthStatus, setAirtableAuthStatus] = useState(null); + const [selectedIntegrationType, setSelectedIntegrationType] = useState< + "googleSheets" | "airtable" | null + >(preSelectedIntegrationType); - // Authenticate with Google Sheets const authenticateWithGoogle = () => { window.location.href = `${apiUrl}/auth/google?robotId=${recordingId}`; }; @@ -100,6 +105,7 @@ export const IntegrationSettingsModal = ({ ); setSpreadsheets(response.data); } catch (error: any) { + setLoading(false); console.error("Error fetching spreadsheet files:", error); notify("error", t("integration_settings.google.errors.fetch_error", { message: error.response?.data?.message || error.message, @@ -116,6 +122,7 @@ export const IntegrationSettingsModal = ({ ); setAirtableBases(response.data); } catch (error: any) { + setLoading(false); console.error("Error fetching Airtable bases:", error); notify("error", t("integration_settings.airtable.errors.fetch_error", { message: error.response?.data?.message || error.message, @@ -123,7 +130,6 @@ export const IntegrationSettingsModal = ({ } }; - const fetchAirtableTables = async (baseId: string, recordingId: string) => { try { const response = await axios.get( @@ -133,6 +139,7 @@ export const IntegrationSettingsModal = ({ setAirtableTables(response.data); } catch (error: any) { + setLoading(false); console.error("Error fetching Airtable tables:", error); notify("error", t("integration_settings.airtable.errors.fetch_tables_error", { message: error.response?.data?.message || error.message, @@ -155,17 +162,14 @@ export const IntegrationSettingsModal = ({ // Handle Airtable base selection const handleAirtableBaseSelect = async (e: React.ChangeEvent) => { const selectedBase = airtableBases.find((base) => base.id === e.target.value); - console.log(selectedBase); if (selectedBase) { - // Update local state setSettings((prevSettings) => ({ ...prevSettings, airtableBaseId: selectedBase.id, airtableBaseName: selectedBase.name, })); - // Fetch tables for the selected base if (recordingId) { await fetchAirtableTables(selectedBase.id, recordingId); } else { @@ -175,7 +179,6 @@ export const IntegrationSettingsModal = ({ }; const handleAirtabletableSelect = (e: React.ChangeEvent) => { - console.log( e.target.value); const selectedTable = airtableTables.find((table) => table.id === e.target.value); if (selectedTable) { setSettings((prevSettings) => ({ @@ -186,11 +189,17 @@ export const IntegrationSettingsModal = ({ } }; + const refreshRecordingData = async () => { + if (!recordingId) return null; + const updatedRecording = await getStoredRecording(recordingId); + setRecording(updatedRecording); + setRerenderRobots(true); + return updatedRecording; + }; - - // Update Google Sheets integration const updateGoogleSheetId = async () => { try { + setLoading(true); await axios.post( `${apiUrl}/auth/gsheets/update`, { @@ -200,8 +209,14 @@ export const IntegrationSettingsModal = ({ }, { withCredentials: true } ); + + // Refresh recording data immediately + await refreshRecordingData(); + notify("success", t("integration_settings.google.notifications.sheet_selected")); + setLoading(false); } catch (error: any) { + setLoading(false); console.error("Error updating Google Sheet ID:", error); notify("error", t("integration_settings.google.errors.update_error", { message: error.response?.data?.message || error.message, @@ -212,6 +227,7 @@ export const IntegrationSettingsModal = ({ // Update Airtable integration const updateAirtableBase = async () => { try { + setLoading(true); await axios.post( `${apiUrl}/auth/airtable/update`, { @@ -223,8 +239,14 @@ export const IntegrationSettingsModal = ({ }, { withCredentials: true } ); + + // Refresh recording data immediately + await refreshRecordingData(); + notify("success", t("integration_settings.airtable.notifications.base_selected")); + setLoading(false); } catch (error: any) { + setLoading(false); console.error("Error updating Airtable base:", error); notify("error", t("integration_settings.airtable.errors.update_error", { message: error.response?.data?.message || error.message, @@ -235,15 +257,24 @@ export const IntegrationSettingsModal = ({ // Remove Google Sheets integration const removeGoogleSheetsIntegration = async () => { try { + setLoading(true); await axios.post( `${apiUrl}/auth/gsheets/remove`, { robotId: recordingId }, { withCredentials: true } ); + + // Clear UI state setSpreadsheets([]); setSettings({ ...settings, spreadsheetId: "", spreadsheetName: "" }); + + // Refresh recording data + await refreshRecordingData(); + notify("success", t("integration_settings.google.notifications.integration_removed")); + setLoading(false); } catch (error: any) { + setLoading(false); console.error("Error removing Google Sheets integration:", error); notify("error", t("integration_settings.google.errors.remove_error", { message: error.response?.data?.message || error.message, @@ -251,19 +282,28 @@ export const IntegrationSettingsModal = ({ } }; - // Remove Airtable integration const removeAirtableIntegration = async () => { try { + setLoading(true); await axios.post( `${apiUrl}/auth/airtable/remove`, { robotId: recordingId }, { withCredentials: true } ); + + // Clear UI state setAirtableBases([]); - setSettings({ ...settings, airtableBaseId: "", airtableBaseName: "", airtableTableName:"" }); + setAirtableTables([]); + setSettings({ ...settings, airtableBaseId: "", airtableBaseName: "", airtableTableName:"", airtableTableId: "" }); + + // Refresh recording data + await refreshRecordingData(); + notify("success", t("integration_settings.airtable.notifications.integration_removed")); + setLoading(false); } catch (error: any) { + setLoading(false); console.error("Error removing Airtable integration:", error); notify("error", t("integration_settings.airtable.errors.remove_error", { message: error.response?.data?.message || error.message, @@ -276,48 +316,57 @@ export const IntegrationSettingsModal = ({ try { const response = await axios.get(`${apiUrl}/auth/airtable/callback`); if (response.data.success) { - setAirtableAuthStatus(true); - fetchAirtableBases(); // Fetch bases after successful authentication + await refreshRecordingData(); } } catch (error) { setError(t("integration_settings.airtable.errors.auth_error")); } }; - // Fetch recording info on component mount + // Fetch recording info on component mount and when recordingId changes useEffect(() => { const fetchRecordingInfo = async () => { if (!recordingId) return; + + console.log("Fetching recording info for ID:", recordingId); + setLoading(true); + const recording = await getStoredRecording(recordingId); if (recording) { + console.log("Recording fetched:", recording); setRecording(recording); + + // Update settings based on existing integrations if (recording.google_sheet_id) { - setSettings({ ...settings, integrationType: "googleSheets" }); + setSettings(prev => ({ ...prev, integrationType: "googleSheets" })); } else if (recording.airtable_base_id) { setSettings(prev => ({ ...prev, airtableBaseId: recording.airtable_base_id || "", airtableBaseName: recording.airtable_base_name || "", airtableTableName: recording.airtable_table_name || "", + airtableTableId: recording.airtable_table_id || "", integrationType: recording.airtable_base_id ? "airtable" : "googleSheets" })); } } + + setLoading(false); }; + fetchRecordingInfo(); - }, [recordingId]); + }, [recordingId, preSelectedIntegrationType]); - // Handle Airtable authentication status + // Handle Airtable authentication cookies useEffect(() => { const status = getCookie("airtable_auth_status"); const message = getCookie("airtable_auth_message"); - if (status === "success" && message) { - notify("success", message); + if (status === "success") { + notify("success", message || t("integration_settings.airtable.notifications.auth_success")); removeCookie("airtable_auth_status"); removeCookie("airtable_auth_message"); - setAirtableAuthStatus(true); - fetchAirtableBases(); // Fetch bases after successful authentication + refreshRecordingData(); } const urlParams = new URLSearchParams(window.location.search); @@ -325,64 +374,57 @@ export const IntegrationSettingsModal = ({ if (code) { handleAirtableOAuthCallback(); } - }, [recordingId]); + }, []); - console.log(recording) + // Add this UI at the top of the modal return statement + if (!selectedIntegrationType) { + return ( + +
+ + {t("integration_settings.title_select_integration")} + +
+ {/* Google Sheets Button */} + - - const [selectedIntegrationType, setSelectedIntegrationType] = useState< - "googleSheets" | "airtable" | null ->(null); - -// Add this UI at the top of the modal return statement -if (!selectedIntegrationType) { - return ( - -
- - {t("integration_settings.title_select_integration")} - -
- {/* Google Sheets Button */} - - - {/* Airtable Button */} - + {/* Airtable Button */} + +
-
- - ); -} - + + ); + } - return (
- {t("integration_settings.google.buttons.remove_integration")} + {loading ? : t("integration_settings.google.buttons.remove_integration")} ) : ( @@ -432,8 +475,9 @@ if (!selectedIntegrationType) { variant="contained" color="primary" onClick={authenticateWithGoogle} + disabled={loading} > - {t("integration_settings.google.buttons.authenticate")} + {loading ? : t("integration_settings.google.buttons.authenticate")} ) : ( @@ -452,6 +496,7 @@ if (!selectedIntegrationType) { variant="outlined" color="primary" onClick={fetchSpreadsheetFiles} + disabled={loading} > {t("integration_settings.google.buttons.fetch_sheets")} @@ -475,14 +520,11 @@ if (!selectedIntegrationType) { )} @@ -521,8 +563,9 @@ if (!selectedIntegrationType) { color="error" onClick={removeAirtableIntegration} style={{ marginTop: "15px" }} + disabled={loading} > - {t("integration_settings.airtable.buttons.remove_integration")} + {loading ? : t("integration_settings.airtable.buttons.remove_integration")} ) : ( @@ -534,8 +577,9 @@ if (!selectedIntegrationType) { variant="contained" color="primary" onClick={authenticateWithAirtable} + disabled={loading} > - {t("integration_settings.airtable.buttons.authenticate")} + {loading ? : t("integration_settings.airtable.buttons.authenticate")} ) : ( @@ -552,6 +596,7 @@ if (!selectedIntegrationType) { variant="outlined" color="primary" onClick={fetchAirtableBases} + disabled={loading} > {t("integration_settings.airtable.buttons.fetch_bases")} @@ -590,14 +635,11 @@ if (!selectedIntegrationType) { )} From c3b89ba126dd939596a9885bcb439952b1a76fa4 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 26 Feb 2025 23:57:13 +0530 Subject: [PATCH 033/197] feat: cleanup --- src/components/integration/IntegrationSettings.tsx | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/components/integration/IntegrationSettings.tsx b/src/components/integration/IntegrationSettings.tsx index 30e14f98..18bddfec 100644 --- a/src/components/integration/IntegrationSettings.tsx +++ b/src/components/integration/IntegrationSettings.tsx @@ -240,7 +240,6 @@ export const IntegrationSettingsModal = ({ { withCredentials: true } ); - // Refresh recording data immediately await refreshRecordingData(); notify("success", t("integration_settings.airtable.notifications.base_selected")); @@ -292,12 +291,10 @@ export const IntegrationSettingsModal = ({ { withCredentials: true } ); - // Clear UI state setAirtableBases([]); setAirtableTables([]); setSettings({ ...settings, airtableBaseId: "", airtableBaseName: "", airtableTableName:"", airtableTableId: "" }); - // Refresh recording data await refreshRecordingData(); notify("success", t("integration_settings.airtable.notifications.integration_removed")); @@ -311,7 +308,6 @@ export const IntegrationSettingsModal = ({ } }; - // Handle OAuth callback for Airtable const handleAirtableOAuthCallback = async () => { try { const response = await axios.get(`${apiUrl}/auth/airtable/callback`); @@ -323,20 +319,16 @@ export const IntegrationSettingsModal = ({ } }; - // Fetch recording info on component mount and when recordingId changes useEffect(() => { const fetchRecordingInfo = async () => { if (!recordingId) return; - console.log("Fetching recording info for ID:", recordingId); setLoading(true); const recording = await getStoredRecording(recordingId); if (recording) { - console.log("Recording fetched:", recording); setRecording(recording); - // Update settings based on existing integrations if (recording.google_sheet_id) { setSettings(prev => ({ ...prev, integrationType: "googleSheets" })); } else if (recording.airtable_base_id) { @@ -357,7 +349,6 @@ export const IntegrationSettingsModal = ({ fetchRecordingInfo(); }, [recordingId, preSelectedIntegrationType]); - // Handle Airtable authentication cookies useEffect(() => { const status = getCookie("airtable_auth_status"); const message = getCookie("airtable_auth_message"); @@ -394,7 +385,6 @@ export const IntegrationSettingsModal = ({ {t("integration_settings.title_select_integration")}
- {/* Google Sheets Button */} @@ -401,11 +402,12 @@ export const IntegrationSettingsModal = ({ variant="contained" color="primary" onClick={() => { - setSelectedIntegrationType("airtable"); - setSettings({ ...settings, integrationType: "airtable" }); - navigate(`/robots/${recordingId}/integrate/airtable`); + setSelectedIntegrationType("airtable"); + setSettings({ ...settings, integrationType: "airtable" }); + navigate(`/robots/${recordingId}/integrate/airtable`); }} > + Airtable Airtable
From fcd07a328e6872033816fba90619ccc31a91df88 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 27 Feb 2025 01:50:42 +0530 Subject: [PATCH 049/197] fix: broken gsheet svg path --- src/components/integration/IntegrationSettings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/integration/IntegrationSettings.tsx b/src/components/integration/IntegrationSettings.tsx index 8c4318b4..0140b97d 100644 --- a/src/components/integration/IntegrationSettings.tsx +++ b/src/components/integration/IntegrationSettings.tsx @@ -394,7 +394,7 @@ export const IntegrationSettingsModal = ({ navigate(`/robots/${recordingId}/integrate/google`); }} > - Google Sheets + Google Sheets Google Sheets From 38df6991f61c60cdb44ba9d2b8d5b3b0471033a2 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 27 Feb 2025 01:52:33 +0530 Subject: [PATCH 050/197] feat: align icons with text --- src/components/integration/IntegrationSettings.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/integration/IntegrationSettings.tsx b/src/components/integration/IntegrationSettings.tsx index 0140b97d..984a5275 100644 --- a/src/components/integration/IntegrationSettings.tsx +++ b/src/components/integration/IntegrationSettings.tsx @@ -393,8 +393,9 @@ export const IntegrationSettingsModal = ({ setSettings({ ...settings, integrationType: "googleSheets" }); navigate(`/robots/${recordingId}/integrate/google`); }} + style={{ display: "flex", flexDirection: "column", alignItems: "center" }} > - Google Sheets + Google Sheets Google Sheets @@ -406,8 +407,9 @@ export const IntegrationSettingsModal = ({ setSettings({ ...settings, integrationType: "airtable" }); navigate(`/robots/${recordingId}/integrate/airtable`); }} + style={{ display: "flex", flexDirection: "column", alignItems: "center" }} > - Airtable + Airtable Airtable
From 4fb919ec9e215c4db84896661ccca66bae3a5c5d Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 27 Feb 2025 01:52:47 +0530 Subject: [PATCH 051/197] chore: lint --- src/components/integration/IntegrationSettings.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/integration/IntegrationSettings.tsx b/src/components/integration/IntegrationSettings.tsx index 984a5275..86480235 100644 --- a/src/components/integration/IntegrationSettings.tsx +++ b/src/components/integration/IntegrationSettings.tsx @@ -389,9 +389,9 @@ export const IntegrationSettingsModal = ({ variant="contained" color="primary" onClick={() => { - setSelectedIntegrationType("googleSheets"); - setSettings({ ...settings, integrationType: "googleSheets" }); - navigate(`/robots/${recordingId}/integrate/google`); + setSelectedIntegrationType("googleSheets"); + setSettings({ ...settings, integrationType: "googleSheets" }); + navigate(`/robots/${recordingId}/integrate/google`); }} style={{ display: "flex", flexDirection: "column", alignItems: "center" }} > @@ -403,9 +403,9 @@ export const IntegrationSettingsModal = ({ variant="contained" color="primary" onClick={() => { - setSelectedIntegrationType("airtable"); - setSettings({ ...settings, integrationType: "airtable" }); - navigate(`/robots/${recordingId}/integrate/airtable`); + setSelectedIntegrationType("airtable"); + setSettings({ ...settings, integrationType: "airtable" }); + navigate(`/robots/${recordingId}/integrate/airtable`); }} style={{ display: "flex", flexDirection: "column", alignItems: "center" }} > From 56d5b102a2f4359bf1088273ecdb92ac82df77b9 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 27 Feb 2025 01:55:57 +0530 Subject: [PATCH 052/197] feat: set outlined variant --- src/components/integration/IntegrationSettings.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/integration/IntegrationSettings.tsx b/src/components/integration/IntegrationSettings.tsx index 86480235..695677a4 100644 --- a/src/components/integration/IntegrationSettings.tsx +++ b/src/components/integration/IntegrationSettings.tsx @@ -386,7 +386,7 @@ export const IntegrationSettingsModal = ({ }}>
@@ -409,7 +409,7 @@ export const IntegrationSettingsModal = ({ }} style={{ display: "flex", flexDirection: "column", alignItems: "center" }} > - Airtable + Airtable Airtable
From 38b61cefc55b324f925b5251ab37af3b8032e1eb Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 27 Feb 2025 01:56:58 +0530 Subject: [PATCH 054/197] chore: lint --- src/components/integration/IntegrationSettings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/integration/IntegrationSettings.tsx b/src/components/integration/IntegrationSettings.tsx index 84ce15ff..aed8dadb 100644 --- a/src/components/integration/IntegrationSettings.tsx +++ b/src/components/integration/IntegrationSettings.tsx @@ -395,7 +395,7 @@ export const IntegrationSettingsModal = ({ }} style={{ display: "flex", flexDirection: "column", alignItems: "center" }} > - Google Sheets + Google Sheets Google Sheets From 4ae2bb53ee29fe07a8fddecc865429bf2c365a4f Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 27 Feb 2025 01:59:39 +0530 Subject: [PATCH 055/197] feat: consistent style in all modes --- src/components/integration/IntegrationSettings.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/integration/IntegrationSettings.tsx b/src/components/integration/IntegrationSettings.tsx index aed8dadb..846dd3a3 100644 --- a/src/components/integration/IntegrationSettings.tsx +++ b/src/components/integration/IntegrationSettings.tsx @@ -387,13 +387,13 @@ export const IntegrationSettingsModal = ({
@@ -407,7 +407,7 @@ export const IntegrationSettingsModal = ({ }} style={{ display: "flex", flexDirection: "column", alignItems: "center", background: 'white', color: '#ff00c3' }} > - Airtable + Airtable Airtable
From c548050e47a19eaf5786da8f56f81d23ef008832 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 27 Feb 2025 03:46:07 +0530 Subject: [PATCH 058/197] fix: remove docker-compose.yml from .gitignore --- .gitignore | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index a2a80766..6b2b3f98 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,4 @@ /build -package-lock.json - -docker-compose.yml \ No newline at end of file +package-lock.json \ No newline at end of file From a62320614aa5cae865f13f3c3c6ac1fa99fb6509 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 27 Feb 2025 03:46:31 +0530 Subject: [PATCH 059/197] chore: remove airtable migrations --- ...125120216-add-airtable-columns-to-robot.js | 16 -------------- ...0125182726-update-airtable-token-length.js | 22 ------------------- .../20250207133740-added_table_id.js | 16 -------------- 3 files changed, 54 deletions(-) delete mode 100644 server/migrations/20250125120216-add-airtable-columns-to-robot.js delete mode 100644 server/migrations/20250125182726-update-airtable-token-length.js delete mode 100644 server/migrations/20250207133740-added_table_id.js diff --git a/server/migrations/20250125120216-add-airtable-columns-to-robot.js b/server/migrations/20250125120216-add-airtable-columns-to-robot.js deleted file mode 100644 index ac2c852b..00000000 --- a/server/migrations/20250125120216-add-airtable-columns-to-robot.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; - -module.exports = { - up: async (queryInterface, Sequelize) => { - - await queryInterface.addColumn('robot', 'airtable_refresh_token', { - type: Sequelize.STRING, - allowNull: true, - }); - }, - - down: async (queryInterface, Sequelize) => { - - await queryInterface.removeColumn('robot', 'airtable_refresh_token'); - }, -}; \ No newline at end of file diff --git a/server/migrations/20250125182726-update-airtable-token-length.js b/server/migrations/20250125182726-update-airtable-token-length.js deleted file mode 100644 index b75d788c..00000000 --- a/server/migrations/20250125182726-update-airtable-token-length.js +++ /dev/null @@ -1,22 +0,0 @@ -// migrations/XXXXXXXXXXXXXX-update-airtable-token-length.js -module.exports = { - up: async (queryInterface, Sequelize) => { - await queryInterface.changeColumn('robot', 'airtable_access_token', { - type: Sequelize.TEXT - }); - - await queryInterface.changeColumn('robot', 'airtable_refresh_token', { - type: Sequelize.TEXT - }); - }, - - down: async (queryInterface, Sequelize) => { - await queryInterface.changeColumn('robot', 'airtable_access_token', { - type: Sequelize.STRING(255) - }); - - await queryInterface.changeColumn('robot', 'airtable_refresh_token', { - type: Sequelize.STRING(255) - }); - } -}; \ No newline at end of file diff --git a/server/migrations/20250207133740-added_table_id.js b/server/migrations/20250207133740-added_table_id.js deleted file mode 100644 index 0a40d0c7..00000000 --- a/server/migrations/20250207133740-added_table_id.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; - -/** @type {import('sequelize-cli').Migration} */ -module.exports = { - async up (queryInterface, Sequelize) { - await queryInterface.addColumn('robot', 'airtable_table_id', { - type: Sequelize.TEXT - }); - }, - - async down (queryInterface, Sequelize) { - await queryInterface.removeColumn('robot', 'airtable_table_id', { - type: Sequelize.TEXT - }); - } -}; From 6fa82c92c57a161bc912a22285ede28db2f01ed6 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 27 Feb 2025 03:49:26 +0530 Subject: [PATCH 060/197] feat: -rm airtable from remove_integration --- public/locales/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/de.json b/public/locales/de.json index aade3c67..79fd9bd7 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -368,7 +368,7 @@ "authenticate": "Mit Airtable verbinden", "fetch_bases": "Airtable Basen abrufen", "fetch_tables": "Airtable Tabellen abrufen", - "remove_integration": "Airtable-Integration entfernen", + "remove_integration": "Integration entfernen", "submit": "Basis und Tabelle auswählen" }, "fields": { From 9c80ffea362471f455f5e8dbd39fc4d4c79a3a53 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 27 Feb 2025 03:49:53 +0530 Subject: [PATCH 061/197] feat: -rm airtable from remove_integration --- public/locales/es.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/es.json b/public/locales/es.json index 07234806..1a8b3a43 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -369,7 +369,7 @@ "authenticate": "Conectar con Airtable", "fetch_bases": "Obtener Bases de Airtable", "fetch_tables": "Obtener Tablas de Airtable", - "remove_integration": "Eliminar Integración de Airtable", + "remove_integration": "Eliminar Integración", "submit": "Seleccionar Base y Tabla" }, "fields": { From df03a22d698de3f15523de222296d11d9277609a Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 27 Feb 2025 03:50:07 +0530 Subject: [PATCH 062/197] feat: -rm airtable from remove_integration --- public/locales/ja.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/ja.json b/public/locales/ja.json index a57ff750..2f37b1c5 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -369,7 +369,7 @@ "authenticate": "Airtableと接続", "fetch_bases": "Airtableベースを取得", "fetch_tables": "Airtableテーブルを取得", - "remove_integration": "Airtable連携を解除", + "remove_integration": "連携を解除", "submit": "ベースとテーブルを選択" }, "fields": { From beabca8c350d8399dbd958aab08b104afd720c2f Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 27 Feb 2025 03:50:29 +0530 Subject: [PATCH 063/197] feat: -rm airtable from remove_integration --- public/locales/zh.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/zh.json b/public/locales/zh.json index f0204da7..39987995 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -369,7 +369,7 @@ "authenticate": "连接Airtable", "fetch_bases": "获取Airtable基础", "fetch_tables": "获取Airtable表格", - "remove_integration": "移除Airtable集成", + "remove_integration": "移除集成", "submit": "选择基础和表格" }, "fields": { From 67a7caa2979ada26c0ae9be78d5f80e752239829 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 27 Feb 2025 03:54:04 +0530 Subject: [PATCH 064/197] feat(spanish): airtable base integrated --- public/locales/es.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/es.json b/public/locales/es.json index 1a8b3a43..d7796b0e 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -358,7 +358,7 @@ }, "alerts": { "success": { - "title": "Conectado a Airtable", + "title": "Base Airtable integrada con éxito", "content": "Su robot ahora está conectado a la base: {{baseName}} y tabla: {{tableName}}. Puede acceder a ella ", "here": "aquí", "note": "Nota:", From 4a755767321f02fb869c74c2ec235237234efc3b Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 27 Feb 2025 03:54:35 +0530 Subject: [PATCH 065/197] feat(german): airtable base integrated --- public/locales/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/de.json b/public/locales/de.json index 79fd9bd7..92720f4f 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -357,7 +357,7 @@ }, "alerts": { "success": { - "title": "Mit Airtable verbunden", + "title": "Airtable Base erfolgreich integriert", "content": "Ihr Roboter ist jetzt mit der Basis: {{baseName}} und Tabelle: {{tableName}} verbunden. Sie können darauf zugreifen ", "here": "hier", "note": "Hinweis:", From 02a66de8f8b8b8cc4b20547023156be0a4acc44b Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 27 Feb 2025 03:55:08 +0530 Subject: [PATCH 066/197] feat(chinese): airtable base integrated --- public/locales/zh.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/zh.json b/public/locales/zh.json index 39987995..363252ca 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -358,7 +358,7 @@ }, "alerts": { "success": { - "title": "已连接到Airtable", + "title": "Airtable 基地成功集成", "content": "您的机器人现已连接到基础:{{baseName}}和表格:{{tableName}}。您可以访问它", "here": "在这里", "note": "注意:", From 04c58114948d295bbaa15c37d4b875e020e5ceb1 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 27 Feb 2025 03:55:36 +0530 Subject: [PATCH 067/197] feat(japanese): airtable base integrated --- public/locales/ja.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/ja.json b/public/locales/ja.json index 2f37b1c5..33cc9992 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -358,7 +358,7 @@ }, "alerts": { "success": { - "title": "Airtableに接続しました", + "title": "Airtable ベースの統合に成功", "content": "ロボットはベース: {{baseName}} とテーブル: {{tableName}} に接続されました。アクセスできます ", "here": "こちら", "note": "注意:", From 7f8d38a6a841ee73356f6501723934e26f3a2b70 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 27 Feb 2025 03:56:43 +0530 Subject: [PATCH 068/197] feat: airtable integration success content --- public/locales/es.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/es.json b/public/locales/es.json index d7796b0e..53a75f55 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -359,7 +359,7 @@ "alerts": { "success": { "title": "Base Airtable integrada con éxito", - "content": "Su robot ahora está conectado a la base: {{baseName}} y tabla: {{tableName}}. Puede acceder a ella ", + "content": "Cada vez que este robot crea una ejecución exitosa, los datos capturados se agregan a su base {{baseName}} y a su tabla {{tableName}}. Puedes consultar actualizaciones", "here": "aquí", "note": "Nota:", "sync_limitation": "Solo los datos capturados después de la integración se sincronizarán con Airtable." From 1b5b386cf48f7d348ce45eeb4cb265b1c19896e8 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 27 Feb 2025 03:57:13 +0530 Subject: [PATCH 069/197] feat: airtable integration success content --- public/locales/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/de.json b/public/locales/de.json index 92720f4f..cb4c222e 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -358,7 +358,7 @@ "alerts": { "success": { "title": "Airtable Base erfolgreich integriert", - "content": "Ihr Roboter ist jetzt mit der Basis: {{baseName}} und Tabelle: {{tableName}} verbunden. Sie können darauf zugreifen ", + "content": "Jedes Mal, wenn dieser Roboter einen erfolgreichen Lauf erstellt, werden seine erfassten Daten an Ihre {{baseName}}-Basis und Ihre {{tableName}}-Tabelle angehängt. Sie können Updates überprüfen", "here": "hier", "note": "Hinweis:", "sync_limitation": "Nur nach der Integration erfasste Daten werden mit Airtable synchronisiert." From 00699ea211fd4b4fd1b5647ecbf50f51f9c7b2d1 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 27 Feb 2025 03:57:38 +0530 Subject: [PATCH 070/197] feat: airtable integration success content --- public/locales/zh.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/zh.json b/public/locales/zh.json index 363252ca..27a617e8 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -359,7 +359,7 @@ "alerts": { "success": { "title": "Airtable 基地成功集成", - "content": "您的机器人现已连接到基础:{{baseName}}和表格:{{tableName}}。您可以访问它", + "content": "每次此机器人成功运行时,其捕获的数据都会附加到您的 {{baseName}} 基础和 {{tableName}} 表中。您可以检查更新情况", "here": "在这里", "note": "注意:", "sync_limitation": "只有集成后捕获的数据才会同步到Airtable。" From 488c529cc028130e14b798b6f84a3bf5c1a3a33d Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 27 Feb 2025 03:58:00 +0530 Subject: [PATCH 071/197] feat: airtable integration success content --- public/locales/ja.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/ja.json b/public/locales/ja.json index 33cc9992..e5c242c4 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -359,7 +359,7 @@ "alerts": { "success": { "title": "Airtable ベースの統合に成功", - "content": "ロボットはベース: {{baseName}} とテーブル: {{tableName}} に接続されました。アクセスできます ", + "content": "このロボットが正常な実行を作成するたびに、キャプチャされたデータが {{baseName}} ベースと {{tableName}} テーブルに追加されます。更新情報を確認できます", "here": "こちら", "note": "注意:", "sync_limitation": "連携後に取得されたデータのみがAirtableに同期されます。" From 84205f0e3a266b1e44c27da3fdb30c9fb82786c7 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 27 Feb 2025 03:59:02 +0530 Subject: [PATCH 072/197] feat: airtable integration authentication success --- public/locales/es.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/es.json b/public/locales/es.json index 53a75f55..a13c69b6 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -354,7 +354,7 @@ "title": "Integrar con Airtable", "descriptions": { "sync_info": "Si habilita esta opción, cada vez que este robot ejecute una tarea correctamente, sus datos capturados se añadirán a su Airtable.", - "authenticated_as": "Autenticado con Airtable" + "authenticated_as": "Autenticado con Airtable exitosamente. Ahora puede seleccionar la base y la mesa con las que desea integrar." }, "alerts": { "success": { From 5c91fb96bdd91629b2ab5c19b9900fcd094b3c33 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 27 Feb 2025 03:59:32 +0530 Subject: [PATCH 073/197] feat: airtable integration authentication success --- public/locales/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/de.json b/public/locales/de.json index cb4c222e..3db0c64a 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -353,7 +353,7 @@ "title": "Mit Airtable integrieren", "descriptions": { "sync_info": "Wenn Sie diese Option aktivieren, werden bei jeder erfolgreichen Ausführung einer Aufgabe durch diesen Roboter die erfassten Daten an Ihre Airtable angehängt.", - "authenticated_as": "Mit Airtable authentifiziert" + "authenticated_as": "Erfolgreich bei Airtable authentifiziert. Sie können nun die Basis und den Tisch auswählen, in die Sie integrieren möchten." }, "alerts": { "success": { From 08b12ef02f7be29f851b46f809b1f2a6898faaa2 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 27 Feb 2025 03:59:56 +0530 Subject: [PATCH 074/197] feat: airtable integration authentication success --- public/locales/zh.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/zh.json b/public/locales/zh.json index 27a617e8..5db9eed5 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -354,7 +354,7 @@ "title": "与Airtable集成", "descriptions": { "sync_info": "如果启用此选项,每次机器人成功运行任务时,其捕获的数据将附加到您的Airtable中。", - "authenticated_as": "已使用Airtable认证" + "authenticated_as": "已成功通过 Airtable 进行身份验证。您现在可以选择要集成的底座和桌子。" }, "alerts": { "success": { From 9d5397cecb9f55810306e9f8392ec4cdd7dd30ab Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 27 Feb 2025 04:00:24 +0530 Subject: [PATCH 075/197] feat: airtable integration authentication success --- public/locales/ja.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/ja.json b/public/locales/ja.json index e5c242c4..1b38b933 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -354,7 +354,7 @@ "title": "Airtableと連携", "descriptions": { "sync_info": "このオプションを有効にすると、このロボットがタスクを正常に実行するたびに、取得したデータがAirtableに追加されます。", - "authenticated_as": "Airtableで認証済み" + "authenticated_as": "Airtableで認証に成功しました。統合するベースとテーブルを選択できるようになりました。" }, "alerts": { "success": { From e7a09a06663b23af1508fc1a5e45c57069cf51b1 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 27 Feb 2025 04:07:24 +0530 Subject: [PATCH 076/197] feat: margin left 4px --- src/components/integration/IntegrationSettings.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/integration/IntegrationSettings.tsx b/src/components/integration/IntegrationSettings.tsx index 36a809d4..369bcdb7 100644 --- a/src/components/integration/IntegrationSettings.tsx +++ b/src/components/integration/IntegrationSettings.tsx @@ -436,14 +436,15 @@ export const IntegrationSettingsModal = ({ {t("integration_settings.google.alerts.success.title")} {t("integration_settings.google.alerts.success.content", { - sheetName: recording.google_sheet_name, + sheetName: recording.google_sheet_name, })} - {t("integration_settings.google.alerts.success.here")} + {t("integration_settings.google.alerts.success.here")} + + +
+
setModalOpen(false)} modalStyle={modalStyle}>
{t('recordingtable.modal.title')} From e187520efd5bea6508339bc78e8a1f050a8104c7 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 11 Mar 2025 21:26:29 +0530 Subject: [PATCH 168/197] feat: add en translation for modal --- public/locales/en.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/public/locales/en.json b/public/locales/en.json index 4f7ebdc4..41932f1f 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -54,6 +54,12 @@ "label":"URL", "button":"Start Recording" }, + "warning_modal":{ + "title":"Active Browser Detected", + "message": "There is already a browser recording session running. Would you like to discard it and create a new recording?", + "discard_and_create":"Discard & Create New", + "cancel":"Cancel" + }, "edit":"Edit", "delete":"Delete", "duplicate":"Duplicate", From 7b940ab965c3d1ce8f529684f44e7531a4f5b5a3 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 11 Mar 2025 21:55:27 +0530 Subject: [PATCH 169/197] feat: add support for stop and interpret workflow --- server/src/pgboss-worker.ts | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/server/src/pgboss-worker.ts b/server/src/pgboss-worker.ts index 5d2ea86d..5fd87518 100644 --- a/server/src/pgboss-worker.ts +++ b/server/src/pgboss-worker.ts @@ -16,8 +16,17 @@ interface InitializeBrowserData { userId: string; } +interface InterpretWorkflow { + userId: string; +} + +interface StopInterpretWorkflow { + userId: string; +} + interface DestroyBrowserData { browserId: string; + userId: string; } const pgBoss = new PgBoss({connectionString: pgBossConnectionString, schema: 'public'}); @@ -65,10 +74,10 @@ async function startWorkers() { await pgBoss.work('destroy-browser', async (job: Job | Job[]) => { try { const data = extractJobData(job); - const browserId = data.browserId; + const { browserId, userId } = data; logger.log('info', `Starting browser destruction job for browser: ${browserId}`); - const success = await destroyRemoteBrowser(browserId); + const success = await destroyRemoteBrowser(browserId, userId); logger.log('info', `Browser destruction job completed with result: ${success}`); return { success }; } catch (error: unknown) { @@ -79,10 +88,13 @@ async function startWorkers() { }); // Worker for interpreting workflow - await pgBoss.work('interpret-workflow', async () => { + await pgBoss.work('interpret-workflow', async (job: Job | Job[]) => { try { + const data = extractJobData(job); + const userId = data.userId; + logger.log('info', 'Starting workflow interpretation job'); - await interpretWholeWorkflow(); + await interpretWholeWorkflow(userId); logger.log('info', 'Workflow interpretation job completed'); return { success: true }; } catch (error: unknown) { @@ -93,10 +105,13 @@ async function startWorkers() { }); // Worker for stopping workflow interpretation - await pgBoss.work('stop-interpretation', async () => { + await pgBoss.work('stop-interpretation', async (job: Job | Job[]) => { try { + const data = extractJobData(job); + const userId = data.userId; + logger.log('info', 'Starting stop interpretation job'); - await stopRunningInterpretation(); + await stopRunningInterpretation(userId); logger.log('info', 'Stop interpretation job completed'); return { success: true }; } catch (error: unknown) { From 10e1e8a87c096afe3caf4b57962646b99760bb22 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 11 Mar 2025 21:56:12 +0530 Subject: [PATCH 170/197] feat: queue stop and interpret workflow jobs --- server/src/routes/record.ts | 83 ++++++++++++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 6 deletions(-) diff --git a/server/src/routes/record.ts b/server/src/routes/record.ts index de468ef7..17fd56d2 100644 --- a/server/src/routes/record.ts +++ b/server/src/routes/record.ts @@ -148,6 +148,7 @@ router.get('/stop/:browserId', requireSignIn, async (req: AuthenticatedRequest, const jobId = await pgBoss.send('destroy-browser', { browserId: req.params.browserId, + userId: req.user.id, timestamp: new Date().toISOString() }); @@ -224,14 +225,84 @@ router.get('/active/tabs', requireSignIn, (req: AuthenticatedRequest, res) => { * GET endpoint for starting an interpretation of the currently generated workflow. */ router.get('/interpret', requireSignIn, async (req: AuthenticatedRequest, res) => { + if (!req.user) { + return res.status(401).send('User not authenticated'); + } + try { - if (!req.user) { - return res.status(401).send('User not authenticated'); + await pgBoss.createQueue('interpret-workflow'); + + const jobId = await pgBoss.send('interpret-workflow', { + userId: req.user.id, + timestamp: new Date().toISOString() + }); + + if (!jobId) { + logger.log('warn', 'pgBoss.send returned null, falling back to direct destruction'); + await interpretWholeWorkflow(req.user?.id); + return res.send('interpretation done'); } - await interpretWholeWorkflow(req.user?.id); - return res.send('interpretation done'); - } catch (e) { - return res.send('interpretation failed'); + + logger.log('info', `Queued interpret workflow job: ${jobId}, waiting for completion...`); + + try { + const result = await waitForJobCompletion(jobId, 'interpret-workflow', 15000); + + if (result) { + logger.log('info', `Browser destruction job completed with result: ${result.success}`); + return res.send('interpretation done'); + } else { + logger.log('warn', 'Job completed but returned unexpected result'); + return res.send('interpretation failed'); + } + } catch (waitError: any) { + logger.log('warn', `Error waiting for job completion: ${waitError.message}`); + return res.send('interpretation failed'); + } + } catch (error: any) { + logger.log('error', `Failed to stop browser: ${error.message}`); + return res.status(500).send('interpretation failed'); + } +}); + +router.get('/interpret/stop', requireSignIn, async (req: AuthenticatedRequest, res) => { + if (!req.user) { + return res.status(401).send('User not authenticated'); + } + + try { + await pgBoss.createQueue('stop-interpretation'); + + const jobId = await pgBoss.send('stop-interpretation', { + userId: req.user.id, + timestamp: new Date().toISOString() + }); + + if (!jobId) { + logger.log('warn', 'pgBoss.send returned null, falling back to direct destruction'); + await interpretWholeWorkflow(req.user?.id); + return res.send('interpretation done'); + } + + logger.log('info', `Queued stop interpret workflow job: ${jobId}, waiting for completion...`); + + try { + const result = await waitForJobCompletion(jobId, 'stop-interpretation', 15000); + + if (result) { + logger.log('info', `Browser destruction job completed with result: ${result.success}`); + return res.send('interpretation stopped'); + } else { + logger.log('warn', 'Job completed but returned unexpected result'); + return res.send('interpretation failed to stop'); + } + } catch (waitError: any) { + logger.log('warn', `Error waiting for job completion: ${waitError.message}`); + return res.send('interpretation failed to stop'); + } + } catch (error: any) { + logger.log('error', `Failed to stop browser: ${error.message}`); + return res.status(500).send('interpretation failed to stop'); } }); From e09d3179e71b27dcd904bf407db287d99cdb4ec9 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 11 Mar 2025 22:03:44 +0530 Subject: [PATCH 171/197] feat: log cleanup, better error messages --- server/src/routes/record.ts | 38 +++++-------------------------------- 1 file changed, 5 insertions(+), 33 deletions(-) diff --git a/server/src/routes/record.ts b/server/src/routes/record.ts index 17fd56d2..1a00fdb7 100644 --- a/server/src/routes/record.ts +++ b/server/src/routes/record.ts @@ -56,7 +56,6 @@ async function waitForJobCompletion(jobId: string, queueName: string, timeout = } }; - // Start checking checkJobStatus(); }); } @@ -88,9 +87,8 @@ router.get('/start', requireSignIn, async (req: AuthenticatedRequest, res: Respo }); if (!jobId) { - logger.log('warn', 'pgBoss.send returned null, falling back to direct initialization'); const browserId = initializeRemoteBrowserForRecording(req.user.id); - return res.send( browserId ); + return res.send(browserId); } logger.log('info', `Queued browser initialization job: ${jobId}, waiting for completion...`); @@ -99,14 +97,11 @@ router.get('/start', requireSignIn, async (req: AuthenticatedRequest, res: Respo const result = await waitForJobCompletion(jobId, 'initialize-browser-recording', 15000); if (result && result.browserId) { - logger.log('info', `Job completed with browserId: ${result.browserId}`); return res.send(result.browserId); } else { - logger.log('warn', 'Job completed but returned unexpected result'); return res.send(jobId); } } catch (waitError: any) { - logger.log('warn', `Error waiting for job completion: ${waitError.message}`); return res.send(jobId); } } catch (error: any) { @@ -153,7 +148,6 @@ router.get('/stop/:browserId', requireSignIn, async (req: AuthenticatedRequest, }); if (!jobId) { - logger.log('warn', 'pgBoss.send returned null, falling back to direct destruction'); const browserId = initializeRemoteBrowserForRecording(req.user.id); return res.send( browserId ); } @@ -164,14 +158,11 @@ router.get('/stop/:browserId', requireSignIn, async (req: AuthenticatedRequest, const result = await waitForJobCompletion(jobId, 'destroy-browser', 15000); if (result) { - logger.log('info', `Browser destruction job completed with result: ${result.success}`); return res.send(result.success); } else { - logger.log('warn', 'Job completed but returned unexpected result'); return res.send(false); } } catch (waitError: any) { - logger.log('warn', `Error waiting for job completion: ${waitError.message}`); return res.send(false); } } catch (error: any) { @@ -238,7 +229,6 @@ router.get('/interpret', requireSignIn, async (req: AuthenticatedRequest, res) = }); if (!jobId) { - logger.log('warn', 'pgBoss.send returned null, falling back to direct destruction'); await interpretWholeWorkflow(req.user?.id); return res.send('interpretation done'); } @@ -249,18 +239,15 @@ router.get('/interpret', requireSignIn, async (req: AuthenticatedRequest, res) = const result = await waitForJobCompletion(jobId, 'interpret-workflow', 15000); if (result) { - logger.log('info', `Browser destruction job completed with result: ${result.success}`); return res.send('interpretation done'); } else { - logger.log('warn', 'Job completed but returned unexpected result'); return res.send('interpretation failed'); } } catch (waitError: any) { - logger.log('warn', `Error waiting for job completion: ${waitError.message}`); return res.send('interpretation failed'); } } catch (error: any) { - logger.log('error', `Failed to stop browser: ${error.message}`); + logger.log('error', `Failed to stop interpret workflow: ${error.message}`); return res.status(500).send('interpretation failed'); } }); @@ -279,9 +266,8 @@ router.get('/interpret/stop', requireSignIn, async (req: AuthenticatedRequest, r }); if (!jobId) { - logger.log('warn', 'pgBoss.send returned null, falling back to direct destruction'); - await interpretWholeWorkflow(req.user?.id); - return res.send('interpretation done'); + await stopRunningInterpretation(req.user?.id); + return res.send('interpretation stopped'); } logger.log('info', `Queued stop interpret workflow job: ${jobId}, waiting for completion...`); @@ -290,31 +276,17 @@ router.get('/interpret/stop', requireSignIn, async (req: AuthenticatedRequest, r const result = await waitForJobCompletion(jobId, 'stop-interpretation', 15000); if (result) { - logger.log('info', `Browser destruction job completed with result: ${result.success}`); return res.send('interpretation stopped'); } else { - logger.log('warn', 'Job completed but returned unexpected result'); return res.send('interpretation failed to stop'); } } catch (waitError: any) { - logger.log('warn', `Error waiting for job completion: ${waitError.message}`); return res.send('interpretation failed to stop'); } } catch (error: any) { - logger.log('error', `Failed to stop browser: ${error.message}`); + logger.log('error', `Failed to stop interpretation: ${error.message}`); return res.status(500).send('interpretation failed to stop'); } }); -/** - * GET endpoint for stopping an ongoing interpretation of the currently generated workflow. - */ -router.get('/interpret/stop', requireSignIn, async (req: AuthenticatedRequest, res) => { - if (!req.user) { - return res.status(401).send('User not authenticated'); - } - await stopRunningInterpretation(req.user?.id); - return res.send('interpretation stopped'); -}); - export default router; \ No newline at end of file From 11029f3d0a9591c9e1d3bcebae27deb35a0c469b Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Wed, 12 Mar 2025 16:50:18 +0530 Subject: [PATCH 172/197] feat: sample env vars for airtable --- ENVEXAMPLE | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ENVEXAMPLE b/ENVEXAMPLE index 3c999de4..fd0c85f3 100644 --- a/ENVEXAMPLE +++ b/ENVEXAMPLE @@ -28,10 +28,10 @@ GOOGLE_CLIENT_ID=your_google_client_id GOOGLE_CLIENT_SECRET=your_google_client_secret GOOGLE_REDIRECT_URI=your_google_redirect_uri -# Telemetry Settings - Please keep it enabled. Keeping it enabled helps us understand how the product is used and assess the impact of any new changes. -MAXUN_TELEMETRY=true - -#airtable integration +# Optional Airtable OAuth settings for Airtable Integration AIRTABLE_CLIENT_ID=your_airtable_client_id AIRTABLE_REDIRECT_URI=http://localhost:8080/auth/airtable/callback + +# Telemetry Settings - Please keep it enabled. Keeping it enabled helps us understand how the product is used and assess the impact of any new changes. +MAXUN_TELEMETRY=true From 9a2b14b9380ae7d967d22adf40c974678f70869a Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 12 Mar 2025 16:56:04 +0530 Subject: [PATCH 173/197] feat: 1 user 2 browser changes --- .../browser-management/classes/BrowserPool.ts | 313 +++++++++++++++--- 1 file changed, 263 insertions(+), 50 deletions(-) diff --git a/server/src/browser-management/classes/BrowserPool.ts b/server/src/browser-management/classes/BrowserPool.ts index 4bbc97af..c1f0f557 100644 --- a/server/src/browser-management/classes/BrowserPool.ts +++ b/server/src/browser-management/classes/BrowserPool.ts @@ -4,6 +4,12 @@ import logger from "../../logger"; /** * @category Types */ +/** + * Represents the possible states of a remote browser. + * @category Types + */ +type BrowserState = "recording" | "run"; + interface BrowserPoolInfo { /** * The instance of remote browser. @@ -19,6 +25,12 @@ interface BrowserPoolInfo { * The user ID that owns this browser instance. */ userId: string, + /** + * The current state of the browser. + * Can be "recording" or "run". + * @default "recording" + */ + state: BrowserState, } /** @@ -33,7 +45,7 @@ interface PoolDictionary { /** * A browser pool is a collection of remote browsers that are initialized and ready to be used. - * Enforces a "1 User - 1 Browser" policy, while allowing multiple users to have their own browser instances. + * Enforces a "1 User - 2 Browser" policy, while allowing multiple users to have their own browser instances. * Adds the possibility to add, remove and retrieve remote browsers from the pool. * @category BrowserManagement */ @@ -45,12 +57,13 @@ export class BrowserPool { /** * Maps user IDs to their browser IDs. + * A user can have up to 2 browsers. */ - private userToBrowserMap: Map = new Map(); + private userToBrowserMap: Map = new Map(); /** * Adds a remote browser instance to the pool for a specific user. - * If the user already has a browser, the existing browser will be closed and replaced. + * If the user already has two browsers, the oldest browser will be closed and replaced. * * @param id remote browser instance's id * @param browser remote browser instance @@ -62,38 +75,58 @@ export class BrowserPool { id: string, browser: RemoteBrowser, userId: string, - active: boolean = false + active: boolean = false, + state: BrowserState = "recording" ): boolean => { - // Check if user already has a browser - const existingBrowserId = this.userToBrowserMap.get(userId); + // Check if browser with this ID already exists and belongs to this user + if (this.pool[id] && this.pool[id].userId === userId) { + // Just update the existing browser + this.pool[id] = { + browser, + active, + userId, + state: this.pool[id].state || state, + }; + logger.log('debug', `Updated existing browser with id: ${id} for user: ${userId}`); + return false; + } + + // Get existing browsers for this user + let userBrowserIds = this.userToBrowserMap.get(userId) || []; let replaced = false; - if (existingBrowserId) { - // Close and remove the existing browser - if (existingBrowserId !== id) { - this.closeAndDeleteBrowser(existingBrowserId); - replaced = true; - } else { - // If it's the same browser ID, just update the info - this.pool[id] = { - browser, - active, - userId, - }; - logger.log('debug', `Updated existing browser with id: ${id} for user: ${userId}`); + // If trying to add a "recording" browser, check if one already exists + if (state === "recording") { + // Check if user already has a recording browser + const hasRecordingBrowser = userBrowserIds.some(browserId => + this.pool[browserId] && this.pool[browserId].state === "recording" + ); + + if (hasRecordingBrowser) { + logger.log('debug', `User ${userId} already has a browser in "recording" state`); return false; } } + + // For "run" state, check if the user already has the maximum number of browsers (2) + if (userBrowserIds.length >= 2 && !userBrowserIds.includes(id)) { + logger.log('debug', "User already has the maximum number of browsers (2)"); + return false; + } // Add the new browser to the pool this.pool[id] = { browser, active, userId, + state, }; // Update the user-to-browser mapping - this.userToBrowserMap.set(userId, id); + if (!userBrowserIds.includes(id)) { + userBrowserIds.push(id); + } + this.userToBrowserMap.set(userId, userBrowserIds); logger.log('debug', `Remote browser with id: ${id} added to the pool for user: ${userId}`); return !replaced; @@ -115,8 +148,16 @@ export class BrowserPool { // Remove the user-to-browser mapping const userId = this.pool[id].userId; - if (this.userToBrowserMap.get(userId) === id) { - this.userToBrowserMap.delete(userId); + const userBrowserIds = this.userToBrowserMap.get(userId) || []; + + if (userBrowserIds.includes(id)) { + const updatedBrowserIds = userBrowserIds.filter(bid => bid !== id); + + if (updatedBrowserIds.length === 0) { + this.userToBrowserMap.delete(userId); + } else { + this.userToBrowserMap.set(userId, updatedBrowserIds); + } } // Remove from pool @@ -139,8 +180,16 @@ export class BrowserPool { // Remove the user-to-browser mapping const userId = this.pool[id].userId; - if (this.userToBrowserMap.get(userId) === id) { - this.userToBrowserMap.delete(userId); + const userBrowserIds = this.userToBrowserMap.get(userId) || []; + + if (userBrowserIds.includes(id)) { + const updatedBrowserIds = userBrowserIds.filter(bid => bid !== id); + + if (updatedBrowserIds.length === 0) { + this.userToBrowserMap.delete(userId); + } else { + this.userToBrowserMap.set(userId, updatedBrowserIds); + } } // Remove from pool @@ -162,25 +211,73 @@ export class BrowserPool { /** * Returns the active browser's instance id for a specific user. + * If state is specified, only returns a browser with that exact state. * * @param userId the user ID to find the browser for - * @returns the browser ID for the user, or null if no browser exists + * @param state optional browser state filter ("recording" or "run") + * @returns the browser ID for the user, or null if no browser exists with the required state */ - public getActiveBrowserId = (userId: string): string | null => { - const browserId = this.userToBrowserMap.get(userId); - if (!browserId) { + public getActiveBrowserId = (userId: string, state?: BrowserState): string | null => { + const browserIds = this.userToBrowserMap.get(userId); + if (!browserIds || browserIds.length === 0) { logger.log('debug', `No browser found for user: ${userId}`); return null; } - // Verify the browser still exists in the pool - if (!this.pool[browserId]) { - this.userToBrowserMap.delete(userId); - logger.log('warn', `Browser mapping found for user: ${userId}, but browser doesn't exist in pool`); + // If state is specified, only return browsers with that exact state + if (state) { + // Check browsers in reverse order (newest first) to find one with the specified state + for (let i = browserIds.length - 1; i >= 0; i--) { + const browserId = browserIds[i]; + + // Verify the browser still exists in the pool + if (!this.pool[browserId]) { + browserIds.splice(i, 1); + continue; + } + + // Check if browser matches state filter + if (this.pool[browserId].state === state) { + return browserId; + } + } + + // If no browser with matching state, return null + logger.log('debug', `No browser with state ${state} found for user: ${userId}`); return null; } - console.log(`Browser Id ${browserId} found for user: ${userId}`); - return browserId; + + // If no state specified, return any browser + for (let i = browserIds.length - 1; i >= 0; i--) { + const browserId = browserIds[i]; + + // Verify the browser still exists in the pool + if (!this.pool[browserId]) { + browserIds.splice(i, 1); + continue; + } + + // Return the first browser found + if (this.pool[browserId]) { + console.log(`Active browser Id ${browserId} found for user: ${userId}`); + return browserId; + } + } + + // If no active browser, return the most recent one + if (browserIds.length > 0) { + const mostRecentId = browserIds[browserIds.length - 1]; + console.log(`No active browser found, returning most recent browser Id ${mostRecentId} for user: ${userId}`); + return mostRecentId; + } + + // Clean up the mapping if all browsers were invalid + if (browserIds.length === 0) { + this.userToBrowserMap.delete(userId); + } + + logger.log('warn', `Browser mapping found for user: ${userId}, but no valid browsers exist in pool`); + return null; }; /** @@ -213,10 +310,62 @@ export class BrowserPool { logger.log('debug', `Remote browser with id: ${id} set to ${active ? 'active' : 'inactive'}`); return true; }; + + /** + * Sets the state of a browser. + * Only allows one browser in "recording" state per user. + * + * @param id the browser ID + * @param state the new state ("recording" or "run") + * @returns true if successful, false if the browser wasn't found or state change not allowed + */ + public setBrowserState = (id: string, state: BrowserState): boolean => { + if (!this.pool[id]) { + logger.log('warn', `Remote browser with id: ${id} does not exist in the pool`); + return false; + } + + // If trying to set to "recording" state, check if another browser is already recording + if (state === "recording") { + const userId = this.pool[id].userId; + const userBrowserIds = this.userToBrowserMap.get(userId) || []; + + // Check if any other browser for this user is already in recording state + const hasAnotherRecordingBrowser = userBrowserIds.some(browserId => + browserId !== id && + this.pool[browserId] && + this.pool[browserId].state === "recording" + ); + + if (hasAnotherRecordingBrowser) { + logger.log('warn', `Cannot set browser ${id} to "recording" state: User ${userId} already has a browser in recording state`); + return false; + } + } + + this.pool[id].state = state; + logger.log('debug', `Remote browser with id: ${id} state set to ${state}`); + return true; + }; + + /** + * Gets the current state of a browser. + * + * @param id the browser ID + * @returns the current state or null if the browser wasn't found + */ + public getBrowserState = (id: string): BrowserState | null => { + if (!this.pool[id]) { + logger.log('warn', `Remote browser with id: ${id} does not exist in the pool`); + return null; + } + + return this.pool[id].state; + }; /** * Returns all browser instances for a specific user. - * Should only be one per the "1 User - 1 Browser" policy, but included for flexibility. + * With the "1 User - 2 Browser" policy, this can return up to 2 browsers. * * @param userId the user ID to find browsers for * @returns an array of browser IDs belonging to the user @@ -224,24 +373,31 @@ export class BrowserPool { public getAllBrowserIdsForUser = (userId: string): string[] => { const browserIds: string[] = []; - // Normally this would just return the one browser from the map - const mappedBrowserId = this.userToBrowserMap.get(userId); - if (mappedBrowserId && this.pool[mappedBrowserId]) { - browserIds.push(mappedBrowserId); + // Get browser IDs from the map + const mappedBrowserIds = this.userToBrowserMap.get(userId) || []; + + // Filter to only include IDs that exist in the pool + for (const id of mappedBrowserIds) { + if (this.pool[id]) { + browserIds.push(id); + } } - // But as a safeguard, also check the entire pool for any browsers assigned to this user + // As a safeguard, also check the entire pool for any browsers assigned to this user // This helps detect and fix any inconsistencies in the maps for (const [id, info] of Object.entries(this.pool)) { if (info.userId === userId && !browserIds.includes(id)) { browserIds.push(id); - // Fix the map if it's inconsistent - if (!mappedBrowserId) { - this.userToBrowserMap.set(userId, id); - } } } + // Update the map if inconsistencies were found + if (browserIds.length > 0 && JSON.stringify(browserIds) !== JSON.stringify(mappedBrowserIds)) { + // Limit to 2 browsers if more were found + const limitedBrowserIds = browserIds.slice(-2); + this.userToBrowserMap.set(userId, limitedBrowserIds); + } + return browserIds; }; @@ -264,24 +420,81 @@ export class BrowserPool { * This is a migration helper to support code that hasn't been updated to the user-browser model yet. * * @param currentUserId The ID of the current user, which will be prioritized if multiple browsers exist + * @param state Optional state filter to find browsers in a specific state * @returns A browser ID if one can be determined, or null */ - public getActiveBrowserForMigration = (currentUserId?: string): string | null => { + public getActiveBrowserForMigration = (currentUserId?: string, state?: BrowserState): string | null => { // If a current user ID is provided and they have a browser, return that if (currentUserId) { - const browserForUser = this.getActiveBrowserId(currentUserId); + const browserForUser = this.getActiveBrowserId(currentUserId, state); if (browserForUser) { return browserForUser; } + + // If state is specified and no matching browser was found, return null + if (state) { + return null; + } } - // If only one user has a browser, return that + // If only one user has a browser, try to find a matching browser if (this.userToBrowserMap.size === 1) { const userId = Array.from(this.userToBrowserMap.keys())[0]; - return this.userToBrowserMap.get(userId) || null; + const browserIds = this.userToBrowserMap.get(userId) || []; + + // If state is specified, only look for that state + if (state) { + // Return the active browser that matches the state + for (let i = browserIds.length - 1; i >= 0; i--) { + const bid = browserIds[i]; + if (this.pool[bid]?.active && this.pool[bid].state === state) { + return bid; + } + } + + // If no active browser with matching state, try to find any browser with matching state + for (let i = browserIds.length - 1; i >= 0; i--) { + const bid = browserIds[i]; + if (this.pool[bid] && this.pool[bid].state === state) { + return bid; + } + } + + // If still no matching browser, return null + return null; + } + + // If no state filter, find any active browser + for (let i = browserIds.length - 1; i >= 0; i--) { + if (this.pool[browserIds[i]]?.active) { + return browserIds[i]; + } + } + + return browserIds.length > 0 ? browserIds[browserIds.length - 1] : null; } - // Fall back to the first active browser if any + // Fall back to checking all browsers if no user was specified + if (state) { + // Look for active browsers with the specific state + for (const id of Object.keys(this.pool)) { + if (this.pool[id].active && this.pool[id].state === state) { + return id; + } + } + + // Then look for any browser with the specific state + for (const id of Object.keys(this.pool)) { + if (this.pool[id].state === state) { + return id; + } + } + + // If no browser with the requested state is found, return null + return null; + } + + // If no state filter, find any active browser for (const id of Object.keys(this.pool)) { if (this.pool[id].active) { return id; @@ -299,7 +512,7 @@ export class BrowserPool { * If there are multiple active browsers, it returns the first one. * * @returns the first remote active browser instance's id from the pool - * @deprecated Use getBrowserIdForUser instead to enforce the 1 User - 1 Browser policy + * @deprecated Use getBrowserIdForUser instead to enforce the 1 User - 2 Browser policy */ public getActiveBrowserIdLegacy = (): string | null => { for (const id of Object.keys(this.pool)) { From db4669ab8c2cdf00422808c4de819d0fc92d8049 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 12 Mar 2025 16:57:23 +0530 Subject: [PATCH 174/197] feat: add browser state --- server/src/browser-management/controller.ts | 23 +++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/server/src/browser-management/controller.ts b/server/src/browser-management/controller.ts index e6619d77..2cd9498d 100644 --- a/server/src/browser-management/controller.ts +++ b/server/src/browser-management/controller.ts @@ -21,12 +21,12 @@ import logger from "../logger"; * @category BrowserManagement-Controller */ export const initializeRemoteBrowserForRecording = (userId: string): string => { - const id = getActiveBrowserId(userId) || uuid(); + const id = getActiveBrowserIdByState(userId, "recording") || uuid(); createSocketConnection( io.of(id), async (socket: Socket) => { // browser is already active - const activeId = getActiveBrowserId(userId); + const activeId = getActiveBrowserIdByState(userId, "recording"); if (activeId) { const remoteBrowser = browserPool.getRemoteBrowser(activeId); remoteBrowser?.updateSocket(socket); @@ -37,7 +37,7 @@ export const initializeRemoteBrowserForRecording = (userId: string): string => { await browserSession.initialize(userId); await browserSession.registerEditorEvents(); await browserSession.subscribeToScreencast(); - browserPool.addRemoteBrowser(id, browserSession, userId); + browserPool.addRemoteBrowser(id, browserSession, userId, false, "recording"); } socket.emit('loaded'); }); @@ -59,7 +59,7 @@ export const createRemoteBrowserForRun = (userId: string): string => { async (socket: Socket) => { const browserSession = new RemoteBrowser(socket, userId); await browserSession.initialize(userId); - browserPool.addRemoteBrowser(id, browserSession, userId); + browserPool.addRemoteBrowser(id, browserSession, userId, false, "run"); socket.emit('ready-for-run'); }); return id; @@ -92,6 +92,17 @@ export const getActiveBrowserId = (userId: string): string | null => { return browserPool.getActiveBrowserId(userId); }; +/** + * Returns the id of an active browser with the specified state or null. + * @param userId the user ID to find the browser for + * @param state the browser state to filter by ("recording" or "run") + * @returns {string | null} + * @category BrowserManagement-Controller + */ +export const getActiveBrowserIdByState = (userId: string, state: "recording" | "run"): string | null => { + return browserPool.getActiveBrowserId(userId, state); +}; + /** * Returns the url string from a remote browser if exists in the browser pool. * @param id instance id of the remote browser @@ -127,7 +138,7 @@ export const getRemoteBrowserCurrentTabs = (id: string, userId: string): string[ * @category BrowserManagement-Controller */ export const interpretWholeWorkflow = async (userId: string) => { - const id = getActiveBrowserId(userId); + const id = getActiveBrowserIdByState(userId, "recording"); if (id) { const browser = browserPool.getRemoteBrowser(id); if (browser) { @@ -147,7 +158,7 @@ export const interpretWholeWorkflow = async (userId: string) => { * @category BrowserManagement-Controller */ export const stopRunningInterpretation = async (userId: string) => { - const id = getActiveBrowserId(userId); + const id = getActiveBrowserIdByState(userId, "recording"); if (id) { const browser = browserPool.getRemoteBrowser(id); await browser?.stopCurrentInterpretation(); From 22b640b8d75a78aaee3626eb8b4ccf9412c4bd94 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 12 Mar 2025 16:58:32 +0530 Subject: [PATCH 175/197] feat: add browser state for routes --- server/src/routes/record.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/src/routes/record.ts b/server/src/routes/record.ts index 1a00fdb7..fee5039a 100644 --- a/server/src/routes/record.ts +++ b/server/src/routes/record.ts @@ -11,6 +11,7 @@ import { stopRunningInterpretation, getRemoteBrowserCurrentUrl, getRemoteBrowserCurrentTabs, + getActiveBrowserIdByState, } from '../browser-management/controller'; import { chromium } from 'playwright-extra'; import stealthPlugin from 'puppeteer-extra-plugin-stealth'; @@ -178,7 +179,7 @@ router.get('/active', requireSignIn, (req: AuthenticatedRequest, res) => { if (!req.user) { return res.status(401).send('User not authenticated'); } - const id = getActiveBrowserId(req.user?.id); + const id = getActiveBrowserIdByState(req.user?.id, "recording"); return res.send(id); }); @@ -189,7 +190,7 @@ router.get('/active/url', requireSignIn, (req: AuthenticatedRequest, res) => { if (!req.user) { return res.status(401).send('User not authenticated'); } - const id = getActiveBrowserId(req.user?.id); + const id = getActiveBrowserIdByState(req.user?.id, "recording"); if (id) { const url = getRemoteBrowserCurrentUrl(id, req.user?.id); return res.send(url); @@ -204,7 +205,7 @@ router.get('/active/tabs', requireSignIn, (req: AuthenticatedRequest, res) => { if (!req.user) { return res.status(401).send('User not authenticated'); } - const id = getActiveBrowserId(req.user?.id); + const id = getActiveBrowserIdByState(req.user?.id, "recording"); if (id) { const hosts = getRemoteBrowserCurrentTabs(id, req.user?.id); return res.send(hosts); From 750cd11752ae904ab73adba9754bc6cc536a713f Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 12 Mar 2025 16:59:16 +0530 Subject: [PATCH 176/197] feat: add browser state for input handler --- server/src/browser-management/inputHandlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/browser-management/inputHandlers.ts b/server/src/browser-management/inputHandlers.ts index 92c75e1d..8f4161b5 100644 --- a/server/src/browser-management/inputHandlers.ts +++ b/server/src/browser-management/inputHandlers.ts @@ -56,7 +56,7 @@ const handleWrapper = async ( return; } - const id = browserPool.getActiveBrowserId(userId); + const id = browserPool.getActiveBrowserId(userId, "recording"); if (id) { const activeBrowser = browserPool.getRemoteBrowser(id); if (activeBrowser?.interpreter.interpretationInProgress() && !activeBrowser.interpreter.interpretationIsPaused) { From 8dcbb84c01d2eebcc24ab5aa0d24f7eb77672f17 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 12 Mar 2025 16:59:49 +0530 Subject: [PATCH 177/197] feat: add browser state for workflow routes --- server/src/routes/workflow.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/routes/workflow.ts b/server/src/routes/workflow.ts index 3a117ce1..c8ad92d5 100644 --- a/server/src/routes/workflow.ts +++ b/server/src/routes/workflow.ts @@ -49,7 +49,7 @@ router.get('/params/:browserId', requireSignIn, (req, res) => { */ router.delete('/pair/:index', requireSignIn, (req: AuthenticatedRequest, res) => { if (!req.user) { return res.status(401).send('User not authenticated'); } - const id = browserPool.getActiveBrowserId(req.user?.id); + const id = browserPool.getActiveBrowserId(req.user?.id, "recording"); if (id) { const browser = browserPool.getRemoteBrowser(id); if (browser) { @@ -66,7 +66,7 @@ router.delete('/pair/:index', requireSignIn, (req: AuthenticatedRequest, res) => */ router.post('/pair/:index', requireSignIn, (req: AuthenticatedRequest, res) => { if (!req.user) { return res.status(401).send('User not authenticated'); } - const id = browserPool.getActiveBrowserId(req.user?.id); + const id = browserPool.getActiveBrowserId(req.user?.id, "recording"); if (id) { const browser = browserPool.getRemoteBrowser(id); logger.log('debug', `Adding pair to workflow`); @@ -87,7 +87,7 @@ router.post('/pair/:index', requireSignIn, (req: AuthenticatedRequest, res) => { */ router.put('/pair/:index', requireSignIn, (req: AuthenticatedRequest, res) => { if (!req.user) { return res.status(401).send('User not authenticated'); } - const id = browserPool.getActiveBrowserId(req.user?.id); + const id = browserPool.getActiveBrowserId(req.user?.id, "recording"); if (id) { const browser = browserPool.getRemoteBrowser(id); logger.log('debug', `Updating pair in workflow`); From 8e80067b1dae9c22d22299b9e789cf4ebc613bec Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 12 Mar 2025 17:00:25 +0530 Subject: [PATCH 178/197] feat: add browser state for generator --- server/src/workflow-management/classes/Generator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 4899ac3b..aea37126 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -152,7 +152,7 @@ export class WorkflowGenerator { }); socket.on('activeIndex', (data) => this.generatedData.lastIndex = parseInt(data)); socket.on('decision', async ({ pair, actionType, decision, userId }) => { - const id = browserPool.getActiveBrowserId(userId); + const id = browserPool.getActiveBrowserId(userId, "recording"); if (id) { // const activeBrowser = browserPool.getRemoteBrowser(id); // const currentPage = activeBrowser?.getCurrentPage(); From ea6730208d76849ca6bd651908aa574aecde1478 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 12 Mar 2025 19:25:18 +0530 Subject: [PATCH 179/197] feat: queue robot run --- server/src/routes/storage.ts | 187 ++++++++++++++++------------------- 1 file changed, 87 insertions(+), 100 deletions(-) diff --git a/server/src/routes/storage.ts b/server/src/routes/storage.ts index aa3f4e12..ffdf0149 100644 --- a/server/src/routes/storage.ts +++ b/server/src/routes/storage.ts @@ -1,6 +1,6 @@ import { Router } from 'express'; import logger from "../logger"; -import { createRemoteBrowserForRun, destroyRemoteBrowser } from "../browser-management/controller"; +import { createRemoteBrowserForRun, destroyRemoteBrowser, getActiveBrowserIdByState } from "../browser-management/controller"; import { chromium } from 'playwright-extra'; import stealthPlugin from 'puppeteer-extra-plugin-stealth'; import { browserPool } from "../server"; @@ -22,6 +22,7 @@ import { encrypt, decrypt } from '../utils/auth'; import { WorkflowFile } from 'maxun-core'; import { Page } from 'playwright'; import { airtableUpdateTasks, processAirtableUpdates } from '../workflow-management/integrations/airtable'; +import { pgBoss } from '../pgboss-worker'; chromium.use(stealthPlugin()); export const router = Router(); @@ -494,6 +495,8 @@ router.delete('/runs/:id', requireSignIn, async (req: AuthenticatedRequest, res) /** * PUT endpoint for starting a remote browser instance and saving run metadata to the storage. * Making it ready for interpretation and returning a runId. + * + * If the user has reached their browser limit, the run will be queued using PgBoss. */ router.put('/runs/:id', requireSignIn, async (req: AuthenticatedRequest, res) => { try { @@ -525,35 +528,81 @@ router.put('/runs/:id', requireSignIn, async (req: AuthenticatedRequest, res) => }; } - console.log(`Proxy config for run: ${JSON.stringify(proxyOptions)}`) - - const id = createRemoteBrowserForRun(req.user.id); + console.log(`Proxy config for run: ${JSON.stringify(proxyOptions)}`); + // Generate runId first const runId = uuid(); + + // Check if user has reached browser limit + const userBrowserIds = browserPool.getAllBrowserIdsForUser(req.user.id); + const canCreateBrowser = userBrowserIds.length < 2; + + if (canCreateBrowser) { + // User has available browser slots, create it directly + const id = createRemoteBrowserForRun(req.user.id); - const run = await Run.create({ - status: 'running', - name: recording.recording_meta.name, - robotId: recording.id, - robotMetaId: recording.recording_meta.id, - startedAt: new Date().toLocaleString(), - finishedAt: '', - browserId: id, - interpreterSettings: req.body, - log: '', - runId, - runByUserId: req.user.id, - serializableOutput: {}, - binaryOutput: {}, - }); + const run = await Run.create({ + status: 'running', + name: recording.recording_meta.name, + robotId: recording.id, + robotMetaId: recording.recording_meta.id, + startedAt: new Date().toLocaleString(), + finishedAt: '', + browserId: id, + interpreterSettings: req.body, + log: '', + runId, + runByUserId: req.user.id, + serializableOutput: {}, + binaryOutput: {}, + }); - const plainRun = run.toJSON(); + const plainRun = run.toJSON(); - return res.send({ - browserId: id, - runId: plainRun.runId, - robotMetaId: recording.recording_meta.id, - }); + return res.send({ + browserId: id, + runId: plainRun.runId, + robotMetaId: recording.recording_meta.id, + queued: false + }); + } else { + const browserId = getActiveBrowserIdByState(req.user.id, "run") + + if (browserId) { + // User has reached the browser limit, queue the run + try { + // Create the run record with 'queued' status + await Run.create({ + status: 'queued', + name: recording.recording_meta.name, + robotId: recording.id, + robotMetaId: recording.recording_meta.id, + startedAt: new Date().toLocaleString(), + finishedAt: '', + browserId: browserId, // Random will be updated later + interpreterSettings: req.body, + log: 'Run queued - waiting for available browser slot', + runId, + runByUserId: req.user.id, + serializableOutput: {}, + binaryOutput: {}, + }); + + return res.send({ + browserId: browserId, + runId: runId, + robotMetaId: recording.recording_meta.id, + queued: true, + }); + } catch (queueError: any) { + logger.log('error', `Failed to queue run job: ${queueError.message}`); + return res.status(503).send({ error: 'Unable to queue run, please try again later' }); + } + } else { + logger.log('info', "Browser id does not exist"); + return res.send(''); + } + } } catch (e) { const { message } = e as Error; logger.log('info', `Error while creating a run with robot id: ${req.params.id} - ${message}`); @@ -608,82 +657,20 @@ router.post('/runs/run/:id', requireSignIn, async (req: AuthenticatedRequest, re return res.status(404).send(false); } - // interpret the run in active browser - const browser = browserPool.getRemoteBrowser(plainRun.browserId); - let currentPage = browser?.getCurrentPage(); - if (browser && currentPage) { - const workflow = AddGeneratedFlags(recording.recording); - const interpretationInfo = await browser.interpreter.InterpretRecording( - workflow, currentPage, (newPage: Page) => currentPage = newPage, plainRun.interpreterSettings); - const binaryOutputService = new BinaryOutputService('maxun-run-screenshots'); - const uploadedBinaryOutput = await binaryOutputService.uploadAndStoreBinaryOutput(run, interpretationInfo.binaryOutput); - await destroyRemoteBrowser(plainRun.browserId, req.user?.id); - await run.update({ - ...run, - status: 'success', - finishedAt: new Date().toLocaleString(), - browserId: plainRun.browserId, - log: interpretationInfo.log.join('\n'), - serializableOutput: interpretationInfo.serializableOutput, - binaryOutput: uploadedBinaryOutput, + try { + // Queue the execution job + await pgBoss.createQueue('execute-run'); + + const jobId = await pgBoss.send('execute-run', { + userId: req.user.id, + runId: req.params.id, + browserId: plainRun.browserId }); - - let totalRowsExtracted = 0; - let extractedScreenshotsCount = 0; - let extractedItemsCount = 0; - - if (run.dataValues.binaryOutput && run.dataValues.binaryOutput["item-0"]) { - extractedScreenshotsCount = 1; - } - - if (run.dataValues.serializableOutput && run.dataValues.serializableOutput["item-0"]) { - const itemsArray = run.dataValues.serializableOutput["item-0"]; - extractedItemsCount = itemsArray.length; - - totalRowsExtracted = itemsArray.reduce((total, item) => { - return total + Object.keys(item).length; - }, 0); - } - - console.log(`Extracted Items Count: ${extractedItemsCount}`); - console.log(`Extracted Screenshots Count: ${extractedScreenshotsCount}`); - console.log(`Total Rows Extracted: ${totalRowsExtracted}`); - - capture( - 'maxun-oss-run-created-manual', - { - runId: req.params.id, - user_id: req.user?.id, - created_at: new Date().toISOString(), - status: 'success', - totalRowsExtracted, - extractedItemsCount, - extractedScreenshotsCount, - } - ) - try { - googleSheetUpdateTasks[plainRun.runId] = { - robotId: plainRun.robotMetaId, - runId: plainRun.runId, - status: 'pending', - retries: 5, - }; - - airtableUpdateTasks[plainRun.runId] = { - robotId: plainRun.robotMetaId, - runId: plainRun.runId, - status: 'pending', - retries: 5, - }; - - processAirtableUpdates(); - processGoogleSheetUpdates(); - } catch (err: any) { - logger.log('error', `Failed to update Google Sheet for run: ${plainRun.runId}: ${err.message}`); - } - return res.send(true); - } else { - throw new Error('Could not destroy browser'); + + logger.log('info', `Queued run execution job with ID: ${jobId} for run: ${req.params.id}`); + } catch (queueError: any) { + logger.log('error', `Failed to queue run execution`); + } } catch (e) { const { message } = e as Error; From 7b4e689a7cc4ff079ae0f002f746262a5546724f Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 12 Mar 2025 19:26:08 +0530 Subject: [PATCH 180/197] feat: queue and execute robot run via worker --- server/src/pgboss-worker.ts | 339 +++++++++++++++++++++++++++++++++++- 1 file changed, 338 insertions(+), 1 deletion(-) diff --git a/server/src/pgboss-worker.ts b/server/src/pgboss-worker.ts index 5fd87518..a05b7959 100644 --- a/server/src/pgboss-worker.ts +++ b/server/src/pgboss-worker.ts @@ -7,8 +7,19 @@ import { initializeRemoteBrowserForRecording, destroyRemoteBrowser, interpretWholeWorkflow, - stopRunningInterpretation + stopRunningInterpretation, + createRemoteBrowserForRun } from './browser-management/controller'; +import { WorkflowFile } from 'maxun-core'; +import Run from './models/Run'; +import Robot from './models/Robot'; +import { browserPool } from './server'; +import { Page } from 'playwright'; +import { BinaryOutputService } from './storage/mino'; +import { capture } from './utils/analytics'; +import { googleSheetUpdateTasks, processGoogleSheetUpdates } from './workflow-management/integrations/gsheet'; +import { airtableUpdateTasks, processAirtableUpdates } from './workflow-management/integrations/airtable'; +import { RemoteBrowser } from './browser-management/classes/RemoteBrowser'; const pgBossConnectionString = `postgres://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}`; @@ -29,6 +40,12 @@ interface DestroyBrowserData { userId: string; } +interface ExecuteRunData { + userId: string; + runId: string; + browserId: string; +} + const pgBoss = new PgBoss({connectionString: pgBossConnectionString, schema: 'public'}); /** @@ -44,6 +61,323 @@ function extractJobData(job: Job | Job[]): T { return job.data; } +function AddGeneratedFlags(workflow: WorkflowFile) { + const copy = JSON.parse(JSON.stringify(workflow)); + for (let i = 0; i < workflow.workflow.length; i++) { + copy.workflow[i].what.unshift({ + action: 'flag', + args: ['generated'], + }); + } + return copy; +}; + +/** + * Function to reset browser state without creating a new browser + */ +async function resetBrowserState(browser: RemoteBrowser): Promise { + try { + const currentPage = browser.getCurrentPage(); + if (!currentPage) { + logger.log('error', 'No current page available to reset browser state'); + return false; + } + + // Navigate to blank page to reset state + await currentPage.goto('about:blank'); + + // Clear browser storage + await currentPage.evaluate(() => { + try { + localStorage.clear(); + sessionStorage.clear(); + } catch (e) { + // Ignore errors in cleanup + } + }); + + // Clear cookies + const context = currentPage.context(); + await context.clearCookies(); + + return true; + } catch (error) { + logger.log('error', `Failed to reset browser state`); + return false; + } +} + +/** + * Modified checkAndProcessQueuedRun function - only changes browser reset logic + */ +async function checkAndProcessQueuedRun(userId: string, browserId: string): Promise { + try { + // Find the oldest queued run for this specific browser + const queuedRun = await Run.findOne({ + where: { + browserId: browserId, + status: 'queued' + }, + order: [['startedAt', 'ASC']] + }); + + if (!queuedRun) { + logger.log('info', `No queued runs found for browser ${browserId}`); + return false; + } + + // Reset the browser state before next run + const browser = browserPool.getRemoteBrowser(browserId); + if (browser) { + logger.log('info', `Resetting browser state for browser ${browserId} before next run`); + await resetBrowserState(browser); + } + + // Update the queued run to running status + await queuedRun.update({ + status: 'running', + log: 'Run started - using browser from previous run' + }); + + // Schedule the run execution + await pgBoss.createQueue('execute-run'); + const executeJobId = await pgBoss.send('execute-run', { + userId: userId, + runId: queuedRun.runId, + browserId: browserId + }); + + logger.log('info', `Scheduled queued run ${queuedRun.runId} to use browser ${browserId}, job ID: ${executeJobId}`); + return true; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.log('error', `Error checking for queued runs: ${errorMessage}`); + return false; + } +} + +/** + * Modified processRunExecution function - only add browser reset + */ +async function processRunExecution(job: Job) { + try { + const data = job.data; + logger.log('info', `Processing run execution job for runId: ${data.runId}, browserId: ${data.browserId}`); + + // Find the run + const run = await Run.findOne({ where: { runId: data.runId } }); + if (!run) { + logger.log('error', `Run ${data.runId} not found in database`); + return { success: false }; + } + + const plainRun = run.toJSON(); + + // Find the recording + const recording = await Robot.findOne({ where: { 'recording_meta.id': plainRun.robotMetaId }, raw: true }); + if (!recording) { + logger.log('error', `Recording for run ${data.runId} not found`); + + // Update run status to failed + await run.update({ + status: 'failed', + finishedAt: new Date().toLocaleString(), + log: 'Failed: Recording not found', + }); + + // Check for queued runs even if this one failed + await checkAndProcessQueuedRun(data.userId, data.browserId); + + return { success: false }; + } + + // Get the browser and execute the run + const browser = browserPool.getRemoteBrowser(plainRun.browserId); + let currentPage = browser?.getCurrentPage(); + + if (!browser || !currentPage) { + logger.log('error', `Browser or page not available for run ${data.runId}`); + + // Update run status to failed + await run.update({ + status: 'failed', + finishedAt: new Date().toLocaleString(), + log: 'Failed: Browser or page not available', + }); + + await pgBoss.fail(job.id, "Failed to get browser or page for run"); + + // Even if this run failed, check for queued runs + await checkAndProcessQueuedRun(data.userId, data.browserId); + + return { success: false }; + } + + try { + // Reset the browser state before executing this run + await resetBrowserState(browser); + + // Execute the workflow + const workflow = AddGeneratedFlags(recording.recording); + const interpretationInfo = await browser.interpreter.InterpretRecording( + workflow, + currentPage, + (newPage: Page) => currentPage = newPage, + plainRun.interpreterSettings + ); + + // Process the results + const binaryOutputService = new BinaryOutputService('maxun-run-screenshots'); + const uploadedBinaryOutput = await binaryOutputService.uploadAndStoreBinaryOutput(run, interpretationInfo.binaryOutput); + + // Update the run record with results + await run.update({ + ...run, + status: 'success', + finishedAt: new Date().toLocaleString(), + browserId: plainRun.browserId, + log: interpretationInfo.log.join('\n'), + serializableOutput: interpretationInfo.serializableOutput, + binaryOutput: uploadedBinaryOutput, + }); + + // Track extraction metrics + let totalRowsExtracted = 0; + let extractedScreenshotsCount = 0; + let extractedItemsCount = 0; + + if (run.dataValues.binaryOutput && run.dataValues.binaryOutput["item-0"]) { + extractedScreenshotsCount = 1; + } + + if (run.dataValues.serializableOutput && run.dataValues.serializableOutput["item-0"]) { + const itemsArray = run.dataValues.serializableOutput["item-0"]; + extractedItemsCount = itemsArray.length; + + totalRowsExtracted = itemsArray.reduce((total, item) => { + return total + Object.keys(item).length; + }, 0); + } + + console.log(`Extracted Items Count: ${extractedItemsCount}`); + console.log(`Extracted Screenshots Count: ${extractedScreenshotsCount}`); + console.log(`Total Rows Extracted: ${totalRowsExtracted}`); + + // Capture metrics + capture( + 'maxun-oss-run-created-manual', + { + runId: data.runId, + user_id: data.userId, + created_at: new Date().toISOString(), + status: 'success', + totalRowsExtracted, + extractedItemsCount, + extractedScreenshotsCount, + } + ); + + // Schedule updates for Google Sheets and Airtable + try { + googleSheetUpdateTasks[plainRun.runId] = { + robotId: plainRun.robotMetaId, + runId: plainRun.runId, + status: 'pending', + retries: 5, + }; + + airtableUpdateTasks[plainRun.runId] = { + robotId: plainRun.robotMetaId, + runId: plainRun.runId, + status: 'pending', + retries: 5, + }; + + processAirtableUpdates(); + processGoogleSheetUpdates(); + } catch (err: any) { + logger.log('error', `Failed to update Google Sheet for run: ${plainRun.runId}: ${err.message}`); + } + + // Check for and process queued runs before destroying the browser + const queuedRunProcessed = await checkAndProcessQueuedRun(data.userId, plainRun.browserId); + + // Only destroy the browser if no queued run was found + if (!queuedRunProcessed) { + await destroyRemoteBrowser(plainRun.browserId, data.userId); + logger.log('info', `No queued runs found for browser ${plainRun.browserId}, browser destroyed`); + } + + return { success: true }; + } catch (executionError: any) { + logger.log('error', `Run execution failed for run ${data.runId}: ${executionError.message}`); + + await run.update({ + status: 'failed', + finishedAt: new Date().toLocaleString(), + log: `Failed: ${executionError.message}`, + }); + + // Check for queued runs before destroying the browser + const queuedRunProcessed = await checkAndProcessQueuedRun(data.userId, plainRun.browserId); + + // Only destroy the browser if no queued run was found + if (!queuedRunProcessed) { + try { + await destroyRemoteBrowser(plainRun.browserId, data.userId); + logger.log('info', `No queued runs found for browser ${plainRun.browserId}, browser destroyed`); + } catch (cleanupError: any) { + logger.log('warn', `Failed to clean up browser for failed run ${data.runId}: ${cleanupError.message}`); + } + } + + // Capture failure metrics + capture( + 'maxun-oss-run-created-manual', + { + runId: data.runId, + user_id: data.userId, + created_at: new Date().toISOString(), + status: 'failed', + error_message: executionError.message, + } + ); + + return { success: false }; + } + + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.log('error', `Failed to process run execution job: ${errorMessage}`); + return { success: false }; + } +} + + +async function registerRunExecutionWorker() { + try { + // Worker for executing runs + await pgBoss.work('execute-run', async (job: Job | Job[]) => { + try { + const singleJob = Array.isArray(job) ? job[0] : job; + return await processRunExecution(singleJob); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.log('error', `Run execution job failed: ${errorMessage}`); + throw error; + } + }); + + // setInterval(checkForStuckQueuedRuns, 30000); + + logger.log('info', 'Run execution worker registered successfully'); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.log('error', `Failed to register run execution worker: ${errorMessage}`); + } +} + + /** * Initialize PgBoss and register all workers */ @@ -120,6 +454,9 @@ async function startWorkers() { throw error; } }); + + // Register the run execution worker + await registerRunExecutionWorker(); logger.log('info', 'All recording workers registered successfully'); } catch (error: unknown) { From 8163c46fb854a88d6b715558e13c5fcbfa0e3e57 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 12 Mar 2025 19:30:41 +0530 Subject: [PATCH 181/197] feat: add run status chip translation --- public/locales/de.json | 1 + public/locales/en.json | 1 + public/locales/es.json | 1 + public/locales/ja.json | 1 + public/locales/zh.json | 1 + 5 files changed, 5 insertions(+) diff --git a/public/locales/de.json b/public/locales/de.json index 3db0c64a..363aebbf 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -512,6 +512,7 @@ "success": "Erfolg", "running": "Läuft", "scheduled": "Geplant", + "queued": "In Warteschlange", "failed": "Fehlgeschlagen" }, "run_settings_modal": { diff --git a/public/locales/en.json b/public/locales/en.json index 41932f1f..53ac4184 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -520,6 +520,7 @@ "success": "Success", "running": "Running", "scheduled": "Scheduled", + "queued": "Queued", "failed": "Failed" }, "run_settings_modal": { diff --git a/public/locales/es.json b/public/locales/es.json index a13c69b6..14db7b5d 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -513,6 +513,7 @@ "success": "Éxito", "running": "Ejecutando", "scheduled": "Programado", + "queued": "En cola", "failed": "Fallido" }, "run_settings_modal": { diff --git a/public/locales/ja.json b/public/locales/ja.json index 1b38b933..54f7e788 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -513,6 +513,7 @@ "success": "成功", "running": "実行中", "scheduled": "スケジュール済み", + "queued": "キューに入れました", "failed": "失敗" }, "run_settings_modal": { diff --git a/public/locales/zh.json b/public/locales/zh.json index 5db9eed5..cc23556e 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -513,6 +513,7 @@ "success": "成功", "running": "运行中", "scheduled": "已计划", + "queued": "排队", "failed": "失败" }, "run_settings_modal": { From 767c8ac85c16883f973e68169fda1fccbe4205b8 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 12 Mar 2025 19:31:19 +0530 Subject: [PATCH 182/197] feat: add queued status chip --- src/components/run/ColapsibleRow.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/run/ColapsibleRow.tsx b/src/components/run/ColapsibleRow.tsx index 87e38684..4d5fe822 100644 --- a/src/components/run/ColapsibleRow.tsx +++ b/src/components/run/ColapsibleRow.tsx @@ -123,6 +123,7 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRun {row.status === 'success' && } {row.status === 'running' && } {row.status === 'scheduled' && } + {row.status === 'queued' && } {row.status === 'failed' && } ) From 0b758d772ca94a7f2b96244aa2ee5f02e08349b4 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 12 Mar 2025 20:03:10 +0530 Subject: [PATCH 183/197] feat: load based on run status queued and running --- src/components/run/RunContent.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/run/RunContent.tsx b/src/components/run/RunContent.tsx index cb3b63a6..77bce544 100644 --- a/src/components/run/RunContent.tsx +++ b/src/components/run/RunContent.tsx @@ -134,13 +134,13 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe }}>
- {interpretationInProgress ? currentLog : row.log} + {row.status === 'running' ? currentLog : row.log}
- {interpretationInProgress ? : null} - {interpretationInProgress ? ( + {row.status === 'running' || row.status === 'queued' ? ( {t('run_content.loading')} @@ -248,4 +248,4 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe ); -}; +}; \ No newline at end of file From a3f15b13e4ff4b3a1fe26a16fa6dfd3ff3e3dbc5 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 12 Mar 2025 20:47:39 +0530 Subject: [PATCH 184/197] feat: emit socket event for notif --- server/src/pgboss-worker.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server/src/pgboss-worker.ts b/server/src/pgboss-worker.ts index a05b7959..633d5efa 100644 --- a/server/src/pgboss-worker.ts +++ b/server/src/pgboss-worker.ts @@ -20,6 +20,7 @@ import { capture } from './utils/analytics'; import { googleSheetUpdateTasks, processGoogleSheetUpdates } from './workflow-management/integrations/gsheet'; import { airtableUpdateTasks, processAirtableUpdates } from './workflow-management/integrations/airtable'; import { RemoteBrowser } from './browser-management/classes/RemoteBrowser'; +import { io as serverIo } from "./server"; const pgBossConnectionString = `postgres://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}`; @@ -298,6 +299,14 @@ async function processRunExecution(job: Job) { } catch (err: any) { logger.log('error', `Failed to update Google Sheet for run: ${plainRun.runId}: ${err.message}`); } + + serverIo.of(plainRun.browserId).emit('run-completed', { + runId: data.runId, + robotMetaId: plainRun.robotMetaId, + robotName: recording.recording_meta.name, + status: 'success', + finishedAt: new Date().toLocaleString() + });; // Check for and process queued runs before destroying the browser const queuedRunProcessed = await checkAndProcessQueuedRun(data.userId, plainRun.browserId); From 351084f20e6e24e9a98a0d8761288072a23c58e3 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 12 Mar 2025 20:48:22 +0530 Subject: [PATCH 185/197] feat: check run complete and show notif --- src/pages/MainPage.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx index 0801a933..60990291 100644 --- a/src/pages/MainPage.tsx +++ b/src/pages/MainPage.tsx @@ -103,6 +103,17 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) setSockets(sockets => [...sockets, socket]); socket.on('ready-for-run', () => readyForRunHandler(browserId, runId)); socket.on('debugMessage', debugMessageHandler); + socket.on('run-completed', (data) => { + setRerenderRuns(true); + + const robotName = data.robotName; + + if (data.status === 'success') { + notify('success', t('main_page.notifications.interpretation_success', { name: robotName })); + } else { + notify('error', t('main_page.notifications.interpretation_failed', { name: robotName })); + } + }); setContent('runs'); if (browserId) { notify('info', t('main_page.notifications.run_started', { name: runningRecordingName })); From f7938469598f250f5f87953eb1f13c0383891129 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 12 Mar 2025 21:09:49 +0530 Subject: [PATCH 186/197] feat: rm run update browser not exist --- server/src/pgboss-worker.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/server/src/pgboss-worker.ts b/server/src/pgboss-worker.ts index 633d5efa..3566b4f8 100644 --- a/server/src/pgboss-worker.ts +++ b/server/src/pgboss-worker.ts @@ -199,13 +199,6 @@ async function processRunExecution(job: Job) { if (!browser || !currentPage) { logger.log('error', `Browser or page not available for run ${data.runId}`); - // Update run status to failed - await run.update({ - status: 'failed', - finishedAt: new Date().toLocaleString(), - log: 'Failed: Browser or page not available', - }); - await pgBoss.fail(job.id, "Failed to get browser or page for run"); // Even if this run failed, check for queued runs From a65b34d15ca60317a0d87bc78ea07eba370e7726 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 12 Mar 2025 21:56:10 +0530 Subject: [PATCH 187/197] feat: support redis auth --- server/src/worker.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/worker.ts b/server/src/worker.ts index 3010a6b2..352fcb13 100644 --- a/server/src/worker.ts +++ b/server/src/worker.ts @@ -9,6 +9,7 @@ const connection = new IORedis({ host: process.env.REDIS_HOST, port: process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT, 10) : 6379, maxRetriesPerRequest: null, + password: process.env.REDIS_PASSWORD ? process.env.REDIS_PASSWORD : undefined, }); connection.on('connect', () => { From 79291a325d65c4f0ee2092a27baeeb4ab20c3168 Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Wed, 12 Mar 2025 21:58:34 +0530 Subject: [PATCH 188/197] feat: add REDIS_PASSWORD --- ENVEXAMPLE | 1 + 1 file changed, 1 insertion(+) diff --git a/ENVEXAMPLE b/ENVEXAMPLE index fd0c85f3..704954f2 100644 --- a/ENVEXAMPLE +++ b/ENVEXAMPLE @@ -14,6 +14,7 @@ MINIO_ACCESS_KEY=minio_access_key # MinIO access key MINIO_SECRET_KEY=minio_secret_key # MinIO secret key REDIS_HOST=redis # Redis host in Docker REDIS_PORT=6379 # Redis port (default: 6379) +REDIS_PASSWORD=redis_password # Redis password (This is optional) # Backend and Frontend URLs and Ports BACKEND_PORT=8080 # Port to run backend on. Needed for Docker setup From fdd6edee01513fb541702213414e9ee208381215 Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Wed, 12 Mar 2025 22:00:55 +0530 Subject: [PATCH 189/197] feat: add REDIS_PASSWORD --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3470a4ee..75fee4c5 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,8 @@ You can access the frontend at http://localhost:5173/ and backend at http://loca | `AIRTABLE_CLIENT_ID` | No | Client ID for Airtable, used for Airtable integration authentication. | Airtable login will not work. | | `AIRTABLE_REDIRECT_URI` | No | Redirect URI for handling Airtable OAuth responses. | Airtable login will not work. | | `REDIS_HOST` | Yes | Host address of the Redis server, used by BullMQ for scheduling robots. | Redis connection will fail. | -| `REDIS_PORT` | Yes | Port number for the Redis server. | Redis connection will fail. | +| `REDIS_PORT` | Yes | Port number for the Redis server. | Redis connection will fail. | +| `REDIS_PASSWORD` | No | Password for Redis Authentication. | Needed to authenticate with a password-protected Redis instance; if not set, Redis will attempt to connect without authentication. | | `MAXUN_TELEMETRY` | No | Disables telemetry to stop sending anonymous usage data. Keeping it enabled helps us understand how the product is used and assess the impact of any new changes. Please keep it enabled. | Telemetry data will not be collected. | From 2bdb8135f5e05080088a24aced4388cc89ad49ac Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Wed, 12 Mar 2025 22:01:32 +0530 Subject: [PATCH 190/197] feat: add REDIS_PASSWORD --- ENVEXAMPLE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ENVEXAMPLE b/ENVEXAMPLE index 704954f2..dbe5470c 100644 --- a/ENVEXAMPLE +++ b/ENVEXAMPLE @@ -14,7 +14,7 @@ MINIO_ACCESS_KEY=minio_access_key # MinIO access key MINIO_SECRET_KEY=minio_secret_key # MinIO secret key REDIS_HOST=redis # Redis host in Docker REDIS_PORT=6379 # Redis port (default: 6379) -REDIS_PASSWORD=redis_password # Redis password (This is optional) +REDIS_PASSWORD=redis_password # Redis password (This is optional. Needed to authenticate with a password-protected Redis instance; if not set, Redis will connect without authentication.) # Backend and Frontend URLs and Ports BACKEND_PORT=8080 # Port to run backend on. Needed for Docker setup From 2a5f4e85d9f23c9dcff34af393af15a34ff82940 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 12 Mar 2025 23:26:13 +0530 Subject: [PATCH 191/197] feat: destructure url and user id --- src/components/browser/BrowserNavBar.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/browser/BrowserNavBar.tsx b/src/components/browser/BrowserNavBar.tsx index 72456931..64759d62 100644 --- a/src/components/browser/BrowserNavBar.tsx +++ b/src/components/browser/BrowserNavBar.tsx @@ -51,10 +51,10 @@ const BrowserNavBar: FC = ({ socket?.emit('input:url', address); }, [socket]); - const handleCurrentUrlChange = useCallback((url: string) => { - handleUrlChanged(url); - setRecordingUrl(url); - window.sessionStorage.setItem('recordingUrl', url); + const handleCurrentUrlChange = useCallback((data: { url: string, userId: string }) => { + handleUrlChanged(data.url); + setRecordingUrl(data.url); + window.sessionStorage.setItem('recordingUrl', data.url); }, [handleUrlChanged, recordingUrl]); useEffect(() => { From 7ed2bad10c534c68af32af295a08767cff2c8fb5 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 12 Mar 2025 23:37:00 +0530 Subject: [PATCH 192/197] chore: core v0.0.13 --- maxun-core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maxun-core/package.json b/maxun-core/package.json index 2f04d42a..50d2c420 100644 --- a/maxun-core/package.json +++ b/maxun-core/package.json @@ -1,6 +1,6 @@ { "name": "maxun-core", - "version": "0.0.12", + "version": "0.0.13", "description": "Core package for Maxun, responsible for data extraction", "main": "build/index.js", "typings": "build/index.d.ts", From 6cfc7a4d45e37b748314c402681d27478c103970 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 12 Mar 2025 23:43:16 +0530 Subject: [PATCH 193/197] chore: upgrade core v0.0.13 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 666f9e00..57a22d1d 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "lodash": "^4.17.21", "loglevel": "^1.8.0", "loglevel-plugin-remote": "^0.6.8", - "maxun-core": "^0.0.12", + "maxun-core": "^0.0.13", "minio": "^8.0.1", "moment-timezone": "^0.5.45", "node-cron": "^3.0.3", From 3b9dc937e9b9c48ef00ee4ff114d82750b4de3f8 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 13 Mar 2025 00:16:05 +0530 Subject: [PATCH 194/197] feat: -rm perf monitor --- src/components/recorder/canvas.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/recorder/canvas.tsx b/src/components/recorder/canvas.tsx index 4a015c87..a7d9d984 100644 --- a/src/components/recorder/canvas.tsx +++ b/src/components/recorder/canvas.tsx @@ -28,9 +28,6 @@ export interface Coordinates { const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { - const performanceMonitor = useRef(new EnhancedPerformanceMonitor()); - console.log('Frontend Performance Report:', performanceMonitor.current.getPerformanceReport()); - const canvasRef = useRef(null); const { socket } = useSocketStore(); const { setLastAction, lastAction } = useGlobalInfoStore(); From 01fe506594a5ea439f50cb745776567fa55f9dd1 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 13 Mar 2025 00:16:28 +0530 Subject: [PATCH 195/197] feat: -rm perf monitor import --- src/components/recorder/canvas.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/recorder/canvas.tsx b/src/components/recorder/canvas.tsx index a7d9d984..db48fda0 100644 --- a/src/components/recorder/canvas.tsx +++ b/src/components/recorder/canvas.tsx @@ -6,7 +6,6 @@ import DatePicker from '../pickers/DatePicker'; import Dropdown from '../pickers/Dropdown'; import TimePicker from '../pickers/TimePicker'; import DateTimeLocalPicker from '../pickers/DateTimeLocalPicker'; -import { EnhancedPerformanceMonitor } from '../../../perf/performance'; interface CreateRefCallback { (ref: React.RefObject): void; From 9b56537f2e54639381b81b903fbc56d597127323 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 13 Mar 2025 00:16:59 +0530 Subject: [PATCH 196/197] feat: -rm perf monitor event latency --- src/components/recorder/canvas.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/recorder/canvas.tsx b/src/components/recorder/canvas.tsx index db48fda0..f16da36d 100644 --- a/src/components/recorder/canvas.tsx +++ b/src/components/recorder/canvas.tsx @@ -113,7 +113,6 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { }, [socket]); const onMouseEvent = useCallback((event: MouseEvent) => { - performanceMonitor.current.measureEventLatency(event); if (socket && canvasRef.current) { // Get the canvas bounding rectangle const rect = canvasRef.current.getBoundingClientRect(); From c9c21fd47e25b8c3bd5adc1c5d5f2e65385345ff Mon Sep 17 00:00:00 2001 From: amhsirak Date: Thu, 13 Mar 2025 00:17:16 +0530 Subject: [PATCH 197/197] feat: -rm perf logging --- src/components/recorder/canvas.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/components/recorder/canvas.tsx b/src/components/recorder/canvas.tsx index f16da36d..9640c061 100644 --- a/src/components/recorder/canvas.tsx +++ b/src/components/recorder/canvas.tsx @@ -171,16 +171,6 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { } }, [socket]); - // performance logging - useEffect(() => { - const intervalId = setInterval(() => { - const report = performanceMonitor.current.getPerformanceReport(); - console.log('Frontend Performance Report:', report); - }, 5000); - - return () => clearInterval(intervalId); - }, []); - const onKeyboardEvent = useCallback((event: KeyboardEvent) => { if (socket) { switch (event.type) {