@@ -19,6 +19,11 @@ interface RobotAttributes {
|
||||
id: string;
|
||||
recording_meta: RobotMeta;
|
||||
recording: RobotWorkflow;
|
||||
google_sheet_email?: string | null;
|
||||
google_sheet_name?: string | null;
|
||||
google_sheet_id?: string | null;
|
||||
google_access_token?: string | null;
|
||||
google_refresh_token?: string | null;
|
||||
}
|
||||
|
||||
interface RobotCreationAttributes extends Optional<RobotAttributes, 'id'> { }
|
||||
@@ -27,6 +32,11 @@ class Robot extends Model<RobotAttributes, RobotCreationAttributes> implements R
|
||||
public id!: string;
|
||||
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_access_token!: string | null;
|
||||
public google_refresh_token!: string | null;
|
||||
}
|
||||
|
||||
Robot.init(
|
||||
@@ -44,6 +54,26 @@ Robot.init(
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: false,
|
||||
},
|
||||
google_sheet_email: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
google_sheet_name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
google_sheet_id: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
google_access_token: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
google_refresh_token: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
|
||||
@@ -12,7 +12,6 @@ interface UserAttributes {
|
||||
proxy_password?: string | null;
|
||||
}
|
||||
|
||||
// Optional fields for creating a new user
|
||||
interface UserCreationAttributes extends Optional<UserAttributes, 'id'> { }
|
||||
|
||||
class User extends Model<UserAttributes, UserCreationAttributes> implements UserAttributes {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import User from '../models/User';
|
||||
import Robot from '../models/Robot';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { hashPassword, comparePassword } from '../utils/auth';
|
||||
import { requireSignIn } from '../middlewares/auth';
|
||||
import { genAPIKey } from '../utils/api';
|
||||
import { google } from 'googleapis';
|
||||
export const router = Router();
|
||||
|
||||
interface AuthenticatedRequest extends Request {
|
||||
@@ -163,3 +165,192 @@ router.delete('/delete-api-key', requireSignIn, async (req, res) => {
|
||||
return res.status(500).json({ message: 'Error deleting API key', error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
const oauth2Client = new google.auth.OAuth2(
|
||||
process.env.GOOGLE_CLIENT_ID,
|
||||
process.env.GOOGLE_CLIENT_SECRET,
|
||||
process.env.GOOGLE_REDIRECT_URI
|
||||
);
|
||||
|
||||
// Step 1: Redirect to Google for authentication
|
||||
router.get('/google', (req, res) => {
|
||||
const { robotId } = req.query;
|
||||
if (!robotId) {
|
||||
return res.status(400).json({ message: 'Robot ID is required' });
|
||||
}
|
||||
const scopes = [
|
||||
'https://www.googleapis.com/auth/spreadsheets',
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/drive.readonly',
|
||||
];
|
||||
const url = oauth2Client.generateAuthUrl({
|
||||
access_type: 'offline',
|
||||
prompt: 'consent', // Ensures you get a refresh token on first login
|
||||
scope: scopes,
|
||||
state: robotId.toString(),
|
||||
});
|
||||
res.redirect(url);
|
||||
});
|
||||
|
||||
// Step 2: Handle Google OAuth callback
|
||||
router.get('/google/callback', requireSignIn, async (req, res) => {
|
||||
const { code, state } = req.query;
|
||||
try {
|
||||
if (!state) {
|
||||
return res.status(400).json({ message: 'Robot ID is required' });
|
||||
}
|
||||
|
||||
const robotId = state
|
||||
|
||||
// Get access and refresh tokens
|
||||
if (typeof code !== 'string') {
|
||||
return res.status(400).json({ message: 'Invalid code' });
|
||||
}
|
||||
const { tokens } = await oauth2Client.getToken(code);
|
||||
oauth2Client.setCredentials(tokens);
|
||||
|
||||
// Get user profile from Google
|
||||
const oauth2 = google.oauth2({ version: 'v2', auth: oauth2Client });
|
||||
const { data: { email } } = await oauth2.userinfo.get();
|
||||
|
||||
if (!email) {
|
||||
return res.status(400).json({ message: 'Email not found' });
|
||||
}
|
||||
|
||||
// Get the currently authenticated user (from `requireSignIn`)
|
||||
let user = await User.findOne({ where: { id: req.user.id } });
|
||||
|
||||
if (!user) {
|
||||
return res.status(400).json({ message: 'User not found' });
|
||||
}
|
||||
|
||||
let robot = await Robot.findOne({ where: { 'recording_meta.id': robotId } });
|
||||
|
||||
if (!robot) {
|
||||
return res.status(400).json({ message: 'Robot not found' });
|
||||
}
|
||||
|
||||
robot = await robot.update({
|
||||
google_sheet_email: email,
|
||||
google_access_token: tokens.access_token,
|
||||
google_refresh_token: tokens.refresh_token,
|
||||
});
|
||||
|
||||
// List user's Google Sheets from their Google Drive
|
||||
const drive = google.drive({ version: 'v3', auth: oauth2Client });
|
||||
const response = await drive.files.list({
|
||||
q: "mimeType='application/vnd.google-apps.spreadsheet'", // List only Google Sheets files
|
||||
fields: 'files(id, name)', // Retrieve the ID and name of each file
|
||||
});
|
||||
|
||||
const files = response.data.files || [];
|
||||
if (files.length === 0) {
|
||||
return res.status(404).json({ message: 'No spreadsheets found.' });
|
||||
}
|
||||
|
||||
// Generate JWT token for session
|
||||
const jwtToken = jwt.sign({ userId: user.id }, process.env.JWT_SECRET as string, { expiresIn: '12h' });
|
||||
res.cookie('token', jwtToken, { httpOnly: true });
|
||||
|
||||
res.json({
|
||||
message: 'Google authentication successful',
|
||||
google_sheet_email: robot.google_sheet_email,
|
||||
jwtToken,
|
||||
files
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ message: `Google OAuth error: ${error.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
// Step 3: Get data from Google Sheets
|
||||
router.post('/gsheets/data', requireSignIn, async (req, res) => {
|
||||
const { spreadsheetId, robotId } = req.body;
|
||||
const user = await User.findByPk(req.user.id, { raw: true });
|
||||
|
||||
if (!user) {
|
||||
return res.status(400).json({ message: 'User not found' });
|
||||
}
|
||||
|
||||
const robot = await Robot.findOne({ where: { 'recording_meta.id': robotId }, raw: true });
|
||||
|
||||
if (!robot) {
|
||||
return res.status(400).json({ message: 'Robot not found' });
|
||||
}
|
||||
|
||||
// Set Google OAuth credentials
|
||||
oauth2Client.setCredentials({
|
||||
access_token: robot.google_access_token,
|
||||
refresh_token: robot.google_refresh_token,
|
||||
});
|
||||
|
||||
const sheets = google.sheets({ version: 'v4', auth: oauth2Client });
|
||||
|
||||
try {
|
||||
// Fetch data from the spreadsheet (you can let the user choose a specific range too)
|
||||
const sheetData = await sheets.spreadsheets.values.get({
|
||||
spreadsheetId,
|
||||
range: 'Sheet1!A1:D5', // Default range, could be dynamic based on user input
|
||||
});
|
||||
res.json(sheetData.data);
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ message: `Error accessing Google Sheets: ${error.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
// Step 4: Get user's Google Sheets files (new route)
|
||||
router.get('/gsheets/files', requireSignIn, async (req, res) => {
|
||||
try {
|
||||
const robotId = req.query.robotId;
|
||||
const robot = await Robot.findOne({ where: { 'recording_meta.id': robotId }, raw:true });
|
||||
|
||||
if (!robot) {
|
||||
return res.status(400).json({ message: 'Robot not found' });
|
||||
}
|
||||
|
||||
oauth2Client.setCredentials({
|
||||
access_token: robot.google_access_token,
|
||||
refresh_token: robot.google_refresh_token,
|
||||
});
|
||||
|
||||
// List user's Google Sheets files from their Google Drive
|
||||
const drive = google.drive({ version: 'v3', auth: oauth2Client });
|
||||
const response = await drive.files.list({
|
||||
q: "mimeType='application/vnd.google-apps.spreadsheet'",
|
||||
fields: 'files(id, name)',
|
||||
});
|
||||
|
||||
const files = response.data.files || [];
|
||||
if (files.length === 0) {
|
||||
return res.status(404).json({ message: 'No spreadsheets found.' });
|
||||
}
|
||||
|
||||
res.json(files);
|
||||
} catch (error: any) {
|
||||
console.log('Error fetching Google Sheets files:', error);
|
||||
res.status(500).json({ message: `Error retrieving Google Sheets files: ${error.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
// Step 5: Update robot's google_sheet_id when a Google Sheet is selected
|
||||
router.post('/gsheets/update', requireSignIn, async (req, res) => {
|
||||
const { spreadsheetId, spreadsheetName, robotId } = req.body;
|
||||
|
||||
if (!spreadsheetId || !robotId) {
|
||||
return res.status(400).json({ message: 'Spreadsheet ID and Robot ID are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
let robot = await Robot.findOne({ where: { 'recording_meta.id': robotId } });
|
||||
|
||||
if (!robot) {
|
||||
return res.status(404).json({ message: 'Robot not found' });
|
||||
}
|
||||
|
||||
await robot.update({ google_sheet_id: spreadsheetId, google_sheet_name: spreadsheetName });
|
||||
|
||||
res.json({ message: 'Robot updated with selected Google Sheet ID' });
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ message: `Error updating robot: ${error.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ import { requireSignIn } from '../middlewares/auth';
|
||||
import Robot from '../models/Robot';
|
||||
import Run from '../models/Run';
|
||||
import { BinaryOutputService } from '../storage/mino';
|
||||
import { workflowQueue } from '../worker';
|
||||
// import { workflowQueue } from '../worker';
|
||||
|
||||
export const router = Router();
|
||||
|
||||
@@ -37,6 +37,23 @@ router.get('/recordings', requireSignIn, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET endpoint for getting a recording.
|
||||
*/
|
||||
router.get('/recordings/:id', requireSignIn, async (req, res) => {
|
||||
try {
|
||||
const data = await Robot.findOne({
|
||||
where: { 'recording_meta.id': req.params.id },
|
||||
raw: true
|
||||
}
|
||||
);
|
||||
return res.send(data);
|
||||
} catch (e) {
|
||||
logger.log('info', 'Error while reading recordings');
|
||||
return res.send(null);
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* DELETE endpoint for deleting a recording from the storage.
|
||||
*/
|
||||
@@ -202,13 +219,17 @@ router.post('/runs/run/:id', requireSignIn, async (req, res) => {
|
||||
serializableOutput: interpretationInfo.serializableOutput,
|
||||
binaryOutput: uploadedBinaryOutput,
|
||||
});
|
||||
googleSheetUpdateTasks[req.params.id] = {
|
||||
name: plainRun.name,
|
||||
runId: plainRun.runId,
|
||||
status: 'pending',
|
||||
retries: 5,
|
||||
};
|
||||
processGoogleSheetUpdates();
|
||||
try {
|
||||
googleSheetUpdateTasks[plainRun.runId] = {
|
||||
robotId: plainRun.robotMetaId,
|
||||
runId: plainRun.runId,
|
||||
status: 'pending',
|
||||
retries: 5,
|
||||
};
|
||||
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');
|
||||
@@ -282,16 +303,16 @@ router.put('/schedule/:id/', requireSignIn, async (req, res) => {
|
||||
const runId = uuid();
|
||||
const userId = req.user.id;
|
||||
|
||||
await workflowQueue.add(
|
||||
'run workflow',
|
||||
{ id, runId, userId },
|
||||
{
|
||||
repeat: {
|
||||
pattern: cronExpression,
|
||||
tz: timezone
|
||||
}
|
||||
}
|
||||
);
|
||||
// await workflowQueue.add(
|
||||
// 'run workflow',
|
||||
// { id, runId, userId },
|
||||
// {
|
||||
// repeat: {
|
||||
// pattern: cronExpression,
|
||||
// tz: timezone
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
|
||||
res.status(200).json({
|
||||
message: 'success',
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { google } from "googleapis";
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import logger from "../../logger";
|
||||
import { readFile } from "../storage";
|
||||
import Run from "../../models/Run";
|
||||
import Robot from "../../models/Robot";
|
||||
|
||||
interface GoogleSheetUpdateTask {
|
||||
name: string;
|
||||
robotId: string;
|
||||
runId: string;
|
||||
status: 'pending' | 'completed' | 'failed';
|
||||
retries: number;
|
||||
@@ -14,80 +14,101 @@ const MAX_RETRIES = 5;
|
||||
|
||||
export let googleSheetUpdateTasks: { [runId: string]: GoogleSheetUpdateTask } = {};
|
||||
|
||||
|
||||
// *** Temporary Path to the JSON file that will store the integration details ***
|
||||
const getIntegrationsFilePath = (fileName: string) => path.join(__dirname, `integrations-${fileName}.json`);
|
||||
|
||||
export function loadIntegrations(fileName: string) {
|
||||
const filePath = getIntegrationsFilePath(fileName);
|
||||
if (fs.existsSync(filePath)) {
|
||||
const data = fs.readFileSync(filePath, 'utf-8');
|
||||
return JSON.parse(data);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
export function saveIntegrations(fileName: string, integrations: any) {
|
||||
const filePath = getIntegrationsFilePath(fileName);
|
||||
fs.writeFileSync(filePath, JSON.stringify(integrations, null, 2));
|
||||
}
|
||||
|
||||
export async function updateGoogleSheet(fileName: string, runId: string) {
|
||||
export async function updateGoogleSheet(robotId: string, runId: string) {
|
||||
try {
|
||||
const run = await readFile(`./../storage/runs/${fileName}_${runId}.json`);
|
||||
const parsedRun = JSON.parse(run);
|
||||
const run = await Run.findOne({ where: { runId } });
|
||||
|
||||
if (parsedRun.status === 'success' && parsedRun.serializableOutput) {
|
||||
const data = parsedRun.serializableOutput['item-0'] as { [key: string]: any }[];
|
||||
const integrationConfig = await loadIntegrations(fileName);
|
||||
|
||||
if (integrationConfig) {
|
||||
const { fileName, spreadsheetId, range, credentials } = integrationConfig;
|
||||
|
||||
if (fileName && spreadsheetId && range && credentials) {
|
||||
// Convert data to Google Sheets format (headers and rows)
|
||||
const headers = Object.keys(data[0]);
|
||||
const rows = data.map((row: { [key: string]: any }) => Object.values(row));
|
||||
const outputData = [headers, ...rows];
|
||||
|
||||
await writeDataToSheet(fileName, spreadsheetId, range, outputData);
|
||||
logger.log('info', `Data written to Google Sheet successfully for ${fileName}_${runId}`);
|
||||
}
|
||||
}
|
||||
logger.log('error', `Google Sheet integration not configured for ${fileName}_${runId}`);
|
||||
if (!run) {
|
||||
throw new Error(`Run not found for runId: ${runId}`);
|
||||
}
|
||||
|
||||
const plainRun = run.toJSON();
|
||||
|
||||
if (plainRun.status === 'success' && plainRun.serializableOutput) {
|
||||
const data = plainRun.serializableOutput['item-0'] as { [key: string]: any }[];
|
||||
|
||||
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();
|
||||
|
||||
const spreadsheetId = plainRobot.google_sheet_id;
|
||||
if (plainRobot.google_sheet_email && spreadsheetId) {
|
||||
console.log(`Preparing to write data to Google Sheet for robot: ${robotId}, spreadsheetId: ${spreadsheetId}`);
|
||||
|
||||
const headers = Object.keys(data[0]);
|
||||
const rows = data.map((row: { [key: string]: any }) => Object.values(row));
|
||||
const outputData = [headers, ...rows];
|
||||
|
||||
await writeDataToSheet(robotId, spreadsheetId, outputData);
|
||||
console.log(`Data written to Google Sheet successfully for Robot: ${robotId} and Run: ${runId}`);
|
||||
} else {
|
||||
console.log('Google Sheets integration not configured.');
|
||||
}
|
||||
} else {
|
||||
console.log('Run status is not success or serializableOutput is missing.');
|
||||
}
|
||||
logger.log('error', `Run not successful or no data to update for ${fileName}_${runId}`);
|
||||
} catch (error: any) {
|
||||
logger.log('error', `Failed to write data to Google Sheet for ${fileName}_${runId}: ${error.message}`);
|
||||
console.error(`Failed to write data to Google Sheet for Robot: ${robotId} and Run: ${runId}: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
export async function writeDataToSheet(fileName: string, spreadsheetId: string, range: string, data: any[]) {
|
||||
export async function writeDataToSheet(robotId: string, spreadsheetId: string, data: any[]) {
|
||||
try {
|
||||
const integrationCredentialsPath = getIntegrationsFilePath(fileName);
|
||||
const integrationCredentials = JSON.parse(fs.readFileSync(integrationCredentialsPath, 'utf-8'));;
|
||||
const robot = await Robot.findOne({ where: { 'recording_meta.id': robotId } });
|
||||
|
||||
const auth = new google.auth.GoogleAuth({
|
||||
credentials: {
|
||||
client_email: integrationCredentials.credentials.client_email,
|
||||
private_key: integrationCredentials.credentials.private_key,
|
||||
},
|
||||
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
|
||||
if (!robot) {
|
||||
throw new Error(`Robot not found for robotId: ${robotId}`);
|
||||
}
|
||||
|
||||
const plainRobot = robot.toJSON();
|
||||
|
||||
if (!plainRobot.google_access_token || !plainRobot.google_refresh_token) {
|
||||
throw new Error('Google Sheets access not configured for user');
|
||||
}
|
||||
|
||||
const oauth2Client = new google.auth.OAuth2(
|
||||
process.env.GOOGLE_CLIENT_ID,
|
||||
process.env.GOOGLE_CLIENT_SECRET,
|
||||
process.env.GOOGLE_REDIRECT_URI
|
||||
);
|
||||
|
||||
oauth2Client.setCredentials({
|
||||
access_token: plainRobot.google_access_token,
|
||||
refresh_token: plainRobot.google_refresh_token,
|
||||
});
|
||||
|
||||
const authToken = await auth.getClient();
|
||||
const sheets = google.sheets({ version: 'v4', auth: authToken as any });
|
||||
oauth2Client.on('tokens', async (tokens) => {
|
||||
if (tokens.refresh_token) {
|
||||
await robot.update({ google_refresh_token: tokens.refresh_token });
|
||||
}
|
||||
if (tokens.access_token) {
|
||||
await robot.update({ google_access_token: tokens.access_token });
|
||||
}
|
||||
});
|
||||
|
||||
const sheets = google.sheets({ version: 'v4', auth: oauth2Client });
|
||||
|
||||
const resource = { values: data };
|
||||
console.log('Attempting to write to spreadsheet:', spreadsheetId);
|
||||
|
||||
await sheets.spreadsheets.values.append({
|
||||
const response = await sheets.spreadsheets.values.append({
|
||||
spreadsheetId,
|
||||
range,
|
||||
range: 'Sheet1!A1',
|
||||
valueInputOption: 'USER_ENTERED',
|
||||
requestBody: resource,
|
||||
});
|
||||
|
||||
logger.log(`info`, `Data written to Google Sheet: ${spreadsheetId}, Range: ${range}`);
|
||||
if (response.status === 200) {
|
||||
console.log('Data successfully appended to Google Sheet.');
|
||||
} else {
|
||||
console.error('Google Sheets append failed:', response);
|
||||
}
|
||||
|
||||
logger.log(`info`, `Data written to Google Sheet: ${spreadsheetId}`);
|
||||
} catch (error: any) {
|
||||
logger.log(`error`, `Error writing data to Google Sheet: ${error.message}`);
|
||||
throw error;
|
||||
@@ -99,25 +120,33 @@ export const processGoogleSheetUpdates = async () => {
|
||||
let hasPendingTasks = false;
|
||||
for (const runId in googleSheetUpdateTasks) {
|
||||
const task = googleSheetUpdateTasks[runId];
|
||||
console.log(`Processing task for runId: ${runId}, status: ${task.status}`);
|
||||
|
||||
if (task.status === 'pending') {
|
||||
hasPendingTasks = true;
|
||||
try {
|
||||
await updateGoogleSheet(task.name, task.runId);
|
||||
await updateGoogleSheet(task.robotId, task.runId);
|
||||
console.log(`Successfully updated Google Sheet for runId: ${runId}`);
|
||||
delete googleSheetUpdateTasks[runId];
|
||||
} catch (error: any) {
|
||||
console.error(`Failed to update Google Sheets for run ${task.runId}:`, error);
|
||||
if (task.retries < MAX_RETRIES) {
|
||||
googleSheetUpdateTasks[runId].retries += 1;
|
||||
console.log(`Retrying task for runId: ${runId}, attempt: ${task.retries}`);
|
||||
} else {
|
||||
// Mark as failed after maximum retries
|
||||
googleSheetUpdateTasks[runId].status = 'failed';
|
||||
console.log(`Max retries reached for runId: ${runId}. Marking task as failed.`);
|
||||
}
|
||||
console.error(`Failed to update Google Sheets for run ${task.runId}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasPendingTasks) {
|
||||
console.log('No pending tasks. Exiting loop.');
|
||||
break;
|
||||
}
|
||||
|
||||
console.log('Waiting for 5 seconds before checking again...');
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -132,7 +132,7 @@ async function executeRun(id: string) {
|
||||
});
|
||||
|
||||
googleSheetUpdateTasks[id] = {
|
||||
name: plainRun.name,
|
||||
robotId: plainRun.robotMetaId,
|
||||
runId: id,
|
||||
status: 'pending',
|
||||
retries: 5,
|
||||
|
||||
@@ -32,6 +32,20 @@ export const getStoredRuns = async (): Promise<string[] | null> => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getStoredRecording = async (id: string) => {
|
||||
try {
|
||||
const response = await axios.get(`http://localhost:8080/storage/recordings/${id}`);
|
||||
if (response.status === 200) {
|
||||
return response.data;
|
||||
} else {
|
||||
throw new Error(`Couldn't retrieve stored recording ${id}`);
|
||||
}
|
||||
} catch(error: any) {
|
||||
console.log(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteRecordingFromStorage = async (id: string): Promise<boolean> => {
|
||||
try {
|
||||
const response = await axios.delete(`http://localhost:8080/storage/recordings/${id}`);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { GenericModal } from "../atoms/GenericModal";
|
||||
import { MenuItem, TextField, Typography } from "@mui/material";
|
||||
import { MenuItem, Typography, CircularProgress } from "@mui/material";
|
||||
import Button from "@mui/material/Button";
|
||||
import { modalStyle } from "./AddWhereCondModal";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import axios from 'axios';
|
||||
import { useGlobalInfoStore } from '../../context/globalInfo';
|
||||
import { getStoredRecording } from '../../api/storage';
|
||||
|
||||
interface IntegrationProps {
|
||||
isOpen: boolean;
|
||||
@@ -11,71 +14,175 @@ interface IntegrationProps {
|
||||
}
|
||||
|
||||
export interface IntegrationSettings {
|
||||
credentials: string;
|
||||
spreadsheetId: string;
|
||||
range: string;
|
||||
spreadsheetName: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
export const IntegrationSettingsModal = ({ isOpen, handleStart, handleClose }: IntegrationProps) => {
|
||||
|
||||
const [settings, setSettings] = useState<IntegrationSettings>({
|
||||
credentials: '',
|
||||
spreadsheetId: '',
|
||||
range: '',
|
||||
spreadsheetName: '',
|
||||
data: '',
|
||||
});
|
||||
|
||||
const handleChange = (field: keyof IntegrationSettings) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSettings({ ...settings, [field]: e.target.value });
|
||||
const [spreadsheets, setSpreadsheets] = useState<{ id: string, name: string }[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const { recordingId } = useGlobalInfoStore();
|
||||
const [recording, setRecording] = useState<any>(null);
|
||||
|
||||
const authenticateWithGoogle = () => {
|
||||
window.location.href = `http://localhost:8080/auth/google?robotId=${recordingId}`;
|
||||
};
|
||||
|
||||
const handleOAuthCallback = async () => {
|
||||
try {
|
||||
const response = await axios.get(`http://localhost:8080/auth/google/callback`);
|
||||
const { google_sheet_email, files } = response.data;
|
||||
} catch (error) {
|
||||
setError('Error authenticating with Google');
|
||||
}
|
||||
};
|
||||
|
||||
const fetchSpreadsheetFiles = async () => {
|
||||
try {
|
||||
const response = await axios.get(`http://localhost:8080/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);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSpreadsheetSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const selectedSheet = spreadsheets.find(sheet => sheet.id === e.target.value);
|
||||
if (selectedSheet) {
|
||||
setSettings({ ...settings, spreadsheetId: selectedSheet.id, spreadsheetName: selectedSheet.name });
|
||||
}
|
||||
};
|
||||
|
||||
const updateGoogleSheetId = async () => {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`http://localhost:8080/auth/gsheets/update`,
|
||||
{ spreadsheetId: settings.spreadsheetId, spreadsheetName: settings.spreadsheetName, robotId: recordingId },
|
||||
{ withCredentials: true }
|
||||
);
|
||||
console.log('Google Sheet ID updated:', response.data);
|
||||
} catch (error: any) {
|
||||
console.error('Error updating Google Sheet ID:', error.response?.data?.message || error.message);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// 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);
|
||||
}
|
||||
};
|
||||
|
||||
fetchRecordingInfo();
|
||||
}, [recordingId]);
|
||||
|
||||
return (
|
||||
<GenericModal
|
||||
isOpen={isOpen}
|
||||
onClose={handleClose}
|
||||
modalStyle={modalStyle}
|
||||
>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
marginLeft: '65px',
|
||||
}}>
|
||||
<GenericModal isOpen={isOpen} onClose={handleClose}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', marginLeft: '65px' }}>
|
||||
<Typography sx={{ margin: '20px 0px' }}>Google Sheets Integration</Typography>
|
||||
|
||||
<TextField
|
||||
sx={{ marginBottom: '15px' }}
|
||||
label="Service Account JSON"
|
||||
multiline
|
||||
rows={10}
|
||||
required
|
||||
value={settings.credentials}
|
||||
onChange={handleChange('credentials')}
|
||||
fullWidth
|
||||
/>
|
||||
{recording && recording.google_sheet_id ? (
|
||||
<Typography sx={{ marginBottom: '10px' }}>
|
||||
Google Sheet Integrated Successfully!
|
||||
<br />
|
||||
Sheet Name: {recording.google_sheet_name}
|
||||
<br />
|
||||
Sheet ID: {recording.google_sheet_id}
|
||||
</Typography>
|
||||
) : (
|
||||
<>
|
||||
{!recording?.google_sheet_email ? (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={authenticateWithGoogle}
|
||||
style={{ marginBottom: '15px' }}
|
||||
>
|
||||
Authenticate with Google
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
{recording.google_sheet_email && (
|
||||
<Typography sx={{ marginBottom: '10px' }}>
|
||||
Logged in as: {recording.google_sheet_email}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
sx={{ marginBottom: '15px' }}
|
||||
label="Google Spreadsheet ID"
|
||||
required
|
||||
value={settings.spreadsheetId}
|
||||
onChange={handleChange('spreadsheetId')}
|
||||
fullWidth
|
||||
/>
|
||||
{loading ? (
|
||||
<CircularProgress sx={{ marginBottom: '15px' }} />
|
||||
) : error ? (
|
||||
<Typography color="error">{error}</Typography>
|
||||
) : spreadsheets.length === 0 ? (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={fetchSpreadsheetFiles}
|
||||
style={{ marginBottom: '15px' }}
|
||||
>
|
||||
Fetch Google Spreadsheets
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<TextField
|
||||
sx={{ marginBottom: '15px' }}
|
||||
select
|
||||
label="Select Google Spreadsheet"
|
||||
required
|
||||
value={settings.spreadsheetId}
|
||||
onChange={handleSpreadsheetSelect}
|
||||
fullWidth
|
||||
>
|
||||
{spreadsheets.map(sheet => (
|
||||
<MenuItem key={sheet.id} value={sheet.id}>
|
||||
{sheet.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
sx={{ marginBottom: '15px' }}
|
||||
label="Range (e.g., Sheet1!A1:B2)"
|
||||
required
|
||||
value={settings.range}
|
||||
onChange={handleChange('range')}
|
||||
fullWidth
|
||||
/>
|
||||
{settings.spreadsheetId && (
|
||||
<Typography sx={{ marginBottom: '10px' }}>
|
||||
Selected Sheet: {spreadsheets.find(s => s.id === settings.spreadsheetId)?.name} (ID: {settings.spreadsheetId})
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Button variant="contained" color="primary" onClick={() => handleStart(settings)} style={{ marginTop: '10px' }}>
|
||||
Submit
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
updateGoogleSheetId();
|
||||
handleStart(settings);
|
||||
}}
|
||||
style={{ marginTop: '10px' }}
|
||||
disabled={!settings.spreadsheetId || loading}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</GenericModal>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@ interface RecordingsProps {
|
||||
handleEditRecording: (id: string, fileName: string) => void;
|
||||
handleRunRecording: (settings: RunSettings) => void;
|
||||
handleScheduleRecording: (settings: ScheduleSettings) => void;
|
||||
handleIntegrateRecording: (settings: IntegrationSettings) => void;
|
||||
handleIntegrateRecording: (id: string, settings: IntegrationSettings) => void;
|
||||
setRecordingInfo: (id: string, name: string) => void;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ export const Recordings = ({ handleEditRecording, handleRunRecording, setRecordi
|
||||
const [params, setParams] = useState<string[]>([]);
|
||||
const [selectedRecordingId, setSelectedRecordingId] = useState<string>('');
|
||||
|
||||
console.log(`Selected reocrding id: ${selectedRecordingId}`);
|
||||
|
||||
const handleSettingsAndIntegrate = (id: string, name: string, params: string[]) => {
|
||||
if (params.length === 0) {
|
||||
setIntegrateSettingsAreOpen(true);
|
||||
@@ -94,7 +96,7 @@ export const Recordings = ({ handleEditRecording, handleRunRecording, setRecordi
|
||||
/>
|
||||
<IntegrationSettingsModal isOpen={integrateSettingsAreOpen}
|
||||
handleClose={handleIntegrateClose}
|
||||
handleStart={(settings) => handleIntegrateRecording(settings)}
|
||||
handleStart={(settings) => handleIntegrateRecording(selectedRecordingId, settings)}
|
||||
/>
|
||||
<Grid container direction="column" sx={{ padding: '30px' }}>
|
||||
<Grid item xs>
|
||||
|
||||
@@ -346,6 +346,10 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
}
|
||||
});
|
||||
resetListState();
|
||||
setShowPaginationOptions(false);
|
||||
setShowLimitOptions(false);
|
||||
setCaptureStage('initial');
|
||||
setConfirmedListTextFields({});
|
||||
notify('error', 'Capture List Discarded');
|
||||
}, [browserSteps, stopGetList, deleteBrowserStep, resetListState]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user