diff --git a/apps/rowboat/app/projects/[projectId]/config/components/project.tsx b/apps/rowboat/app/projects/[projectId]/config/components/project.tsx index 8645c736..bb4710ba 100644 --- a/apps/rowboat/app/projects/[projectId]/config/components/project.tsx +++ b/apps/rowboat/app/projects/[projectId]/config/components/project.tsx @@ -3,7 +3,7 @@ import { ReactNode, useEffect, useState, useCallback } from "react"; import { Spinner, Dropdown, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Input, useDisclosure } from "@heroui/react"; import { Button } from "@/components/ui/button"; -import { getProjectConfig, createApiKey, deleteApiKey, listApiKeys, deleteProject, rotateSecret, updateProjectName } from "../../../../actions/project.actions"; +import { getProjectConfig, createApiKey, deleteApiKey, listApiKeys, deleteProject, rotateSecret, updateProjectName, saveWorkflow } from "../../../../actions/project.actions"; import { CopyButton } from "../../../../../components/common/copy-button"; import { EyeIcon, EyeOffIcon, PlusIcon, Trash2Icon } from "lucide-react"; import { WithStringId } from "../../../../lib/types/types"; @@ -14,6 +14,13 @@ import { Label } from "../../../../lib/components/label"; import { sectionHeaderStyles, sectionDescriptionStyles } from './shared-styles'; import { clsx } from "clsx"; import { InputField } from "../../../../lib/components/input-field"; +import { Project, ComposioConnectedAccount } from "../../../../lib/types/project_types"; +import { getToolkit, listComposioTriggerDeployments, deleteComposioTriggerDeployment } from "../../../../actions/composio.actions"; +import { deleteConnectedAccount } from "../../../../actions/composio.actions"; +import { PictureImg } from "@/components/ui/picture-img"; +import { UnlinkIcon, AlertTriangle, Trash2 } from "lucide-react"; +import { ProjectWideChangeConfirmationModal } from "@/components/common/project-wide-change-confirmation-modal"; +import { Workflow } from "../../../../lib/types/workflow_types"; export function Section({ title, @@ -408,6 +415,273 @@ export function ChatWidgetSection({ projectId, chatWidgetHost }: { projectId: st ); } +interface ConnectedToolkit { + slug: string; + name: string; + logo: string; + connectedAccount: z.infer | null; +} + +function DisconnectToolkitsSection({ projectId, onProjectConfigUpdated }: { + projectId: string; + onProjectConfigUpdated?: () => void; +}) { + const [loading, setLoading] = useState(false); + const [connectedToolkits, setConnectedToolkits] = useState([]); + const [disconnectingToolkit, setDisconnectingToolkit] = useState(null); + const [showDisconnectModal, setShowDisconnectModal] = useState(false); + const [selectedToolkit, setSelectedToolkit] = useState(null); + + const loadConnectedToolkits = useCallback(async () => { + setLoading(true); + try { + const project = await getProjectConfig(projectId); + const connectedAccounts = project.composioConnectedAccounts || {}; + const workflow = project.draftWorkflow; + + // Get all connected accounts (both active and inactive) + const allConnections = Object.entries(connectedAccounts); + + // Get all Composio toolkits used in workflow tools (even if not connected) + const workflowToolkitSlugs = new Set(); + if (workflow?.tools) { + workflow.tools.forEach(tool => { + if (tool.isComposio && tool.composioData?.toolkitSlug) { + workflowToolkitSlugs.add(tool.composioData.toolkitSlug); + } + }); + } + + // Combine connected accounts and workflow toolkits + const allToolkitSlugs = new Set([ + ...allConnections.map(([slug]) => slug), + ...workflowToolkitSlugs + ]); + + // Fetch toolkit details for each toolkit + const toolkitPromises = Array.from(allToolkitSlugs).map(async (slug) => { + try { + const toolkit = await getToolkit(projectId, slug); + const connectedAccount = connectedAccounts[slug]; + + return { + slug, + name: toolkit.name, + logo: toolkit.meta.logo, + connectedAccount: connectedAccount || null // null if not connected + }; + } catch (error) { + console.error(`Failed to fetch toolkit ${slug}:`, error); + return null; + } + }); + + const toolkits = (await Promise.all(toolkitPromises)).filter(Boolean) as (ConnectedToolkit | ConnectedToolkit & { connectedAccount: null })[]; + setConnectedToolkits(toolkits); + } catch (error) { + console.error('Failed to load connected toolkits:', error); + } finally { + setLoading(false); + } + }, [projectId]); + + useEffect(() => { + loadConnectedToolkits(); + }, [loadConnectedToolkits]); + + const handleDisconnectClick = (toolkit: ConnectedToolkit) => { + setSelectedToolkit(toolkit); + setShowDisconnectModal(true); + }; + + + + const handleConfirmDisconnect = async () => { + if (!selectedToolkit) return; + + setDisconnectingToolkit(selectedToolkit.slug); + try { + // Step 1: Get current project and workflow + const project = await getProjectConfig(projectId); + const currentWorkflow = project.draftWorkflow; + + if (currentWorkflow) { + // Step 2: Remove all tools from this toolkit from the workflow + const updatedTools = currentWorkflow.tools.filter(tool => + !tool.isComposio || tool.composioData?.toolkitSlug !== selectedToolkit.slug + ); + + // Step 3: Update the workflow + const updatedWorkflow: z.infer = { + ...currentWorkflow, + tools: updatedTools + }; + + await saveWorkflow(projectId, updatedWorkflow); + } + + // Step 4: Delete all triggers for this toolkit + const triggers = await listComposioTriggerDeployments({ projectId }); + const toolkitTriggers = triggers.items.filter(trigger => trigger.toolkitSlug === selectedToolkit.slug); + + for (const trigger of toolkitTriggers) { + try { + await deleteComposioTriggerDeployment({ + projectId, + deploymentId: trigger.id + }); + } catch (error) { + console.error(`Failed to delete trigger ${trigger.id}:`, error); + // Continue with other triggers + } + } + + // Step 5: Disconnect the account (if connected) + if (selectedToolkit.connectedAccount) { + await deleteConnectedAccount( + projectId, + selectedToolkit.slug, + selectedToolkit.connectedAccount.id + ); + } + + // Remove from local state + setConnectedToolkits(prev => + prev.filter(toolkit => toolkit.slug !== selectedToolkit.slug) + ); + + // Notify parent of config update + onProjectConfigUpdated?.(); + } catch (error) { + console.error('Disconnect failed:', error); + } finally { + setDisconnectingToolkit(null); + setShowDisconnectModal(false); + setSelectedToolkit(null); + } + }; + + + + return ( + <> +
+
+ {loading ? ( + + ) : connectedToolkits.length > 0 ? ( +
+ {connectedToolkits.map((toolkit) => ( +
+
+
+ {toolkit.logo ? ( + + ) : ( +
+ + {toolkit.name.charAt(0).toUpperCase()} + +
+ )} +
+
+
+ {toolkit.name} +
+
+ {toolkit.connectedAccount?.status === 'ACTIVE' ? ( + + + Connected + + ) : toolkit.connectedAccount ? ( + + + Disconnected + + ) : ( + + + Not Connected + + )} +
+
+
+
+ {toolkit.connectedAccount?.status === 'ACTIVE' ? ( + + ) : toolkit.connectedAccount ? ( + + ) : ( + + )} + +
+
+ ))} +
+ ) : ( +
+ +

No toolkits found

+

Connect toolkits from the workflow editor or triggers to manage them here

+
+ )} +
+
+ + {/* Disconnect Confirmation Modal */} + { + setShowDisconnectModal(false); + setSelectedToolkit(null); + }} + onConfirm={handleConfirmDisconnect} + title={`Disconnect ${selectedToolkit?.name || 'Toolkit'}`} + confirmationQuestion={`Are you sure you want to disconnect the ${selectedToolkit?.name || 'toolkit'}? This will permanently remove all its tools, triggers, and connections. Your workflows may stop working properly if they depend on this toolkit.`} + confirmButtonText="Disconnect" + isLoading={disconnectingToolkit !== null} + /> + + + + ); +} + function DeleteProjectSection({ projectId }: { projectId: string }) { const [loadingInitial, setLoadingInitial] = useState(false); const [deletingProject, setDeletingProject] = useState(false); @@ -545,6 +819,7 @@ export function SimpleProjectSection({
+
);