diff --git a/apps/rowboat/app/actions/composio.actions.ts b/apps/rowboat/app/actions/composio.actions.ts index 15411014..36741199 100644 --- a/apps/rowboat/app/actions/composio.actions.ts +++ b/apps/rowboat/app/actions/composio.actions.ts @@ -13,6 +13,7 @@ import { ICreateComposioTriggerDeploymentController } from "@/src/interface-adap import { IListComposioTriggerDeploymentsController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/list-composio-trigger-deployments.controller"; import { IDeleteComposioTriggerDeploymentController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/delete-composio-trigger-deployment.controller"; import { IListComposioTriggerTypesController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/list-composio-trigger-types.controller"; +import { IFetchComposioTriggerDeploymentController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/fetch-composio-trigger-deployment.controller"; import { IDeleteComposioConnectedAccountController } from "@/src/interface-adapters/controllers/projects/delete-composio-connected-account.controller"; import { authCheck } from "./auth.actions"; import { ICreateComposioManagedConnectedAccountController } from "@/src/interface-adapters/controllers/projects/create-composio-managed-connected-account.controller"; @@ -26,6 +27,7 @@ const createComposioTriggerDeploymentController = container.resolve("listComposioTriggerDeploymentsController"); const deleteComposioTriggerDeploymentController = container.resolve("deleteComposioTriggerDeploymentController"); const listComposioTriggerTypesController = container.resolve("listComposioTriggerTypesController"); +const fetchComposioTriggerDeploymentController = container.resolve("fetchComposioTriggerDeploymentController"); const deleteComposioConnectedAccountController = container.resolve("deleteComposioConnectedAccountController"); const createComposioManagedConnectedAccountController = container.resolve("createComposioManagedConnectedAccountController"); const createCustomConnectedAccountController = container.resolve("createCustomConnectedAccountController"); @@ -182,4 +184,13 @@ export async function deleteComposioTriggerDeployment(request: { projectId: request.projectId, deploymentId: request.deploymentId, }); +} + +export async function fetchComposioTriggerDeployment(request: { deploymentId: string }) { + const user = await authCheck(); + return await fetchComposioTriggerDeploymentController.execute({ + caller: 'user', + userId: user._id, + deploymentId: request.deploymentId, + }); } \ No newline at end of file diff --git a/apps/rowboat/app/projects/[projectId]/job-rules/components/composio-trigger-deployment-view.tsx b/apps/rowboat/app/projects/[projectId]/job-rules/components/composio-trigger-deployment-view.tsx new file mode 100644 index 00000000..19d54c0c --- /dev/null +++ b/apps/rowboat/app/projects/[projectId]/job-rules/components/composio-trigger-deployment-view.tsx @@ -0,0 +1,171 @@ +'use client'; + +import { useEffect, useMemo, useState } from "react"; +import Link from "next/link"; +import { Spinner } from "@heroui/react"; +import { Panel } from "@/components/common/panel-common"; +import { Button } from "@/components/ui/button"; +import { ArrowLeftIcon, Trash2Icon } from "lucide-react"; +import { z } from "zod"; +import { ComposioTriggerDeployment } from "@/src/entities/models/composio-trigger-deployment"; +import { deleteComposioTriggerDeployment, fetchComposioTriggerDeployment } from "@/app/actions/composio.actions"; +import { JobsList } from "@/app/projects/[projectId]/jobs/components/jobs-list"; +import { JobFiltersSchema } from "@/src/application/repositories/jobs.repository.interface"; + +export function ComposioTriggerDeploymentView({ projectId, deploymentId }: { projectId: string; deploymentId: string; }) { + const [deployment, setDeployment] = useState | null>(null); + const [loading, setLoading] = useState(true); + const [deleting, setDeleting] = useState(false); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + + const jobsFilters = useMemo(() => ({ composioTriggerDeploymentId: deploymentId } satisfies z.infer), [deploymentId]); + + useEffect(() => { + let ignore = false; + (async () => { + setLoading(true); + try { + const res = await fetchComposioTriggerDeployment({ deploymentId }); + if (ignore) return; + setDeployment(res); + } finally { + if (!ignore) setLoading(false); + } + })(); + return () => { ignore = true; }; + }, [deploymentId]); + + const title = useMemo(() => { + if (!deployment) return 'External Trigger'; + return `External Trigger ${deployment.id}`; + }, [deployment]); + + const formatDate = (iso: string) => new Date(iso).toLocaleString(); + + const handleDelete = async () => { + if (!deployment) return; + setDeleting(true); + try { + await deleteComposioTriggerDeployment({ projectId, deploymentId: deployment.id }); + window.location.href = `/projects/${projectId}/job-rules`; + } catch (e) { + console.error(e); + alert('Failed to delete trigger'); + } finally { + setDeleting(false); + setShowDeleteConfirm(false); + } + }; + + return ( + <> + + + + +
{title}
+ + } + rightActions={ +
+ +
+ } + > +
+
+ {loading && ( +
+ +
Loading...
+
+ )} + {!loading && deployment && ( +
+
+
+
+ Deployment ID: + {deployment.id} +
+
+ Trigger Type: + {deployment.triggerTypeSlug} +
+
+ Toolkit: + {deployment.toolkitSlug} +
+
+ Connected Account: + {deployment.connectedAccountId} +
+
+ Created: + {formatDate(deployment.createdAt)} +
+
+ Updated: + {formatDate(deployment.updatedAt)} +
+
+ Trigger Config: +
+{JSON.stringify(deployment.triggerConfig, null, 2)}
+                                            
+
+
+
+ +
+

Jobs Created by This Trigger

+ +
+
+ )} + {!loading && !deployment && ( +
+
Trigger deployment not found.
+
+ )} +
+
+
+ + {showDeleteConfirm && ( +
+
+

Delete External Trigger

+

Are you sure you want to delete this external trigger? This will remove the linked webhook in Composio and delete this deployment.

+
+ + +
+
+
+ )} + + ); +} + + diff --git a/apps/rowboat/app/projects/[projectId]/job-rules/components/triggers-tab.tsx b/apps/rowboat/app/projects/[projectId]/job-rules/components/triggers-tab.tsx index ec0f2922..2ed1c3f3 100644 --- a/apps/rowboat/app/projects/[projectId]/job-rules/components/triggers-tab.tsx +++ b/apps/rowboat/app/projects/[projectId]/job-rules/components/triggers-tab.tsx @@ -1,7 +1,7 @@ 'use client'; import React, { useState, useEffect, useCallback, useMemo } from 'react'; -import { Spinner } from '@heroui/react'; +import { Spinner, Link } from '@heroui/react'; import { Button } from '@/components/ui/button'; import { Panel } from '@/components/common/panel-common'; import { Plus, Trash2, ZapIcon, ChevronDown, ChevronUp } from 'lucide-react'; @@ -350,22 +350,24 @@ export function TriggersTab({ projectId }: { projectId: string }) { >
-
- - Active - - - {triggerTypeNames[trigger.triggerTypeSlug] || trigger.triggerTypeSlug} - -
-
- Created: {new Date(trigger.createdAt).toLocaleDateString()} -
- {Object.keys(trigger.triggerConfig).length > 0 && ( -
+
)} diff --git a/apps/rowboat/app/projects/[projectId]/job-rules/triggers/[deploymentId]/page.tsx b/apps/rowboat/app/projects/[projectId]/job-rules/triggers/[deploymentId]/page.tsx new file mode 100644 index 00000000..b64b2787 --- /dev/null +++ b/apps/rowboat/app/projects/[projectId]/job-rules/triggers/[deploymentId]/page.tsx @@ -0,0 +1,19 @@ +import { Metadata } from "next"; +import { requireActiveBillingSubscription } from '@/app/lib/billing'; +import { ComposioTriggerDeploymentView } from "../../components/composio-trigger-deployment-view"; + +export const metadata: Metadata = { + title: "External Trigger", +}; + +export default async function Page( + props: { + params: Promise<{ projectId: string; deploymentId: string }> + } +) { + const params = await props.params; + await requireActiveBillingSubscription(); + return ; +} + + diff --git a/apps/rowboat/app/projects/[projectId]/jobs/components/job-view.tsx b/apps/rowboat/app/projects/[projectId]/jobs/components/job-view.tsx index a41be6fe..c39b941e 100644 --- a/apps/rowboat/app/projects/[projectId]/jobs/components/job-view.tsx +++ b/apps/rowboat/app/projects/[projectId]/jobs/components/job-view.tsx @@ -55,7 +55,7 @@ export function JobView({ projectId, jobId }: { projectId: string; jobId: string 'Deployment ID': reason.triggerDeploymentId, }, payload: reason.payload, - link: null + link: reason.triggerDeploymentId ? `/projects/${projectId}/job-rules/triggers/${reason.triggerDeploymentId}` : null }; } if (reason.type === 'scheduled_job_rule') { diff --git a/apps/rowboat/app/projects/[projectId]/jobs/components/jobs-list.tsx b/apps/rowboat/app/projects/[projectId]/jobs/components/jobs-list.tsx index a1d5f445..36b9d06d 100644 --- a/apps/rowboat/app/projects/[projectId]/jobs/components/jobs-list.tsx +++ b/apps/rowboat/app/projects/[projectId]/jobs/components/jobs-list.tsx @@ -99,7 +99,7 @@ export function JobsList({ projectId, filters, showTitle = true, customTitle }: return { type: 'Composio Trigger', display: `Composio: ${reason.triggerTypeSlug}`, - link: null + link: reason.triggerDeploymentId ? `/projects/${projectId}/job-rules/triggers/${reason.triggerDeploymentId}` : null }; } if (reason.type === 'scheduled_job_rule') { diff --git a/apps/rowboat/di/container.ts b/apps/rowboat/di/container.ts index 294a3158..f2af8a95 100644 --- a/apps/rowboat/di/container.ts +++ b/apps/rowboat/di/container.ts @@ -23,6 +23,7 @@ import { MongodbProjectsRepository } from "@/src/infrastructure/repositories/mon import { MongodbComposioTriggerDeploymentsRepository } from "@/src/infrastructure/repositories/mongodb.composio-trigger-deployments.repository"; import { CreateComposioTriggerDeploymentUseCase } from "@/src/application/use-cases/composio-trigger-deployments/create-composio-trigger-deployment.use-case"; import { ListComposioTriggerDeploymentsUseCase } from "@/src/application/use-cases/composio-trigger-deployments/list-composio-trigger-deployments.use-case"; +import { FetchComposioTriggerDeploymentUseCase } from "@/src/application/use-cases/composio-trigger-deployments/fetch-composio-trigger-deployment.use-case"; import { DeleteComposioTriggerDeploymentUseCase } from "@/src/application/use-cases/composio-trigger-deployments/delete-composio-trigger-deployment.use-case"; import { ListComposioTriggerTypesUseCase } from "@/src/application/use-cases/composio-trigger-deployments/list-composio-trigger-types.use-case"; import { HandleCompsioWebhookRequestUseCase } from "@/src/application/use-cases/composio/webhook/handle-composio-webhook-request.use-case"; @@ -30,6 +31,7 @@ import { MongoDBJobsRepository } from "@/src/infrastructure/repositories/mongodb import { CreateComposioTriggerDeploymentController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/create-composio-trigger-deployment.controller"; import { DeleteComposioTriggerDeploymentController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/delete-composio-trigger-deployment.controller"; import { ListComposioTriggerDeploymentsController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/list-composio-trigger-deployments.controller"; +import { FetchComposioTriggerDeploymentController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/fetch-composio-trigger-deployment.controller"; import { ListComposioTriggerTypesController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/list-composio-trigger-types.controller"; import { HandleComposioWebhookRequestController } from "@/src/interface-adapters/controllers/composio/webhook/handle-composio-webhook-request.controller"; import { JobsWorker } from "@/src/application/workers/jobs.worker"; @@ -299,10 +301,12 @@ container.register({ listComposioTriggerTypesUseCase: asClass(ListComposioTriggerTypesUseCase).singleton(), createComposioTriggerDeploymentUseCase: asClass(CreateComposioTriggerDeploymentUseCase).singleton(), listComposioTriggerDeploymentsUseCase: asClass(ListComposioTriggerDeploymentsUseCase).singleton(), + fetchComposioTriggerDeploymentUseCase: asClass(FetchComposioTriggerDeploymentUseCase).singleton(), deleteComposioTriggerDeploymentUseCase: asClass(DeleteComposioTriggerDeploymentUseCase).singleton(), createComposioTriggerDeploymentController: asClass(CreateComposioTriggerDeploymentController).singleton(), deleteComposioTriggerDeploymentController: asClass(DeleteComposioTriggerDeploymentController).singleton(), listComposioTriggerDeploymentsController: asClass(ListComposioTriggerDeploymentsController).singleton(), + fetchComposioTriggerDeploymentController: asClass(FetchComposioTriggerDeploymentController).singleton(), listComposioTriggerTypesController: asClass(ListComposioTriggerTypesController).singleton(), // conversations diff --git a/apps/rowboat/src/application/use-cases/composio-trigger-deployments/fetch-composio-trigger-deployment.use-case.ts b/apps/rowboat/src/application/use-cases/composio-trigger-deployments/fetch-composio-trigger-deployment.use-case.ts new file mode 100644 index 00000000..ed0be2de --- /dev/null +++ b/apps/rowboat/src/application/use-cases/composio-trigger-deployments/fetch-composio-trigger-deployment.use-case.ts @@ -0,0 +1,62 @@ +import { NotFoundError } from '@/src/entities/errors/common'; +import { z } from "zod"; +import { IUsageQuotaPolicy } from '../../policies/usage-quota.policy.interface'; +import { IProjectActionAuthorizationPolicy } from '../../policies/project-action-authorization.policy'; +import { IComposioTriggerDeploymentsRepository } from '../../repositories/composio-trigger-deployments.repository.interface'; +import { ComposioTriggerDeployment } from '@/src/entities/models/composio-trigger-deployment'; + +const inputSchema = z.object({ + caller: z.enum(["user", "api"]), + userId: z.string().optional(), + apiKey: z.string().optional(), + deploymentId: z.string(), +}); + +export interface IFetchComposioTriggerDeploymentUseCase { + execute(request: z.infer): Promise>; +} + +export class FetchComposioTriggerDeploymentUseCase implements IFetchComposioTriggerDeploymentUseCase { + private readonly composioTriggerDeploymentsRepository: IComposioTriggerDeploymentsRepository; + private readonly usageQuotaPolicy: IUsageQuotaPolicy; + private readonly projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy; + + constructor({ + composioTriggerDeploymentsRepository, + usageQuotaPolicy, + projectActionAuthorizationPolicy, + }: { + composioTriggerDeploymentsRepository: IComposioTriggerDeploymentsRepository, + usageQuotaPolicy: IUsageQuotaPolicy, + projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy, + }) { + this.composioTriggerDeploymentsRepository = composioTriggerDeploymentsRepository; + this.usageQuotaPolicy = usageQuotaPolicy; + this.projectActionAuthorizationPolicy = projectActionAuthorizationPolicy; + } + + async execute(request: z.infer): Promise> { + // fetch deployment first to get projectId + const deployment = await this.composioTriggerDeploymentsRepository.fetch(request.deploymentId); + if (!deployment) { + throw new NotFoundError(`Composio trigger deployment ${request.deploymentId} not found`); + } + + const { projectId } = deployment; + + // authz check + await this.projectActionAuthorizationPolicy.authorize({ + caller: request.caller, + userId: request.userId, + apiKey: request.apiKey, + projectId, + }); + + // assert and consume quota + await this.usageQuotaPolicy.assertAndConsume(projectId); + + return deployment; + } +} + + diff --git a/apps/rowboat/src/interface-adapters/controllers/composio-trigger-deployments/fetch-composio-trigger-deployment.controller.ts b/apps/rowboat/src/interface-adapters/controllers/composio-trigger-deployments/fetch-composio-trigger-deployment.controller.ts new file mode 100644 index 00000000..a630549b --- /dev/null +++ b/apps/rowboat/src/interface-adapters/controllers/composio-trigger-deployments/fetch-composio-trigger-deployment.controller.ts @@ -0,0 +1,44 @@ +import { BadRequestError } from "@/src/entities/errors/common"; +import z from "zod"; +import { IFetchComposioTriggerDeploymentUseCase } from "@/src/application/use-cases/composio-trigger-deployments/fetch-composio-trigger-deployment.use-case"; +import { ComposioTriggerDeployment } from "@/src/entities/models/composio-trigger-deployment"; + +const inputSchema = z.object({ + caller: z.enum(["user", "api"]), + userId: z.string().optional(), + apiKey: z.string().optional(), + deploymentId: z.string(), +}); + +export interface IFetchComposioTriggerDeploymentController { + execute(request: z.infer): Promise>; +} + +export class FetchComposioTriggerDeploymentController implements IFetchComposioTriggerDeploymentController { + private readonly fetchComposioTriggerDeploymentUseCase: IFetchComposioTriggerDeploymentUseCase; + + constructor({ + fetchComposioTriggerDeploymentUseCase, + }: { + fetchComposioTriggerDeploymentUseCase: IFetchComposioTriggerDeploymentUseCase, + }) { + this.fetchComposioTriggerDeploymentUseCase = fetchComposioTriggerDeploymentUseCase; + } + + async execute(request: z.infer): Promise> { + const result = inputSchema.safeParse(request); + if (!result.success) { + throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`); + } + const { caller, userId, apiKey, deploymentId } = result.data; + + return await this.fetchComposioTriggerDeploymentUseCase.execute({ + caller, + userId, + apiKey, + deploymentId, + }); + } +} + +