From c3a58fc23862ab3a70df20f5cb185f6ba050b0c5 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 27 May 2025 16:09:29 +0530 Subject: [PATCH 01/26] feat: add webhook integration button --- .../integration/IntegrationSettings.tsx | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/src/components/integration/IntegrationSettings.tsx b/src/components/integration/IntegrationSettings.tsx index 5ca71840..455fe72c 100644 --- a/src/components/integration/IntegrationSettings.tsx +++ b/src/components/integration/IntegrationSettings.tsx @@ -23,7 +23,16 @@ interface IntegrationProps { isOpen: boolean; handleStart: (data: IntegrationSettings) => void; handleClose: () => void; - preSelectedIntegrationType?: "googleSheets" | "airtable" | null; + preSelectedIntegrationType?: "googleSheets" | "airtable" | "webhook" | null; +} + +export interface WebhookConfig { + id: string; + name: string; + url: string; + headers: { [key: string]: string }; + events: string[]; + active: boolean; } export interface IntegrationSettings { @@ -33,8 +42,9 @@ export interface IntegrationSettings { airtableBaseName?: string; airtableTableName?: string, airtableTableId?: string, + webhooks?: WebhookConfig[]; data: string; - integrationType: "googleSheets" | "airtable"; + integrationType: "googleSheets" | "airtable" | "webhook"; } const getCookie = (name: string): string | null => { @@ -64,6 +74,7 @@ export const IntegrationSettingsModal = ({ airtableBaseName: "", airtableTableName: "", airtableTableId: "", + webhooks: [], data: "", integrationType: preSelectedIntegrationType || "googleSheets", }); @@ -74,6 +85,18 @@ export const IntegrationSettingsModal = ({ const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + // Webhook-specific state + const [newWebhook, setNewWebhook] = useState({ + id: "", + name: "", + url: "", + headers: {}, + events: ["scrape_completed"], + active: true, + }); + const [newHeaderKey, setNewHeaderKey] = useState(""); + const [newHeaderValue, setNewHeaderValue] = useState(""); + const { recordingId, notify, @@ -84,7 +107,7 @@ export const IntegrationSettingsModal = ({ const navigate = useNavigate(); const [selectedIntegrationType, setSelectedIntegrationType] = useState< - "googleSheets" | "airtable" | null + "googleSheets" | "airtable" | "webhook" | null >(preSelectedIntegrationType); const authenticateWithGoogle = () => { @@ -410,6 +433,19 @@ export const IntegrationSettingsModal = ({ Airtable Airtable + + From cfd1bc5ecbffe47893d464a7c7e34b3b3db88012 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 27 May 2025 16:13:55 +0530 Subject: [PATCH 02/26] feat: add webhook svg --- public/svg/webhook.svg | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 public/svg/webhook.svg diff --git a/public/svg/webhook.svg b/public/svg/webhook.svg new file mode 100644 index 00000000..959bc307 --- /dev/null +++ b/public/svg/webhook.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file From 2b12726d1be0810debf4e7f63d5ff82b9be9cc01 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 27 May 2025 20:06:46 +0530 Subject: [PATCH 03/26] feat: add migration for webhooks column --- .../migrations/20250527105655-add-webhooks.js | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 server/src/db/migrations/20250527105655-add-webhooks.js diff --git a/server/src/db/migrations/20250527105655-add-webhooks.js b/server/src/db/migrations/20250527105655-add-webhooks.js new file mode 100644 index 00000000..60eefd19 --- /dev/null +++ b/server/src/db/migrations/20250527105655-add-webhooks.js @@ -0,0 +1,27 @@ +'use strict'; + +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.addColumn('robot', 'webhooks', { + type: Sequelize.JSONB, + allowNull: true, + defaultValue: null, + comment: 'Webhook configurations for the robot' + }); + + // Optional: Add an index for better query performance if you plan to search within webhook data + await queryInterface.addIndex('robot', { + fields: ['webhooks'], + using: 'gin', // GIN index for JSONB columns + name: 'robot_webhooks_gin_idx' + }); + }, + + async down(queryInterface, Sequelize) { + // Remove the index first + await queryInterface.removeIndex('robot', 'robot_webhooks_gin_idx'); + + // Then remove the column + await queryInterface.removeColumn('robot', 'webhooks'); + } +}; \ No newline at end of file From 1777a598c13fe512c2e9f020879e57d7e8cfe24f Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 27 May 2025 20:08:01 +0530 Subject: [PATCH 04/26] feat: add webhook column model --- server/src/models/Robot.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/server/src/models/Robot.ts b/server/src/models/Robot.ts index 1681eaac..eae9438e 100644 --- a/server/src/models/Robot.ts +++ b/server/src/models/Robot.ts @@ -15,6 +15,19 @@ interface RobotWorkflow { workflow: WhereWhatPair[]; } +interface WebhookConfig { + id: string; + url: string; + events: string[]; + active: boolean; + createdAt: string; + updatedAt: string; + lastCalledAt?: string | null; + retryAttempts?: number; + retryDelay?: number; + timeout?: number; +} + interface RobotAttributes { id: string; userId?: number; @@ -32,6 +45,7 @@ interface RobotAttributes { airtable_refresh_token?: string | null; schedule?: ScheduleConfig | null; airtable_table_id?: string | null; + webhooks?: WebhookConfig[] | null; } interface ScheduleConfig { @@ -66,6 +80,7 @@ class Robot extends Model implements R public airtable_refresh_token!: string | null; public airtable_table_id!: string | null; public schedule!: ScheduleConfig | null; + public webhooks!: WebhookConfig[] | null; } Robot.init( @@ -135,6 +150,11 @@ Robot.init( type: DataTypes.JSONB, allowNull: true, }, + webhooks: { + type: DataTypes.JSONB, + allowNull: true, + defaultValue: null, + }, }, { sequelize, From d38a5cd39c71e1f2382dbe98c7eb31e3106cd995 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 27 May 2025 20:09:06 +0530 Subject: [PATCH 05/26] feat: add webhook navigation endpoint --- src/components/robot/Recordings.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/robot/Recordings.tsx b/src/components/robot/Recordings.tsx index 780d01cf..1b3fc7b2 100644 --- a/src/components/robot/Recordings.tsx +++ b/src/components/robot/Recordings.tsx @@ -114,6 +114,15 @@ export const Recordings = ({ preSelectedIntegrationType="airtable" /> ); + } else if (currentPath.endsWith("/integrate/webhook")) { + return ( + {}} + preSelectedIntegrationType="webhook" + /> + ); } else if (currentPath.endsWith("/integrate")) { return ( Date: Tue, 27 May 2025 20:09:52 +0530 Subject: [PATCH 06/26] feat: register webhook endpoint --- server/src/routes/index.ts | 4 +++- server/src/server.ts | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/server/src/routes/index.ts b/server/src/routes/index.ts index bc616273..3d8a3644 100644 --- a/server/src/routes/index.ts +++ b/server/src/routes/index.ts @@ -4,6 +4,7 @@ import { router as storage } from './storage'; import { router as auth } from './auth'; import { router as integration } from './integration'; import { router as proxy } from './proxy'; +import { router as webhook } from './webhook'; export { record, @@ -11,5 +12,6 @@ export { storage, auth, integration, - proxy + proxy, + webhook }; diff --git a/server/src/server.ts b/server/src/server.ts index 7f2d04d3..bd4a1697 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -4,7 +4,7 @@ import http from 'http'; import cors from 'cors'; import dotenv from 'dotenv'; dotenv.config(); -import { record, workflow, storage, auth, integration, proxy } from './routes'; +import { record, workflow, storage, auth, integration, proxy, webhook } from './routes'; import { BrowserPool } from "./browser-management/classes/BrowserPool"; import logger from './logger'; import { connectDB, syncDB } from './storage/db' @@ -88,6 +88,7 @@ export const browserPool = new BrowserPool(); // parse cookies - "cookie" is true in csrfProtection app.use(cookieParser()) +app.use('/webhook', webhook); app.use('/record', record); app.use('/workflow', workflow); app.use('/storage', storage); From a2e96a1779419a0e78f3785e33025090c1ebc865 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 27 May 2025 20:30:19 +0530 Subject: [PATCH 07/26] feat: add webhooks config modal --- .../integration/IntegrationSettings.tsx | 499 +++++++++++++++++- 1 file changed, 489 insertions(+), 10 deletions(-) diff --git a/src/components/integration/IntegrationSettings.tsx b/src/components/integration/IntegrationSettings.tsx index 455fe72c..09cabbd8 100644 --- a/src/components/integration/IntegrationSettings.tsx +++ b/src/components/integration/IntegrationSettings.tsx @@ -8,7 +8,23 @@ import { AlertTitle, Button, TextField, + IconButton, + Box, + Chip, + Card, + CardContent, + CardActions, + Switch, + FormControlLabel, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, } from "@mui/material"; +import { Add as AddIcon, Delete as DeleteIcon, Edit as EditIcon, Science as ScienceIcon } from "@mui/icons-material"; import axios from "axios"; import { useGlobalInfoStore } from "../../context/globalInfo"; import { getStoredRecording } from "../../api/storage"; @@ -28,11 +44,10 @@ interface IntegrationProps { export interface WebhookConfig { id: string; - name: string; url: string; - headers: { [key: string]: string }; events: string[]; active: boolean; + lastCalledAt?: string | null; } export interface IntegrationSettings { @@ -85,17 +100,15 @@ export const IntegrationSettingsModal = ({ const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - // Webhook-specific state + const [showWebhookForm, setShowWebhookForm] = useState(false); + const [editingWebhook, setEditingWebhook] = useState(null); const [newWebhook, setNewWebhook] = useState({ id: "", - name: "", url: "", - headers: {}, - events: ["scrape_completed"], + events: ["run_completed"], active: true, }); - const [newHeaderKey, setNewHeaderKey] = useState(""); - const [newHeaderValue, setNewHeaderValue] = useState(""); + const [urlError, setUrlError] = useState(null); const { recordingId, @@ -119,6 +132,207 @@ export const IntegrationSettingsModal = ({ window.location.href = `${apiUrl}/auth/airtable?robotId=${recordingId}`; }; + const validateWebhookData = (url: string, events: string[], excludeId?: string) => { + if (!url) { + setUrlError("Please provide webhook URL"); + return false; + } + + const existingWebhook = settings.webhooks?.find( + (webhook) => webhook.url === url && webhook.id !== excludeId + ); + + if (existingWebhook) { + setUrlError("This webhook URL is already in use"); + return false; + } + + if (!events || events.length === 0) { + setUrlError("Please select at least one event"); + return false; + } + + setUrlError(null); + return true; + }; + + const addWebhook = async () => { + if (!validateWebhookData(newWebhook.url, newWebhook.events)) { + if (!newWebhook.url) { + notify("error", "Please provide webhook URL"); + } else if (!newWebhook.events || newWebhook.events.length === 0) { + notify("error", "Please select at least one event"); + } + return; + } + + try { + setLoading(true); + const webhookWithId = { + ...newWebhook, + id: Date.now().toString(), + }; + + const response = await axios.post( + `${apiUrl}/webhook/add`, + { + webhook: webhookWithId, + robotId: recordingId, + }, + { withCredentials: true } + ); + + const updatedWebhooks = [...(settings.webhooks || []), webhookWithId]; + setSettings({ ...settings, webhooks: updatedWebhooks }); + + resetWebhookForm(); + await refreshRecordingData(); + notify("success", "Webhook added successfully"); + setLoading(false); + } catch (error: any) { + setLoading(false); + console.log("Error adding webhook:", error); + notify("error", `Error adding webhook: ${error.response?.data?.message || error.message}`); + } + }; + + const updateWebhook = async () => { + if (!editingWebhook) return; + + if (!validateWebhookData(newWebhook.url, newWebhook.events, editingWebhook)) { + if (!newWebhook.url) { + notify("error", "Please provide webhook URL"); + } else if (!newWebhook.events || newWebhook.events.length === 0) { + notify("error", "Please select at least one event"); + } + return; + } + + try { + setLoading(true); + await axios.post( + `${apiUrl}/webhook/update`, + { + webhook: newWebhook, + robotId: recordingId, + }, + { withCredentials: true } + ); + + const updatedWebhooks = (settings.webhooks || []).map(w => + w.id === editingWebhook ? newWebhook : w + ); + setSettings({ ...settings, webhooks: updatedWebhooks }); + + resetWebhookForm(); + await refreshRecordingData(); + notify("success", "Webhook updated successfully"); + setLoading(false); + } catch (error: any) { + setLoading(false); + console.error("Error updating webhook:", error); + notify("error", `Error updating webhook: ${error.response?.data?.message || error.message}`); + } + }; + + const removeWebhook = async (webhookId: string) => { + try { + setLoading(true); + await axios.post( + `${apiUrl}/webhook/remove`, + { + webhookId, + robotId: recordingId, + }, + { withCredentials: true } + ); + + const updatedWebhooks = (settings.webhooks || []).filter(w => w.id !== webhookId); + setSettings({ ...settings, webhooks: updatedWebhooks }); + + await refreshRecordingData(); + notify("success", "Webhook removed successfully"); + setLoading(false); + } catch (error: any) { + setLoading(false); + console.error("Error removing webhook:", error); + notify("error", `Error removing webhook: ${error.response?.data?.message || error.message}`); + } + }; + + const toggleWebhookStatus = async (webhookId: string) => { + try { + const webhook = settings.webhooks?.find(w => w.id === webhookId); + if (!webhook) return; + + const updatedWebhook = { ...webhook, active: !webhook.active }; + + await axios.post( + `${apiUrl}/webhook/update`, + { + webhook: updatedWebhook, + robotId: recordingId, + }, + { withCredentials: true } + ); + + const updatedWebhooks = (settings.webhooks || []).map(w => + w.id === webhookId ? updatedWebhook : w + ); + setSettings({ ...settings, webhooks: updatedWebhooks }); + + await refreshRecordingData(); + notify("success", `Webhook ${updatedWebhook.active ? "enabled" : "disabled"}`); + } catch (error: any) { + console.error("Error toggling webhook status:", error); + notify("error", `Error updating webhook: ${error.response?.data?.message || error.message}`); + } + }; + + const testWebhook = async (webhook: WebhookConfig) => { + try { + setLoading(true); + await axios.post( + `${apiUrl}/webhook/test`, + { + webhook, + robotId: recordingId, + }, + { withCredentials: true } + ); + + const updatedWebhooks = (settings.webhooks || []).map(w => + w.id === webhook.id ? { ...w, lastCalledAt: new Date().toISOString() } : w + ); + setSettings({ ...settings, webhooks: updatedWebhooks }); + + notify("success", "Test webhook sent successfully"); + setLoading(false); + } catch (error: any) { + setLoading(false); + console.error("Error testing webhook:", error); + notify("error", `Error testing webhook: ${error.response?.data?.message || error.message}`); + } + }; + + const editWebhook = (webhook: WebhookConfig) => { + setNewWebhook(webhook); + setEditingWebhook(webhook.id); + setShowWebhookForm(true); + }; + + const resetWebhookForm = () => { + setNewWebhook({ + id: "", + url: "", + events: ["run_completed"], + active: true, + }); + setShowWebhookForm(false); + setEditingWebhook(null); + setUrlError(null); + }; + // Fetch Google Sheets files const fetchSpreadsheetFiles = async () => { try { @@ -366,6 +580,12 @@ export const IntegrationSettingsModal = ({ airtableTableId: recording.airtable_table_id || "", integrationType: recording.airtable_base_id ? "airtable" : "googleSheets" })); + } else if (recording.webhooks && recording.webhooks.length > 0) { + setSettings(prev => ({ + ...prev, + webhooks: recording.webhooks, + integrationType: "webhook" + })); } } @@ -393,7 +613,50 @@ export const IntegrationSettingsModal = ({ } }, []); - // Add this UI at the top of the modal return statement + const formatEventName = (event: string) => { + switch (event) { + case "run_completed": + return "Run finished"; + case "run_completed_success": + return "Run finished successfully"; + case "run_failed": + return "Run failed"; + default: + return event; + } + }; + + const formatLastCalled = (lastCalledAt?: string | null) => { + if (!lastCalledAt) { + return "Not called yet"; + } + + const date = new Date(lastCalledAt); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); + const diffMinutes = Math.floor(diffMs / (1000 * 60)); + + if (diffMinutes < 1) { + return "Just now"; + } else if (diffMinutes < 60) { + return `${diffMinutes} minute${diffMinutes === 1 ? '' : 's'} ago`; + } else if (diffHours < 24) { + return `${diffHours} hour${diffHours === 1 ? '' : 's'} ago`; + } else if (diffDays < 7) { + return `${diffDays} day${diffDays === 1 ? '' : 's'} ago`; + } else { + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + } + }; + if (!selectedIntegrationType) { return ( {settings.integrationType === "googleSheets" && ( @@ -676,6 +940,219 @@ export const IntegrationSettingsModal = ({ )} )} + + {settings.integrationType === "webhook" && ( + <> + + Integrate using Webhooks + + + {settings.webhooks && settings.webhooks.length > 0 && ( + + + + + Webhook URL + Call when + Last called + Status + Actions + + + + {settings.webhooks.map((webhook) => ( + + {webhook.url} + {formatEventName(webhook.events[0])} + {formatLastCalled(webhook.lastCalledAt)} + + toggleWebhookStatus(webhook.id)} + size="small" + /> + + + + testWebhook(webhook)} + disabled={loading || !webhook.active} + title="Test" + > + + + editWebhook(webhook)} + disabled={loading} + title="Edit" + > + + + removeWebhook(webhook.id)} + disabled={loading} + title="Delete" + > + + + + + + ))} + +
+
+ )} + + {!showWebhookForm && ( + + + { + setNewWebhook({ ...newWebhook, url: e.target.value }); + if (urlError) setUrlError(null); + }} + error={!!urlError} + helperText={urlError} + /> + setNewWebhook({ + ...newWebhook, + events: [e.target.value] + })} + sx={{ minWidth: "200px" }} + > + Run finished + Run finished successfully + Run failed + + + + + + Refer to the API documentation for examples and details. + + + + + )} + + {showWebhookForm && ( + + + + {editingWebhook ? "Edit Webhook" : "Add New Webhook"} + + + { + setNewWebhook({ ...newWebhook, url: e.target.value }); + if (urlError) setUrlError(null); + }} + sx={{ marginBottom: "15px" }} + placeholder="https://your-api.com/webhook/endpoint" + required + error={!!urlError} + helperText={urlError} + /> + + setNewWebhook({ + ...newWebhook, + events: typeof e.target.value === 'string' ? [e.target.value] : e.target.value + })} + SelectProps={{ + multiple: true, + renderValue: (selected) => ( + + {(selected as string[]).map((value) => ( + + ))} + + ), + }} + sx={{ marginBottom: "20px" }} + required + > + Run finished + Run finished successfully + Run failed + + + setNewWebhook({ ...newWebhook, active: e.target.checked })} + /> + } + label="Active" + sx={{ marginBottom: "10px" }} + /> + + + + + + + + )} + + )}
); @@ -685,10 +1162,12 @@ export const modalStyle = { top: "40%", left: "50%", transform: "translate(-50%, -50%)", - width: "50%", + width: "60%", backgroundColor: "background.paper", p: 4, height: "fit-content", display: "block", padding: "20px", + maxHeight: "90vh", + overflow: "auto", }; \ No newline at end of file From 6f11b69f12950979e36c26c218b56bdae26a9cf4 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 27 May 2025 22:12:00 +0530 Subject: [PATCH 08/26] feat: add webhook routes --- server/src/routes/webhook.ts | 445 +++++++++++++++++++++++++++++++++++ 1 file changed, 445 insertions(+) create mode 100644 server/src/routes/webhook.ts diff --git a/server/src/routes/webhook.ts b/server/src/routes/webhook.ts new file mode 100644 index 00000000..ddc9c1bf --- /dev/null +++ b/server/src/routes/webhook.ts @@ -0,0 +1,445 @@ +import { Router, Request, Response } from 'express'; +import Robot from '../models/Robot'; +import { requireSignIn } from '../middlewares/auth'; +import axios from 'axios'; + +export const router = Router(); + +interface AuthenticatedRequest extends Request { + user?: { id: string }; +} + +interface WebhookConfig { + id: string; + url: string; + events: string[]; + active: boolean; + createdAt: string; + updatedAt: string; + lastCalledAt?: string | null; + retryAttempts?: number; + retryDelay?: number; + timeout?: number; +} + +const updateWebhookLastCalled = async (robotId: string, webhookId: string): Promise => { + try { + const robot = await Robot.findOne({ where: { 'recording_meta.id': robotId } }); + if (!robot || !robot.webhooks) { + return; + } + + const updatedWebhooks = robot.webhooks.map((w: WebhookConfig) => { + if (w.id === webhookId) { + return { + ...w, + lastCalledAt: new Date().toISOString() + }; + } + return w; + }); + + await robot.update({ webhooks: updatedWebhooks }); + } catch (error) { + console.error('Error updating webhook lastCalledAt:', error); + } +}; + +// Add new webhook +router.post('/add', requireSignIn, async (req: Request, res: Response) => { + const { webhook, robotId } = req.body; + const authenticatedReq = req as AuthenticatedRequest; + + try { + if (!authenticatedReq.user) { + return res.status(401).json({ ok: false, error: 'Unauthorized' }); + } + + if (!webhook || !robotId) { + return res.status(400).json({ ok: false, error: 'Webhook configuration and robot ID are required' }); + } + + if (!webhook.url) { + return res.status(400).json({ ok: false, error: 'Webhook URL is required' }); + } + + // Validate URL format + try { + new URL(webhook.url); + } catch (error) { + return res.status(400).json({ ok: false, error: 'Invalid webhook URL format' }); + } + + const robot = await Robot.findOne({ where: { 'recording_meta.id': robotId } }); + + if (!robot) { + return res.status(404).json({ ok: false, error: 'Robot not found' }); + } + + const currentWebhooks = robot.webhooks || []; + + const existingWebhook = currentWebhooks.find((w: WebhookConfig) => w.url === webhook.url); + if (existingWebhook) { + return res.status(400).json({ ok: false, error: 'Webhook with this url already exists' }); + } + + const newWebhook: WebhookConfig = { + ...webhook, + id: webhook.id || Date.now().toString(), + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + lastCalledAt: null, + retryAttempts: webhook.retryAttempts || 3, + retryDelay: webhook.retryDelay || 5, + timeout: webhook.timeout || 30, + }; + + const updatedWebhooks = [...currentWebhooks, newWebhook]; + + await robot.update({ webhooks: updatedWebhooks }); + + res.status(200).json({ + ok: true, + message: 'Webhook added successfully', + webhook: newWebhook + }); + } catch (error: any) { + console.log(`Could not add webhook - ${error}`); + res.status(500).json({ ok: false, error: 'Could not add webhook configuration' }); + } +}); + +// Update existing webhook +router.post('/update', requireSignIn, async (req: Request, res: Response) => { + const { webhook, robotId } = req.body; + const authenticatedReq = req as AuthenticatedRequest; + + try { + if (!authenticatedReq.user) { + return res.status(401).json({ ok: false, error: 'Unauthorized' }); + } + + if (!webhook || !robotId || !webhook.id) { + return res.status(400).json({ ok: false, error: 'Webhook configuration, webhook ID, and robot ID are required' }); + } + + // Validate URL format if provided + if (webhook.url) { + try { + new URL(webhook.url); + } catch (error) { + return res.status(400).json({ ok: false, error: 'Invalid webhook URL format' }); + } + } + + const robot = await Robot.findOne({ where: { 'recording_meta.id': robotId } }); + + if (!robot) { + return res.status(404).json({ ok: false, error: 'Robot not found' }); + } + + const currentWebhooks = robot.webhooks || []; + const webhookIndex = currentWebhooks.findIndex((w: WebhookConfig) => w.id === webhook.id); + + if (webhookIndex === -1) { + return res.status(404).json({ ok: false, error: 'Webhook not found' }); + } + + // Check for duplicate URLs (excluding current webhook) + const duplicateUrl = currentWebhooks.find((w: WebhookConfig, index: number) => + w.url === webhook.url && index !== webhookIndex + ); + if (duplicateUrl) { + return res.status(400).json({ ok: false, error: 'Webhook with this URL already exists' }); + } + + const updatedWebhook: WebhookConfig = { + ...currentWebhooks[webhookIndex], + ...webhook, + updatedAt: new Date().toISOString(), + lastCalledAt: currentWebhooks[webhookIndex].lastCalledAt + }; + + const updatedWebhooks = [...currentWebhooks]; + updatedWebhooks[webhookIndex] = updatedWebhook; + + await robot.update({ webhooks: updatedWebhooks }); + + res.status(200).json({ + ok: true, + message: 'Webhook updated successfully', + webhook: updatedWebhook + }); + } catch (error: any) { + console.log(`Could not update webhook - ${error}`); + res.status(500).json({ ok: false, error: 'Could not update webhook configuration' }); + } +}); + +// Remove webhook +router.post('/remove', requireSignIn, async (req: Request, res: Response) => { + const { webhookId, robotId } = req.body; + const authenticatedReq = req as AuthenticatedRequest; + + try { + if (!authenticatedReq.user) { + return res.status(401).json({ ok: false, error: 'Unauthorized' }); + } + + if (!webhookId || !robotId) { + return res.status(400).json({ ok: false, error: 'Webhook ID and robot ID are required' }); + } + + const robot = await Robot.findOne({ where: { 'recording_meta.id': robotId } }); + + if (!robot) { + return res.status(404).json({ ok: false, error: 'Robot not found' }); + } + + const currentWebhooks = robot.webhooks || []; + const webhookExists = currentWebhooks.find((w: WebhookConfig) => w.id === webhookId); + + if (!webhookExists) { + return res.status(404).json({ ok: false, error: 'Webhook not found' }); + } + + const updatedWebhooks = currentWebhooks.filter((w: WebhookConfig) => w.id !== webhookId); + + await robot.update({ webhooks: updatedWebhooks }); + + res.status(200).json({ + ok: true, + message: 'Webhook removed successfully' + }); + } catch (error: any) { + console.log(`Could not remove webhook - ${error}`); + res.status(500).json({ ok: false, error: 'Could not remove webhook configuration' }); + } +}); + +// Get all webhooks for a robot +router.get('/list/:robotId', requireSignIn, async (req: Request, res: Response) => { + const { robotId } = req.params; + const authenticatedReq = req as AuthenticatedRequest; + + try { + if (!authenticatedReq.user) { + return res.status(401).json({ ok: false, error: 'Unauthorized' }); + } + + const robot = await Robot.findOne({ + where: { 'recording_meta.id': robotId }, + attributes: ['webhooks'] + }); + + if (!robot) { + return res.status(404).json({ ok: false, error: 'Robot not found' }); + } + + const webhooks = robot.webhooks || []; + + res.status(200).json({ + ok: true, + webhooks: webhooks + }); + } catch (error: any) { + console.log(`Could not retrieve webhooks - ${error}`); + res.status(500).json({ ok: false, error: 'Could not retrieve webhook configurations' }); + } +}); + +// Test webhook endpoint +router.post('/test', requireSignIn, async (req: Request, res: Response) => { + const { webhook, robotId } = req.body; + const authenticatedReq = req as AuthenticatedRequest; + + try { + if (!authenticatedReq.user) { + return res.status(401).json({ ok: false, error: 'Unauthorized' }); + } + + if (!webhook || !robotId) { + return res.status(400).json({ ok: false, error: 'Webhook configuration and robot ID are required' }); + } + + const robot = await Robot.findOne({ where: { 'recording_meta.id': robotId } }); + + if (!robot) { + return res.status(404).json({ ok: false, error: 'Robot not found' }); + } + + // Create test payload + const testPayload = { + event_type: "webhook_test", + timestamp: new Date().toISOString(), + webhook_id: webhook.id, + data: { + workflow_id: robotId, + run_id: "110c4dae-c39b-4b30-a932-eff1022e4bb0", + robot_name: robot.recording_meta?.name || "Unknown Robot", + status: "test", + started_at: new Date().toISOString(), + finished_at: new Date().toISOString(), + execution_time_ms: 5000, + extracted_data: { + schema_items: { + "item-0": { title: "Test Item 1", value: "Sample Value 1" }, + "item-1": { title: "Test Item 2", value: "Sample Value 2" } + }, + list_items: { + "item-0": [ + { name: "List Item 1", price: "$10.99" }, + { name: "List Item 2", price: "$15.99" } + ] + }, + total_rows: 4, + schema_count: 2, + list_count: 2, + screenshots_count: 3 + }, + metadata: { + test_mode: true, + browser_id: "d27ace57-75cb-441c-8589-8ba34e52f7d1", + user_id: "108" + } + } + }; + + await updateWebhookLastCalled(robotId, webhook.id); + + const response = await axios.post(webhook.url, testPayload, { + timeout: (webhook.timeout || 30) * 1000, + validateStatus: (status) => status < 500 + }); + + const success = response.status >= 200 && response.status < 300; + + res.status(200).json({ + ok: true, + message: success ? 'Test webhook sent successfully' : 'Webhook endpoint responded with non-success status', + details: { + status: response.status, + statusText: response.statusText, + success: success + } + }); + } catch (error: any) { + console.log(`Could not test webhook - ${error}`); + + try { + await updateWebhookLastCalled(robotId, webhook.id); + } catch (updateError) { + console.error('Failed to update lastCalledAt after webhook error:', updateError); + } + + let errorMessage = 'Could not send test webhook'; + if (error.code === 'ECONNREFUSED') { + errorMessage = 'Connection refused - webhook URL is not accessible'; + } else if (error.code === 'ETIMEDOUT') { + errorMessage = 'Request timeout - webhook endpoint did not respond in time'; + } else if (error.response) { + errorMessage = `Webhook endpoint responded with error: ${error.response.status} ${error.response.statusText}`; + } + + res.status(500).json({ + ok: false, + error: errorMessage, + details: { + code: error.code, + message: error.message + } + }); + } +}); + +// Send webhook +export const sendWebhook = async (robotId: string, eventType: string, data: any): Promise => { + try { + const robot = await Robot.findOne({ where: { 'recording_meta.id': robotId } }); + if (!robot || !robot.webhooks) { + return; + } + + const activeWebhooks = robot.webhooks.filter((w: WebhookConfig) => + w.active && w.events.includes(eventType) + ); + + if (activeWebhooks.length === 0) { + return; + } + + const webhookPromises = activeWebhooks.map(async (webhook: WebhookConfig) => { + const payload = { + event_type: eventType, + timestamp: new Date().toISOString(), + webhook_id: webhook.id, + data: data + }; + + return sendWebhookWithRetry(robotId, webhook, payload); + }); + + await Promise.allSettled(webhookPromises); + } catch (error) { + console.error('Error sending webhooks:', error); + } +}; + +// Helper function to send webhook with retry logic +const sendWebhookWithRetry = async (robotId: string, webhook: WebhookConfig, payload: any, attempt: number = 1): Promise => { + const maxRetries = webhook.retryAttempts || 3; + const retryDelay = webhook.retryDelay || 5; + const timeout = webhook.timeout || 30; + + try { + await updateWebhookLastCalled(robotId, webhook.id); + + const response = await axios.post(webhook.url, payload, { + timeout: timeout * 1000, + validateStatus: (status) => status >= 200 && status < 300 + }); + + console.log(`Webhook sent successfully to ${webhook.url}: ${response.status}`); + } catch (error: any) { + console.error(`Webhook failed for ${webhook.url} (attempt ${attempt}):`, error.message); + + if (attempt < maxRetries) { + const delay = retryDelay * Math.pow(2, attempt - 1); + console.log(`Retrying webhook ${webhook.url} in ${delay} seconds...`); + + setTimeout(async () => { + await sendWebhookWithRetry(robotId, webhook, payload, attempt + 1); + }, delay * 1000); + } else { + console.error(`Webhook ${webhook.url} failed after ${maxRetries} attempts`); + } + } +}; + +// Clear all webhooks for a robot +router.delete('/clear/:robotId', requireSignIn, async (req: Request, res: Response) => { + const { robotId } = req.params; + const authenticatedReq = req as AuthenticatedRequest; + + try { + if (!authenticatedReq.user) { + return res.status(401).json({ ok: false, error: 'Unauthorized' }); + } + + const robot = await Robot.findOne({ where: { 'recording_meta.id': robotId } }); + + if (!robot) { + return res.status(404).json({ ok: false, error: 'Robot not found' }); + } + + await robot.update({ webhooks: [] }); + + res.status(200).json({ + ok: true, + message: 'All webhooks cleared successfully' + }); + } catch (error: any) { + console.log(`Could not clear webhooks - ${error}`); + res.status(500).json({ ok: false, error: 'Could not clear webhook configurations' }); + } +}); \ No newline at end of file From 96df256f3f193d0cb8fde041fe4a081b26308e0c Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 27 May 2025 22:12:37 +0530 Subject: [PATCH 09/26] feat: add webhook api functions --- src/api/webhook.ts | 149 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 src/api/webhook.ts diff --git a/src/api/webhook.ts b/src/api/webhook.ts new file mode 100644 index 00000000..ad5c75dc --- /dev/null +++ b/src/api/webhook.ts @@ -0,0 +1,149 @@ +import { default as axios } from "axios"; +import { apiUrl } from "../apiConfig"; + +export interface WebhookConfig { + id: string; + url: string; + events: string[]; + active: boolean; + createdAt?: string; + updatedAt?: string; + lastCalledAt?: string | null; + retryAttempts?: number; + retryDelay?: number; + timeout?: number; +} + +export interface WebhookResponse { + ok: boolean; + message?: string; + webhook?: WebhookConfig; + webhooks?: WebhookConfig[]; + error?: string; + details?: any; +} + +export const addWebhook = async (webhook: WebhookConfig, robotId: string): Promise => { + try { + const response = await axios.post(`${apiUrl}/webhook/add`, { + webhook, + robotId + }, { withCredentials: true }); + + if (response.status === 200) { + return response.data; + } else { + throw new Error(`Failed to add webhook. Status code: ${response.status}`); + } + } catch (error: any) { + console.error('Error adding webhook:', error.message || error); + return { + ok: false, + error: error.response?.data?.message || error.message || 'Failed to add webhook' + }; + } +}; + +export const updateWebhook = async (webhook: WebhookConfig, robotId: string): Promise => { + try { + const response = await axios.post(`${apiUrl}/webhook/update`, { + webhook, + robotId + }, { withCredentials: true }); + + if (response.status === 200) { + return response.data; + } else { + throw new Error(`Failed to update webhook. Status code: ${response.status}`); + } + } catch (error: any) { + console.error('Error updating webhook:', error.message || error); + return { + ok: false, + error: error.response?.data?.message || error.message || 'Failed to update webhook' + }; + } +}; + +export const removeWebhook = async (webhookId: string, robotId: string): Promise => { + try { + const response = await axios.post(`${apiUrl}/webhook/remove`, { + webhookId, + robotId + }, { withCredentials: true }); + + if (response.status === 200) { + return response.data; + } else { + throw new Error(`Failed to remove webhook. Status code: ${response.status}`); + } + } catch (error: any) { + console.error('Error removing webhook:', error.message || error); + return { + ok: false, + error: error.response?.data?.message || error.message || 'Failed to remove webhook' + }; + } +}; + +export const getWebhooks = async (robotId: string): Promise => { + try { + const response = await axios.get(`${apiUrl}/webhook/list/${robotId}`, { + withCredentials: true + }); + + if (response.status === 200) { + return response.data; + } else { + throw new Error(`Failed to fetch webhooks. Status code: ${response.status}`); + } + } catch (error: any) { + console.error('Error fetching webhooks:', error.message || error); + return { + ok: false, + error: error.response?.data?.message || error.message || 'Failed to fetch webhooks', + webhooks: [] + }; + } +}; + +export const testWebhook = async (webhook: WebhookConfig, robotId: string): Promise => { + try { + const response = await axios.post(`${apiUrl}/webhook/test`, { + webhook, + robotId + }, { withCredentials: true }); + + if (response.status === 200) { + return response.data; + } else { + throw new Error(`Failed to test webhook. Status code: ${response.status}`); + } + } catch (error: any) { + console.error('Error testing webhook:', error.message || error); + return { + ok: false, + error: error.response?.data?.message || error.message || 'Failed to test webhook' + }; + } +}; + +export const clearAllWebhooks = async (robotId: string): Promise => { + try { + const response = await axios.delete(`${apiUrl}/webhook/clear/${robotId}`, { + withCredentials: true + }); + + if (response.status === 200) { + return response.data; + } else { + throw new Error(`Failed to clear webhooks. Status code: ${response.status}`); + } + } catch (error: any) { + console.error('Error clearing webhooks:', error.message || error); + return { + ok: false, + error: error.response?.data?.message || error.message || 'Failed to clear webhooks' + }; + } +}; \ No newline at end of file From 13f8e95c8f987f1f9d2eb206546cd49c99ede942 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 27 May 2025 22:13:21 +0530 Subject: [PATCH 10/26] feat: use webhook api functions --- .../integration/IntegrationSettings.tsx | 231 +++++++++--------- 1 file changed, 121 insertions(+), 110 deletions(-) diff --git a/src/components/integration/IntegrationSettings.tsx b/src/components/integration/IntegrationSettings.tsx index 09cabbd8..607ff6e5 100644 --- a/src/components/integration/IntegrationSettings.tsx +++ b/src/components/integration/IntegrationSettings.tsx @@ -35,6 +35,8 @@ import Cookies from "js-cookie"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; +import { addWebhook, updateWebhook, removeWebhook, getWebhooks, testWebhook,WebhookConfig } from "../../api/webhook"; + interface IntegrationProps { isOpen: boolean; handleStart: (data: IntegrationSettings) => void; @@ -42,14 +44,6 @@ interface IntegrationProps { preSelectedIntegrationType?: "googleSheets" | "airtable" | "webhook" | null; } -export interface WebhookConfig { - id: string; - url: string; - events: string[]; - active: boolean; - lastCalledAt?: string | null; -} - export interface IntegrationSettings { spreadsheetId?: string; spreadsheetName?: string; @@ -156,7 +150,30 @@ export const IntegrationSettingsModal = ({ return true; }; - const addWebhook = async () => { + const fetchWebhooks = async () => { + try { + setLoading(true); + if (!recordingId) return; + + const response = await getWebhooks(recordingId); + + if (response.ok && response.webhooks) { + setSettings(prev => ({ + ...prev, + webhooks: response.webhooks + })); + } else { + notify("error", response.error || "Failed to fetch webhooks"); + } + setLoading(false); + } catch (error: any) { + setLoading(false); + console.error("Error fetching webhooks:", error); + notify("error", "Failed to fetch webhooks"); + } + }; + + const addWebhookSetting = async () => { if (!validateWebhookData(newWebhook.url, newWebhook.events)) { if (!newWebhook.url) { notify("error", "Please provide webhook URL"); @@ -166,6 +183,8 @@ export const IntegrationSettingsModal = ({ return; } + if (!recordingId) return; + try { setLoading(true); const webhookWithId = { @@ -173,31 +192,28 @@ export const IntegrationSettingsModal = ({ id: Date.now().toString(), }; - const response = await axios.post( - `${apiUrl}/webhook/add`, - { - webhook: webhookWithId, - robotId: recordingId, - }, - { withCredentials: true } - ); + const response = await addWebhook(webhookWithId, recordingId); - const updatedWebhooks = [...(settings.webhooks || []), webhookWithId]; - setSettings({ ...settings, webhooks: updatedWebhooks }); - - resetWebhookForm(); - await refreshRecordingData(); - notify("success", "Webhook added successfully"); + if (response.ok) { + const updatedWebhooks = [...(settings.webhooks || []), webhookWithId]; + setSettings({ ...settings, webhooks: updatedWebhooks }); + + resetWebhookForm(); + await refreshRecordingData(); + notify("success", "Webhook added successfully"); + } else { + notify("error", response.error || "Failed to add webhook"); + } setLoading(false); } catch (error: any) { setLoading(false); console.log("Error adding webhook:", error); - notify("error", `Error adding webhook: ${error.response?.data?.message || error.message}`); + notify("error", "Failed to add webhook"); } }; - const updateWebhook = async () => { - if (!editingWebhook) return; + const updateWebhookSetting = async () => { + if (!editingWebhook || !recordingId) return; if (!validateWebhookData(newWebhook.url, newWebhook.events, editingWebhook)) { if (!newWebhook.url) { @@ -210,112 +226,106 @@ export const IntegrationSettingsModal = ({ try { setLoading(true); - await axios.post( - `${apiUrl}/webhook/update`, - { - webhook: newWebhook, - robotId: recordingId, - }, - { withCredentials: true } - ); + const response = await updateWebhook(newWebhook, recordingId); - const updatedWebhooks = (settings.webhooks || []).map(w => - w.id === editingWebhook ? newWebhook : w - ); - setSettings({ ...settings, webhooks: updatedWebhooks }); + if (response.ok) { + const updatedWebhooks = (settings.webhooks || []).map(w => + w.id === editingWebhook ? newWebhook : w + ); + setSettings({ ...settings, webhooks: updatedWebhooks }); - resetWebhookForm(); - await refreshRecordingData(); - notify("success", "Webhook updated successfully"); + resetWebhookForm(); + await refreshRecordingData(); + notify("success", "Webhook updated successfully"); + } else { + notify("error", response.error || "Failed to update webhook"); + } setLoading(false); } catch (error: any) { setLoading(false); console.error("Error updating webhook:", error); - notify("error", `Error updating webhook: ${error.response?.data?.message || error.message}`); + notify("error", "Failed to update webhook"); } }; - const removeWebhook = async (webhookId: string) => { + const removeWebhookSetting = async (webhookId: string) => { + if (!recordingId) return; + try { setLoading(true); - await axios.post( - `${apiUrl}/webhook/remove`, - { - webhookId, - robotId: recordingId, - }, - { withCredentials: true } - ); + const response = await removeWebhook(webhookId, recordingId); - const updatedWebhooks = (settings.webhooks || []).filter(w => w.id !== webhookId); - setSettings({ ...settings, webhooks: updatedWebhooks }); + if (response.ok) { + const updatedWebhooks = (settings.webhooks || []).filter(w => w.id !== webhookId); + setSettings({ ...settings, webhooks: updatedWebhooks }); - await refreshRecordingData(); - notify("success", "Webhook removed successfully"); + await refreshRecordingData(); + notify("success", "Webhook removed successfully"); + } else { + notify("error", response.error || "Failed to remove webhook"); + } setLoading(false); } catch (error: any) { setLoading(false); console.error("Error removing webhook:", error); - notify("error", `Error removing webhook: ${error.response?.data?.message || error.message}`); + notify("error", "Failed to remove webhook"); } }; - const toggleWebhookStatus = async (webhookId: string) => { + const toggleWebhookStatusSetting = async (webhookId: string) => { + if (!recordingId) return; + try { const webhook = settings.webhooks?.find(w => w.id === webhookId); if (!webhook) return; const updatedWebhook = { ...webhook, active: !webhook.active }; - await axios.post( - `${apiUrl}/webhook/update`, - { - webhook: updatedWebhook, - robotId: recordingId, - }, - { withCredentials: true } - ); + const response = await updateWebhook(updatedWebhook, recordingId); - const updatedWebhooks = (settings.webhooks || []).map(w => - w.id === webhookId ? updatedWebhook : w - ); - setSettings({ ...settings, webhooks: updatedWebhooks }); + if (response.ok) { + const updatedWebhooks = (settings.webhooks || []).map(w => + w.id === webhookId ? updatedWebhook : w + ); + setSettings({ ...settings, webhooks: updatedWebhooks }); - await refreshRecordingData(); - notify("success", `Webhook ${updatedWebhook.active ? "enabled" : "disabled"}`); + await refreshRecordingData(); + notify("success", `Webhook ${updatedWebhook.active ? "enabled" : "disabled"}`); + } else { + notify("error", response.error || "Failed to update webhook"); + } } catch (error: any) { console.error("Error toggling webhook status:", error); - notify("error", `Error updating webhook: ${error.response?.data?.message || error.message}`); + notify("error", "Failed to update webhook"); } }; - const testWebhook = async (webhook: WebhookConfig) => { + const testWebhookSetting = async (webhook: WebhookConfig) => { + if (!recordingId) return; + try { setLoading(true); - await axios.post( - `${apiUrl}/webhook/test`, - { - webhook, - robotId: recordingId, - }, - { withCredentials: true } - ); + const response = await testWebhook(webhook, recordingId); - const updatedWebhooks = (settings.webhooks || []).map(w => - w.id === webhook.id ? { ...w, lastCalledAt: new Date().toISOString() } : w - ); - setSettings({ ...settings, webhooks: updatedWebhooks }); + if (response.ok) { + const updatedWebhooks = (settings.webhooks || []).map(w => + w.id === webhook.id ? { ...w, lastCalledAt: new Date().toISOString() } : w + ); + setSettings({ ...settings, webhooks: updatedWebhooks }); - notify("success", "Test webhook sent successfully"); + notify("success", "Test webhook sent successfully"); + } else { + notify("error", response.error || "Failed to test webhook"); + } setLoading(false); } catch (error: any) { setLoading(false); console.error("Error testing webhook:", error); - notify("error", `Error testing webhook: ${error.response?.data?.message || error.message}`); + notify("error", "Failed to test webhook"); } }; - const editWebhook = (webhook: WebhookConfig) => { + const editWebhookSetting = (webhook: WebhookConfig) => { setNewWebhook(webhook); setEditingWebhook(webhook.id); setShowWebhookForm(true); @@ -430,6 +440,9 @@ export const IntegrationSettingsModal = ({ if (!recordingId) return null; const updatedRecording = await getStoredRecording(recordingId); setRecording(updatedRecording); + + await fetchWebhooks(); + setRerenderRobots(true); return updatedRecording; }; @@ -568,8 +581,7 @@ export const IntegrationSettingsModal = ({ if (preSelectedIntegrationType) { setSettings(prev => ({ ...prev, integrationType: preSelectedIntegrationType })); - } - else if (recording.google_sheet_id) { + } else if (recording.google_sheet_id) { setSettings(prev => ({ ...prev, integrationType: "googleSheets" })); } else if (recording.airtable_base_id) { setSettings(prev => ({ @@ -578,15 +590,18 @@ export const IntegrationSettingsModal = ({ airtableBaseName: recording.airtable_base_name || "", airtableTableName: recording.airtable_table_name || "", airtableTableId: recording.airtable_table_id || "", - integrationType: recording.airtable_base_id ? "airtable" : "googleSheets" - })); - } else if (recording.webhooks && recording.webhooks.length > 0) { - setSettings(prev => ({ - ...prev, - webhooks: recording.webhooks, - integrationType: "webhook" + integrationType: "airtable" })); } + + await fetchWebhooks(); + + if (!preSelectedIntegrationType && !recording.google_sheet_id && !recording.airtable_base_id) { + const webhookResponse = await getWebhooks(recordingId); + if (webhookResponse.ok && webhookResponse.webhooks && webhookResponse.webhooks.length > 0) { + setSettings(prev => ({ ...prev, integrationType: "webhook" })); + } + } } setLoading(false); @@ -617,8 +632,6 @@ export const IntegrationSettingsModal = ({ switch (event) { case "run_completed": return "Run finished"; - case "run_completed_success": - return "Run finished successfully"; case "run_failed": return "Run failed"; default: @@ -680,7 +693,7 @@ export const IntegrationSettingsModal = ({ }} style={{ display: "flex", flexDirection: "column", alignItems: "center", background: 'white', color: '#ff00c3' }} > - Google Sheets + Google Sheets Google Sheets @@ -693,7 +706,7 @@ export const IntegrationSettingsModal = ({ }} style={{ display: "flex", flexDirection: "column", alignItems: "center", background: 'white', color: '#ff00c3' }} > - Airtable + Airtable Airtable @@ -706,7 +719,7 @@ export const IntegrationSettingsModal = ({ }} style={{ display: "flex", flexDirection: "column", alignItems: "center", background: 'white', color: '#ff00c3' }} > - Webhook + Webhook Webhooks @@ -968,7 +981,7 @@ export const IntegrationSettingsModal = ({ toggleWebhookStatus(webhook.id)} + onChange={() => toggleWebhookStatusSetting(webhook.id)} size="small" /> @@ -976,7 +989,7 @@ export const IntegrationSettingsModal = ({ testWebhook(webhook)} + onClick={() => testWebhookSetting(webhook)} disabled={loading || !webhook.active} title="Test" > @@ -984,7 +997,7 @@ export const IntegrationSettingsModal = ({ editWebhook(webhook)} + onClick={() => editWebhookSetting(webhook)} disabled={loading} title="Edit" > @@ -992,7 +1005,7 @@ export const IntegrationSettingsModal = ({ removeWebhook(webhook.id)} + onClick={() => removeWebhookSetting(webhook.id)} disabled={loading} title="Delete" > @@ -1033,7 +1046,6 @@ export const IntegrationSettingsModal = ({ sx={{ minWidth: "200px" }} > Run finished - Run finished successfully Run failed @@ -1057,7 +1069,7 @@ export const IntegrationSettingsModal = ({ if (!validateWebhookData(newWebhook.url, newWebhook.events)) { return; } - addWebhook(); + addWebhookSetting(); }} disabled={!newWebhook.url || !newWebhook.events || newWebhook.events.length === 0 || loading || !!urlError} > @@ -1112,7 +1124,6 @@ export const IntegrationSettingsModal = ({ required > Run finished - Run finished successfully Run failed @@ -1132,7 +1143,7 @@ export const IntegrationSettingsModal = ({