Merge pull request #74 from amhsirak/develop

feat: gsheet oauth flow
This commit is contained in:
Karishma Shukla
2024-10-18 00:36:44 +05:30
committed by GitHub
10 changed files with 532 additions and 135 deletions

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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}` });
}
});

View File

@@ -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',

View File

@@ -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));
}
};
};

View File

@@ -132,7 +132,7 @@ async function executeRun(id: string) {
});
googleSheetUpdateTasks[id] = {
name: plainRun.name,
robotId: plainRun.robotMetaId,
runId: id,
status: 'pending',
retries: 5,

View File

@@ -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}`);

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -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]);