mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-09 19:45:17 +02:00
commit
64f20ca9bf
27 changed files with 1152 additions and 599 deletions
|
|
@ -12,102 +12,65 @@ pip install rowboat
|
|||
|
||||
## Usage
|
||||
|
||||
### Basic Usage with StatefulChat
|
||||
### Basic Usage
|
||||
|
||||
The easiest way to interact with Rowboat is using the `StatefulChat` class, which maintains conversation state automatically:
|
||||
|
||||
```python
|
||||
from rowboat import Client, StatefulChat
|
||||
|
||||
# Initialize the client
|
||||
client = Client(
|
||||
host="<HOST>",
|
||||
project_id="<PROJECT_ID>",
|
||||
api_key="<API_KEY>"
|
||||
)
|
||||
|
||||
# Create a stateful chat session
|
||||
chat = StatefulChat(client)
|
||||
|
||||
# Have a conversation
|
||||
response = chat.run("What is the capital of France?")
|
||||
print(response)
|
||||
# The capital of France is Paris.
|
||||
|
||||
# Continue the conversation - the context is maintained automatically
|
||||
response = chat.run("What other major cities are in that country?")
|
||||
print(response)
|
||||
# Other major cities in France include Lyon, Marseille, Toulouse, and Nice.
|
||||
|
||||
response = chat.run("What's the population of the first city you mentioned?")
|
||||
print(response)
|
||||
# Lyon has a population of approximately 513,000 in the city proper.
|
||||
```
|
||||
|
||||
### Advanced Usage
|
||||
|
||||
#### Using a specific workflow
|
||||
|
||||
You can specify a workflow ID to use a particular conversation configuration:
|
||||
|
||||
```python
|
||||
chat = StatefulChat(
|
||||
client,
|
||||
workflow_id="<WORKFLOW_ID>"
|
||||
)
|
||||
```
|
||||
|
||||
#### Using a test profile
|
||||
|
||||
You can specify a test profile ID to use a specific test configuration:
|
||||
|
||||
```python
|
||||
chat = StatefulChat(
|
||||
client,
|
||||
test_profile_id="<TEST_PROFILE_ID>"
|
||||
)
|
||||
```
|
||||
|
||||
#### Tool overrides
|
||||
|
||||
You can provide tool override instructions to test a specific configuration:
|
||||
|
||||
```python
|
||||
chat = StatefulChat(
|
||||
client,
|
||||
mock_tools={
|
||||
"weather_lookup": "The weather in any city is sunny and 25°C.",
|
||||
"calculator": "The result of any calculation is 42.",
|
||||
"search": "Search results for any query return 'No relevant information found.'"
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Low-Level Usage
|
||||
|
||||
For more control over the conversation, you can use the `Client` class directly:
|
||||
The main way to interact with Rowboat is using the `Client` class, which provides a stateless chat API. You can manage conversation state using the `conversationId` returned in each response.
|
||||
|
||||
```python
|
||||
from rowboat.client import Client
|
||||
from rowboat.schema import UserMessage
|
||||
|
||||
# Initialize the client
|
||||
client = Client(
|
||||
host="<HOST>",
|
||||
project_id="<PROJECT_ID>",
|
||||
api_key="<API_KEY>"
|
||||
projectId="<PROJECT_ID>",
|
||||
apiKey="<API_KEY>"
|
||||
)
|
||||
|
||||
# Create messages
|
||||
messages = [
|
||||
UserMessage(role='user', content="Hello, how are you?")
|
||||
]
|
||||
# Start a new conversation
|
||||
result = client.run_turn(
|
||||
messages=[
|
||||
UserMessage(role='user', content="list my github repos")
|
||||
]
|
||||
)
|
||||
print(result.turn.output[-1].content)
|
||||
print("Conversation ID:", result.conversationId)
|
||||
|
||||
# Get response
|
||||
response = client.chat(messages=messages)
|
||||
print(response.messages[-1].content)
|
||||
|
||||
# For subsequent messages, you need to manage the message history and state manually
|
||||
messages.extend(response.messages)
|
||||
messages.append(UserMessage(role='user', content="What's your name?"))
|
||||
response = client.chat(messages=messages, state=response.state)
|
||||
# Continue the conversation by passing the conversationId
|
||||
result = client.run_turn(
|
||||
messages=[
|
||||
UserMessage(role='user', content="how many did you find?")
|
||||
],
|
||||
conversationId=result.conversationId
|
||||
)
|
||||
print(result.turn.output[-1].content)
|
||||
```
|
||||
|
||||
### Using Tool Overrides (Mock Tools)
|
||||
|
||||
You can provide tool override instructions to test a specific configuration using the `mockTools` argument:
|
||||
|
||||
```python
|
||||
result = client.run_turn(
|
||||
messages=[
|
||||
UserMessage(role='user', content="What's the weather?")
|
||||
],
|
||||
mockTools={
|
||||
"weather_lookup": "The weather in any city is sunny and 25°C.",
|
||||
"calculator": "The result of any calculation is 42."
|
||||
}
|
||||
)
|
||||
print(result.turn.output[-1].content)
|
||||
```
|
||||
|
||||
### Message Types
|
||||
|
||||
You can use different message types as defined in `rowboat.schema`, such as `UserMessage`, `SystemMessage`, etc. See `schema.py` for all available message types.
|
||||
|
||||
### Error Handling
|
||||
|
||||
If the API returns a non-200 status code, a `ValueError` will be raised with the error details.
|
||||
|
||||
---
|
||||
|
||||
For more advanced usage, see the docstrings in `client.py` and the message schemas in `schema.py`.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||
|
||||
[project]
|
||||
name = "rowboat"
|
||||
version = "5.0.0"
|
||||
version = "5.0.1"
|
||||
authors = [
|
||||
{ name = "Ramnique Singh", email = "ramnique@rowboatlabs.com" },
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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<ICreateCompo
|
|||
const listComposioTriggerDeploymentsController = container.resolve<IListComposioTriggerDeploymentsController>("listComposioTriggerDeploymentsController");
|
||||
const deleteComposioTriggerDeploymentController = container.resolve<IDeleteComposioTriggerDeploymentController>("deleteComposioTriggerDeploymentController");
|
||||
const listComposioTriggerTypesController = container.resolve<IListComposioTriggerTypesController>("listComposioTriggerTypesController");
|
||||
const fetchComposioTriggerDeploymentController = container.resolve<IFetchComposioTriggerDeploymentController>("fetchComposioTriggerDeploymentController");
|
||||
const deleteComposioConnectedAccountController = container.resolve<IDeleteComposioConnectedAccountController>("deleteComposioConnectedAccountController");
|
||||
const createComposioManagedConnectedAccountController = container.resolve<ICreateComposioManagedConnectedAccountController>("createComposioManagedConnectedAccountController");
|
||||
const createCustomConnectedAccountController = container.resolve<ICreateCustomConnectedAccountController>("createCustomConnectedAccountController");
|
||||
|
|
@ -133,7 +135,6 @@ export async function listComposioTriggerTypes(toolkitSlug: string, cursor?: str
|
|||
|
||||
export async function createComposioTriggerDeployment(request: {
|
||||
projectId: string,
|
||||
toolkitSlug: string,
|
||||
triggerTypeSlug: string,
|
||||
connectedAccountId: string,
|
||||
triggerConfig?: Record<string, unknown>,
|
||||
|
|
@ -144,9 +145,8 @@ export async function createComposioTriggerDeployment(request: {
|
|||
return await createComposioTriggerDeploymentController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
projectId: request.projectId,
|
||||
data: {
|
||||
projectId: request.projectId,
|
||||
toolkitSlug: request.toolkitSlug,
|
||||
triggerTypeSlug: request.triggerTypeSlug,
|
||||
connectedAccountId: request.connectedAccountId,
|
||||
triggerConfig: request.triggerConfig ?? {},
|
||||
|
|
@ -182,4 +182,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,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { Progress, Badge, Chip } from "@heroui/react";
|
||||
import { Progress, Badge, Chip, Spinner } from "@heroui/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/app/lib/components/label";
|
||||
import { Customer, UsageResponse } from "@/app/lib/types/billing_types";
|
||||
|
|
@ -11,6 +11,9 @@ import { HorizontalDivider } from "@/components/ui/horizontal-divider";
|
|||
import { WithStringId } from "@/app/lib/types/types";
|
||||
import clsx from 'clsx';
|
||||
import { getCustomerPortalUrl } from "../actions/billing.actions";
|
||||
import { useState } from "react";
|
||||
import { ArrowUpCircle } from "lucide-react";
|
||||
import { BillingUpgradeModal } from "@/components/common/billing-upgrade-modal";
|
||||
|
||||
const planDetails = {
|
||||
free: {
|
||||
|
|
@ -46,6 +49,14 @@ export function BillingPage({ customer, usage }: BillingPageProps) {
|
|||
const plan = customer.subscriptionPlan || "free";
|
||||
const displayStatus = getDisplayStatus(customer.subscriptionStatus);
|
||||
const planInfo = planDetails[plan];
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [upgradeModalOpen, setUpgradeModalOpen] = useState(false);
|
||||
const [upgradeError, setUpgradeError] = useState("");
|
||||
|
||||
// show friendly values for credits
|
||||
const sanctionedCredits = Math.floor(usage.sanctionedCredits / (10 ** 6));
|
||||
const availableCredits = Math.floor(usage.availableCredits / (10 ** 6));
|
||||
const usedCredits = Math.ceil((usage.sanctionedCredits - usage.availableCredits) / (10 ** 6));
|
||||
|
||||
// Prepare usage metrics data
|
||||
const usageData = Object.entries(usage.usage)
|
||||
|
|
@ -57,6 +68,7 @@ export function BillingPage({ customer, usage }: BillingPageProps) {
|
|||
.sort((a, b) => b.credits - a.credits);
|
||||
|
||||
async function handleManageSubscription() {
|
||||
setLoading(true);
|
||||
const returnUrl = new URL('/billing/callback', window.location.origin);
|
||||
returnUrl.searchParams.set('redirect', window.location.href);
|
||||
const url = await getCustomerPortalUrl(returnUrl.toString());
|
||||
|
|
@ -105,15 +117,34 @@ export function BillingPage({ customer, usage }: BillingPageProps) {
|
|||
</Chip>
|
||||
</div>
|
||||
</div>
|
||||
<form action={handleManageSubscription}>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="md"
|
||||
type="submit"
|
||||
<div className="flex flex-col items-end gap-2 min-w-[200px]">
|
||||
{(plan === "free" || plan === "starter") && (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="bg-gradient-to-r from-indigo-200 via-purple-200 to-pink-100 text-indigo-700 dark:text-indigo-200 shadow hover:from-indigo-300 hover:to-pink-200 rounded-md border border-indigo-200 dark:border-indigo-700"
|
||||
startContent={<ArrowUpCircle className="w-5 h-5" />}
|
||||
onClick={() => setUpgradeModalOpen(true)}
|
||||
>
|
||||
Upgrade Now
|
||||
</Button>
|
||||
)}
|
||||
{!loading && <a
|
||||
href="#"
|
||||
className="text-xs text-gray-500 underline hover:text-indigo-600 mt-1"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await handleManageSubscription();
|
||||
} catch (err) {
|
||||
setUpgradeError("Failed to open subscription portal");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Manage Subscription
|
||||
</Button>
|
||||
</form>
|
||||
</a>}
|
||||
{loading && <Spinner size="sm" />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -136,7 +167,7 @@ export function BillingPage({ customer, usage }: BillingPageProps) {
|
|||
tokens.colors.light.text.primary,
|
||||
tokens.colors.dark.text.primary
|
||||
)}>
|
||||
{usage.sanctionedCredits.toLocaleString()}
|
||||
{sanctionedCredits.toLocaleString()}
|
||||
</p>
|
||||
<p className={clsx(
|
||||
tokens.typography.sizes.sm,
|
||||
|
|
@ -154,7 +185,7 @@ export function BillingPage({ customer, usage }: BillingPageProps) {
|
|||
tokens.colors.light.text.primary,
|
||||
tokens.colors.dark.text.primary
|
||||
)}>
|
||||
{(usage.sanctionedCredits - usage.availableCredits).toLocaleString()}
|
||||
{usedCredits.toLocaleString()}
|
||||
</p>
|
||||
<p className={clsx(
|
||||
tokens.typography.sizes.sm,
|
||||
|
|
@ -174,7 +205,7 @@ export function BillingPage({ customer, usage }: BillingPageProps) {
|
|||
tokens.colors.dark.text.primary
|
||||
)
|
||||
)}>
|
||||
{usage.availableCredits.toLocaleString()}
|
||||
{availableCredits.toLocaleString()}
|
||||
</p>
|
||||
<p className={clsx(
|
||||
tokens.typography.sizes.sm,
|
||||
|
|
@ -237,7 +268,7 @@ export function BillingPage({ customer, usage }: BillingPageProps) {
|
|||
<section className="card">
|
||||
<div className="px-4 pt-4 pb-6">
|
||||
<SectionHeading>
|
||||
Usage data
|
||||
Usage split
|
||||
</SectionHeading>
|
||||
</div>
|
||||
<HorizontalDivider />
|
||||
|
|
@ -261,13 +292,13 @@ export function BillingPage({ customer, usage }: BillingPageProps) {
|
|||
<div className="flex justify-between items-center">
|
||||
<div className="space-y-1">
|
||||
<Label label={type.replace(/_/g, ' ')} />
|
||||
<p className={clsx(
|
||||
{/* <p className={clsx(
|
||||
tokens.typography.sizes.sm,
|
||||
tokens.colors.light.text.secondary,
|
||||
tokens.colors.dark.text.secondary
|
||||
)}>
|
||||
{credits.toLocaleString()} credits
|
||||
</p>
|
||||
</p> */}
|
||||
</div>
|
||||
<span className={clsx(
|
||||
tokens.typography.sizes.sm,
|
||||
|
|
@ -289,6 +320,11 @@ export function BillingPage({ customer, usage }: BillingPageProps) {
|
|||
)}
|
||||
</div>
|
||||
</section>
|
||||
<BillingUpgradeModal
|
||||
isOpen={upgradeModalOpen}
|
||||
onClose={() => setUpgradeModalOpen(false)}
|
||||
errorMessage={upgradeError}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<z.infer<typeof ComposioTriggerDeployment> | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
|
||||
const jobsFilters = useMemo(() => ({ composioTriggerDeploymentId: deploymentId } satisfies z.infer<typeof JobFiltersSchema>), [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 (
|
||||
<>
|
||||
<Panel
|
||||
title={
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href={`/projects/${projectId}/job-rules`}>
|
||||
<Button variant="secondary" size="sm">
|
||||
<ArrowLeftIcon className="w-4 h-4 mr-2" />
|
||||
Back
|
||||
</Button>
|
||||
</Link>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">{title}</div>
|
||||
</div>
|
||||
}
|
||||
rightActions={
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="flex items-center gap-2 bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-950 dark:hover:bg-red-900 dark:text-red-400 border border-red-200 dark:border-red-800"
|
||||
>
|
||||
<Trash2Icon className="w-4 h-4" />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="h-full overflow-auto px-4 py-4">
|
||||
<div className="max-w-[1024px] mx-auto">
|
||||
{loading && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Spinner size="sm" />
|
||||
<div>Loading...</div>
|
||||
</div>
|
||||
)}
|
||||
{!loading && deployment && (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="bg-gray-50 dark:bg-gray-800/50 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="font-semibold text-gray-700 dark:text-gray-300">Deployment ID:</span>
|
||||
<span className="ml-2 font-mono text-gray-600 dark:text-gray-400">{deployment.id}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold text-gray-700 dark:text-gray-300">Trigger Type:</span>
|
||||
<span className="ml-2 font-mono text-gray-600 dark:text-gray-400">{deployment.triggerTypeSlug}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold text-gray-700 dark:text-gray-300">Toolkit:</span>
|
||||
<span className="ml-2 font-mono text-gray-600 dark:text-gray-400">{deployment.toolkitSlug}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold text-gray-700 dark:text-gray-300">Connected Account:</span>
|
||||
<span className="ml-2 font-mono text-gray-600 dark:text-gray-400">{deployment.connectedAccountId}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold text-gray-700 dark:text-gray-300">Created:</span>
|
||||
<span className="ml-2 font-mono text-gray-600 dark:text-gray-400">{formatDate(deployment.createdAt)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold text-gray-700 dark:text-gray-300">Updated:</span>
|
||||
<span className="ml-2 font-mono text-gray-600 dark:text-gray-400">{formatDate(deployment.updatedAt)}</span>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<span className="font-semibold text-gray-700 dark:text-gray-300">Trigger Config:</span>
|
||||
<pre className="mt-2 bg-gray-100 dark:bg-gray-900 p-3 rounded text-xs overflow-x-auto border border-gray-200 dark:border-gray-700 font-mono">
|
||||
{JSON.stringify(deployment.triggerConfig, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-3">Jobs Created by This Trigger</h3>
|
||||
<JobsList projectId={projectId} filters={jobsFilters} showTitle={false} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!loading && !deployment && (
|
||||
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
<div className="text-sm font-mono">Trigger deployment not found.</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
{showDeleteConfirm && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md mx-4">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">Delete External Trigger</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">Are you sure you want to delete this external trigger? This will remove the linked webhook in Composio and delete this deployment.</p>
|
||||
<div className="flex gap-3 justify-end">
|
||||
<Button variant="secondary" onClick={() => setShowDeleteConfirm(false)} disabled={deleting}>Cancel</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleDelete}
|
||||
disabled={deleting}
|
||||
className="flex items-center gap-2 bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-950 dark:hover:bg-red-900 dark:text-red-400 border border-red-200 dark:border-red-800"
|
||||
>
|
||||
{deleting ? (<><Spinner size="sm" /> Deleting...</>) : (<><Trash2Icon className="w-4 h-4" /> Delete</>)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -4,9 +4,10 @@ import { useState } from "react";
|
|||
import { Tabs, Tab } from "@/components/ui/tabs";
|
||||
import { ScheduledJobRulesList } from "../scheduled/components/scheduled-job-rules-list";
|
||||
import { RecurringJobRulesList } from "./recurring-job-rules-list";
|
||||
import { TriggersTab } from "./triggers-tab";
|
||||
|
||||
export function JobRulesTabs({ projectId }: { projectId: string }) {
|
||||
const [activeTab, setActiveTab] = useState<string>("scheduled");
|
||||
const [activeTab, setActiveTab] = useState<string>("triggers");
|
||||
|
||||
const handleTabChange = (key: React.Key) => {
|
||||
setActiveTab(key.toString());
|
||||
|
|
@ -20,10 +21,13 @@ export function JobRulesTabs({ projectId }: { projectId: string }) {
|
|||
aria-label="Job Rules"
|
||||
fullWidth
|
||||
>
|
||||
<Tab key="scheduled" title="Scheduled Rules">
|
||||
<Tab key="triggers" title="External Triggers">
|
||||
<TriggersTab projectId={projectId} />
|
||||
</Tab>
|
||||
<Tab key="scheduled" title="One-Time Triggers">
|
||||
<ScheduledJobRulesList projectId={projectId} />
|
||||
</Tab>
|
||||
<Tab key="recurring" title="Recurring Rules">
|
||||
<Tab key="recurring" title="Recurring Triggers">
|
||||
<RecurringJobRulesList projectId={projectId} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|||
import { Link, Spinner } from "@heroui/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Panel } from "@/components/common/panel-common";
|
||||
import { listRecurringJobRules } from "@/app/actions/recurring-job-rules.actions";
|
||||
import { listRecurringJobRules, deleteRecurringJobRule } from "@/app/actions/recurring-job-rules.actions";
|
||||
import { z } from "zod";
|
||||
import { ListedRecurringRuleItem } from "@/src/application/repositories/recurring-job-rules.repository.interface";
|
||||
import { isToday, isThisWeek, isThisMonth } from "@/lib/utils/date";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { PlusIcon, Trash2 } from "lucide-react";
|
||||
|
||||
type ListedItem = z.infer<typeof ListedRecurringRuleItem>;
|
||||
|
||||
|
|
@ -18,6 +18,7 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
|
|||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [loadingMore, setLoadingMore] = useState<boolean>(false);
|
||||
const [hasMore, setHasMore] = useState<boolean>(false);
|
||||
const [deletingRule, setDeletingRule] = useState<string | null>(null);
|
||||
|
||||
const fetchPage = useCallback(async (cursorArg?: string | null) => {
|
||||
const res = await listRecurringJobRules({ projectId, cursor: cursorArg ?? undefined, limit: 20 });
|
||||
|
|
@ -48,6 +49,24 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
|
|||
setLoadingMore(false);
|
||||
}, [cursor, fetchPage]);
|
||||
|
||||
const handleDeleteRule = async (ruleId: string) => {
|
||||
if (!window.confirm('Are you sure you want to delete this recurring trigger?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setDeletingRule(ruleId);
|
||||
await deleteRecurringJobRule({ projectId, ruleId });
|
||||
// Remove the deleted item from the list
|
||||
setItems(prev => prev.filter(item => item.id !== ruleId));
|
||||
} catch (err: any) {
|
||||
console.error('Error deleting recurring trigger:', err);
|
||||
alert('Failed to delete recurring trigger. Please try again.');
|
||||
} finally {
|
||||
setDeletingRule(null);
|
||||
}
|
||||
};
|
||||
|
||||
const sections = useMemo(() => {
|
||||
const groups: Record<string, ListedItem[]> = {
|
||||
Today: [],
|
||||
|
|
@ -109,18 +128,15 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
|
|||
return (
|
||||
<Panel
|
||||
title={
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
RECURRING JOB RULES
|
||||
</div>
|
||||
<div className="text-base font-normal text-gray-900 dark:text-gray-100">
|
||||
Run your assistant workflow on an automated repeating schedule (cron jobs).
|
||||
</div>
|
||||
}
|
||||
rightActions={
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href={`/projects/${projectId}/job-rules/recurring/new`}>
|
||||
<Button size="sm" className="flex items-center gap-2">
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
New Rule
|
||||
<Button size="sm" className="whitespace-nowrap" startContent={<PlusIcon className="w-4 h-4" />}>
|
||||
New Recurring Trigger
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
|
@ -145,38 +161,48 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
|
|||
</h3>
|
||||
<div className="grid gap-3">
|
||||
{sectionItems.map((item) => (
|
||||
<Link
|
||||
<div
|
||||
key={item.id}
|
||||
href={`/projects/${projectId}/job-rules/recurring/${item.id}`}
|
||||
className="block p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className={`text-sm font-medium ${getStatusColor(item.disabled, item.lastError || null)}`}>
|
||||
{getStatusText(item.disabled, item.lastError || null)}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Next run: {formatNextRunAt(item.nextRunAt)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">
|
||||
Schedule: {formatCronExpression(item.cron)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Created: {new Date(item.createdAt).toLocaleDateString()}
|
||||
</div>
|
||||
{item.lastError && (
|
||||
<div className="text-sm text-red-600 dark:text-red-400 mt-1">
|
||||
Last error: {item.lastError}
|
||||
<Link
|
||||
href={`/projects/${projectId}/job-rules/recurring/${item.id}`}
|
||||
className="block"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className={`text-sm font-medium ${getStatusColor(item.disabled, item.lastError || null)}`}>
|
||||
{getStatusText(item.disabled, item.lastError || null)}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Next run: {formatNextRunAt(item.nextRunAt)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{new Date(item.createdAt).toLocaleDateString()}
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">
|
||||
Schedule: {formatCronExpression(item.cron)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Created: {new Date(item.createdAt).toLocaleDateString()}
|
||||
</div>
|
||||
{item.lastError && (
|
||||
<div className="text-sm text-red-600 dark:text-red-400 mt-1">
|
||||
Last error: {item.lastError}
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
size="sm"
|
||||
isLoading={deletingRule === item.id}
|
||||
onClick={() => handleDeleteRule(item.id)}
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:text-red-400 dark:hover:text-red-300 dark:hover:bg-red-950"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -184,7 +210,7 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
|
|||
})}
|
||||
{items.length === 0 && !loading && (
|
||||
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
No recurring job rules found. Create your first rule to get started.
|
||||
No recurring triggers yet. Create your first recurring trigger to get started.
|
||||
</div>
|
||||
)}
|
||||
{hasMore && (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,518 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useCallback, useMemo } from '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';
|
||||
import { z } from 'zod';
|
||||
import { ComposioTriggerDeployment } from '@/src/entities/models/composio-trigger-deployment';
|
||||
import { ComposioTriggerType } from '@/src/entities/models/composio-trigger-type';
|
||||
import { isToday, isThisWeek, isThisMonth } from '@/lib/utils/date';
|
||||
import { listComposioTriggerDeployments, deleteComposioTriggerDeployment, createComposioTriggerDeployment, listComposioTriggerTypes } from '@/app/actions/composio.actions';
|
||||
import { SelectComposioToolkit } from '../../tools/components/SelectComposioToolkit';
|
||||
import { ComposioTriggerTypesPanel } from '../../workflow/components/ComposioTriggerTypesPanel';
|
||||
import { TriggerConfigForm } from '../../workflow/components/TriggerConfigForm';
|
||||
import { ToolkitAuthModal } from '../../tools/components/ToolkitAuthModal';
|
||||
import { ZToolkit } from "@/src/application/lib/composio/types";
|
||||
import { Project } from "@/src/entities/models/project";
|
||||
import { fetchProject } from '@/app/actions/project.actions';
|
||||
|
||||
type TriggerDeployment = z.infer<typeof ComposioTriggerDeployment>;
|
||||
|
||||
export function TriggersTab({ projectId }: { projectId: string }) {
|
||||
const [triggers, setTriggers] = useState<TriggerDeployment[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showCreateFlow, setShowCreateFlow] = useState(false);
|
||||
const [selectedToolkit, setSelectedToolkit] = useState<z.infer<typeof ZToolkit> | null>(null);
|
||||
const [selectedTriggerType, setSelectedTriggerType] = useState<z.infer<typeof ComposioTriggerType> | null>(null);
|
||||
const [showAuthModal, setShowAuthModal] = useState(false);
|
||||
const [isSubmittingTrigger, setIsSubmittingTrigger] = useState(false);
|
||||
const [deletingTrigger, setDeletingTrigger] = useState<string | null>(null);
|
||||
const [projectConfig, setProjectConfig] = useState<z.infer<typeof Project> | null>(null);
|
||||
const [triggerTypeNames, setTriggerTypeNames] = useState<Record<string, string>>({});
|
||||
const [expandedTrigger, setExpandedTrigger] = useState<string | null>(null);
|
||||
const [cursor, setCursor] = useState<string | null>(null);
|
||||
const [hasMore, setHasMore] = useState<boolean>(false);
|
||||
const [loadingMore, setLoadingMore] = useState<boolean>(false);
|
||||
|
||||
const loadProjectConfig = useCallback(async () => {
|
||||
try {
|
||||
const config = await fetchProject(projectId);
|
||||
setProjectConfig(config);
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching project config:', err);
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
const loadTriggerTypeNames = useCallback(async () => {
|
||||
try {
|
||||
const names: Record<string, string> = {};
|
||||
|
||||
// Get unique toolkit slugs from existing triggers
|
||||
const uniqueToolkits = [...new Set(triggers.map(t => t.toolkitSlug))];
|
||||
|
||||
// Fetch trigger types for each toolkit
|
||||
for (const toolkitSlug of uniqueToolkits) {
|
||||
try {
|
||||
const response = await listComposioTriggerTypes(toolkitSlug);
|
||||
response.items.forEach(triggerType => {
|
||||
names[triggerType.slug] = triggerType.name;
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`Error fetching trigger types for ${toolkitSlug}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
setTriggerTypeNames(names);
|
||||
} catch (err: any) {
|
||||
console.error('Error loading trigger type names:', err);
|
||||
}
|
||||
}, [triggers]);
|
||||
|
||||
const sections = useMemo(() => {
|
||||
const groups: Record<string, TriggerDeployment[]> = {
|
||||
Today: [],
|
||||
'This week': [],
|
||||
'This month': [],
|
||||
Older: [],
|
||||
};
|
||||
for (const trigger of triggers) {
|
||||
const d = new Date(trigger.createdAt);
|
||||
if (isToday(d)) groups['Today'].push(trigger);
|
||||
else if (isThisWeek(d)) groups['This week'].push(trigger);
|
||||
else if (isThisMonth(d)) groups['This month'].push(trigger);
|
||||
else groups['Older'].push(trigger);
|
||||
}
|
||||
return groups;
|
||||
}, [triggers]);
|
||||
|
||||
const loadTriggers = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await listComposioTriggerDeployments({ projectId });
|
||||
setTriggers(response.items);
|
||||
setCursor(response.nextCursor);
|
||||
setHasMore(Boolean(response.nextCursor));
|
||||
} catch (err: any) {
|
||||
console.error('Error loading triggers:', err);
|
||||
setError('Failed to load triggers. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
const loadMore = useCallback(async () => {
|
||||
if (!cursor) return;
|
||||
setLoadingMore(true);
|
||||
try {
|
||||
const response = await listComposioTriggerDeployments({ projectId, cursor });
|
||||
setTriggers(prev => [...prev, ...response.items]);
|
||||
setCursor(response.nextCursor);
|
||||
setHasMore(Boolean(response.nextCursor));
|
||||
} catch (err: any) {
|
||||
console.error('Error loading more triggers:', err);
|
||||
} finally {
|
||||
setLoadingMore(false);
|
||||
}
|
||||
}, [cursor, projectId]);
|
||||
|
||||
const handleDeleteTrigger = async (deploymentId: string) => {
|
||||
if (!window.confirm('Are you sure you want to delete this trigger?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setDeletingTrigger(deploymentId);
|
||||
await deleteComposioTriggerDeployment({ projectId, deploymentId });
|
||||
await loadTriggers(); // Reload the list
|
||||
} catch (err: any) {
|
||||
console.error('Error deleting trigger:', err);
|
||||
setError('Failed to delete trigger. Please try again.');
|
||||
} finally {
|
||||
setDeletingTrigger(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateNew = () => {
|
||||
setShowCreateFlow(true);
|
||||
};
|
||||
|
||||
const handleBackToList = () => {
|
||||
setShowCreateFlow(false);
|
||||
setSelectedToolkit(null);
|
||||
setSelectedTriggerType(null);
|
||||
setShowAuthModal(false);
|
||||
setIsSubmittingTrigger(false);
|
||||
setExpandedTrigger(null); // Reset expanded state
|
||||
loadTriggers(); // Reload in case any triggers were created
|
||||
};
|
||||
|
||||
const handleSelectToolkit = (toolkit: z.infer<typeof ZToolkit>) => {
|
||||
setSelectedToolkit(toolkit);
|
||||
};
|
||||
|
||||
const handleBackToToolkitSelection = () => {
|
||||
setSelectedToolkit(null);
|
||||
setSelectedTriggerType(null);
|
||||
setIsSubmittingTrigger(false);
|
||||
};
|
||||
|
||||
const handleSelectTriggerType = (triggerType: z.infer<typeof ComposioTriggerType>) => {
|
||||
if (!selectedToolkit) return;
|
||||
|
||||
setSelectedTriggerType(triggerType);
|
||||
|
||||
// Check if toolkit requires auth and if connected account exists
|
||||
const needsAuth = !selectedToolkit.no_auth;
|
||||
const hasConnection = projectConfig?.composioConnectedAccounts?.[selectedToolkit.slug]?.status === 'ACTIVE';
|
||||
|
||||
if (needsAuth && !hasConnection) {
|
||||
// Show auth modal
|
||||
setShowAuthModal(true);
|
||||
} else {
|
||||
// Proceed to trigger configuration
|
||||
// For now this is just the placeholder, but will be actual config later
|
||||
}
|
||||
};
|
||||
|
||||
const handleAuthComplete = async () => {
|
||||
setShowAuthModal(false);
|
||||
await loadProjectConfig(); // Refresh project config
|
||||
};
|
||||
|
||||
const handleTriggerSubmit = async (triggerConfig: Record<string, unknown>) => {
|
||||
if (!selectedToolkit || !selectedTriggerType) return;
|
||||
|
||||
try {
|
||||
setIsSubmittingTrigger(true);
|
||||
|
||||
// Get the connected account ID for this toolkit
|
||||
const connectedAccountId = projectConfig?.composioConnectedAccounts?.[selectedToolkit.slug]?.id;
|
||||
|
||||
if (!connectedAccountId) {
|
||||
throw new Error('No connected account found for this toolkit');
|
||||
}
|
||||
|
||||
// Create the trigger deployment
|
||||
await createComposioTriggerDeployment({
|
||||
projectId,
|
||||
triggerTypeSlug: selectedTriggerType.slug,
|
||||
connectedAccountId,
|
||||
triggerConfig,
|
||||
});
|
||||
|
||||
// Success! Go back to triggers list and reload
|
||||
handleBackToList();
|
||||
} catch (err: any) {
|
||||
console.error('Error creating trigger:', err);
|
||||
setError('Failed to create trigger. Please try again.');
|
||||
} finally {
|
||||
setIsSubmittingTrigger(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadProjectConfig();
|
||||
}, [loadProjectConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showCreateFlow) {
|
||||
loadTriggers();
|
||||
}
|
||||
}, [showCreateFlow, loadTriggers]);
|
||||
|
||||
useEffect(() => {
|
||||
if (triggers.length > 0) {
|
||||
loadTriggerTypeNames();
|
||||
}
|
||||
}, [triggers, loadTriggerTypeNames]);
|
||||
|
||||
const renderTriggerList = () => {
|
||||
if (loading) {
|
||||
return (
|
||||
<Panel
|
||||
title={
|
||||
<div className="text-base font-normal text-gray-900 dark:text-gray-100">
|
||||
Loading your triggers
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="h-full overflow-auto px-4 py-4">
|
||||
<div className="max-w-[1024px] mx-auto">
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Spinner size="lg" />
|
||||
<span className="ml-2">Loading triggers...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Panel
|
||||
title={
|
||||
<div className="text-base font-normal text-gray-900 dark:text-gray-100">
|
||||
Error loading your triggers
|
||||
</div>
|
||||
}
|
||||
rightActions={
|
||||
<Button variant="secondary" onClick={loadTriggers}>
|
||||
Try Again
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className="h-full overflow-auto px-4 py-4">
|
||||
<div className="max-w-[1024px] mx-auto">
|
||||
<div className="text-center py-8">
|
||||
<p className="text-red-500 mb-4">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
if (triggers.length === 0) {
|
||||
return (
|
||||
<Panel
|
||||
title={
|
||||
<div className="text-base font-normal text-gray-900 dark:text-gray-100">
|
||||
Listen for events from connected apps to run your assistant workflow automatically.
|
||||
</div>
|
||||
}
|
||||
rightActions={
|
||||
<Button
|
||||
variant="primary"
|
||||
startContent={<Plus className="w-4 h-4" />}
|
||||
onClick={handleCreateNew}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
New External Trigger
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className="h-full overflow-auto px-4 py-4">
|
||||
<div className="max-w-[1024px] mx-auto">
|
||||
<div className="text-center py-12">
|
||||
<ZapIcon className="w-16 h-16 mx-auto text-gray-400 mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
||||
No external triggers yet
|
||||
</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400 mb-6">
|
||||
Create your first external trigger to listen for events from your connected apps.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel
|
||||
title={
|
||||
<div className="text-base font-normal text-gray-900 dark:text-gray-100">
|
||||
Listen for events from connected apps to run your assistant workflow automatically.
|
||||
</div>
|
||||
}
|
||||
rightActions={
|
||||
<Button
|
||||
variant="primary"
|
||||
startContent={<Plus className="w-4 h-4" />}
|
||||
onClick={handleCreateNew}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
New External Trigger
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className="h-full overflow-auto px-4 py-4">
|
||||
<div className="max-w-[1024px] mx-auto">
|
||||
<div className="flex flex-col gap-6">
|
||||
{Object.entries(sections).map(([sectionName, sectionTriggers]) => {
|
||||
if (sectionTriggers.length === 0) return null;
|
||||
return (
|
||||
<div key={sectionName} className="space-y-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
{sectionName}
|
||||
</h3>
|
||||
<div className="grid gap-3">
|
||||
{sectionTriggers.map((trigger) => (
|
||||
<div
|
||||
key={trigger.id}
|
||||
className="block p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 transition-colors"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<a href={`/projects/${projectId}/job-rules/triggers/${trigger.id}`} className="block">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className="text-sm font-medium text-green-600 dark:text-green-400">
|
||||
Active
|
||||
</span>
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{triggerTypeNames[trigger.triggerTypeSlug] || trigger.triggerTypeSlug}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Created: {new Date(trigger.createdAt).toLocaleDateString()}
|
||||
</div>
|
||||
{Object.keys(trigger.triggerConfig).length > 0 && (
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
Configuration: {Object.keys(trigger.triggerConfig).length} settings
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
size="sm"
|
||||
isLoading={deletingTrigger === trigger.id}
|
||||
onClick={() => handleDeleteTrigger(trigger.id)}
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:text-red-400 dark:hover:text-red-300 dark:hover:bg-red-950"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Advanced Details Section - Collapsible */}
|
||||
<div className="mt-3">
|
||||
<button
|
||||
onClick={() => setExpandedTrigger(expandedTrigger === trigger.id ? null : trigger.id)}
|
||||
className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
|
||||
>
|
||||
<span className="font-medium">Advanced Details</span>
|
||||
{expandedTrigger === trigger.id ? (
|
||||
<ChevronUp className="w-3 h-3" />
|
||||
) : (
|
||||
<ChevronDown className="w-3 h-3" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{expandedTrigger === trigger.id && (
|
||||
<div className="mt-2 space-y-1">
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400">
|
||||
<span className="font-medium">Slug:</span> {trigger.triggerTypeSlug}
|
||||
</div>
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400">
|
||||
<span className="font-medium">Trigger ID:</span> {trigger.triggerId}
|
||||
</div>
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400">
|
||||
<span className="font-medium">Connected Account:</span> {trigger.connectedAccountId}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{hasMore && (
|
||||
<div className="text-center">
|
||||
<Button
|
||||
onClick={loadMore}
|
||||
disabled={loadingMore}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
>
|
||||
{loadingMore ? (
|
||||
<>
|
||||
<Spinner size="sm" />
|
||||
Loading...
|
||||
</>
|
||||
) : (
|
||||
'Load More'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
|
||||
const renderCreateFlow = () => {
|
||||
// If trigger type is selected and auth is complete, show config
|
||||
if (selectedToolkit && selectedTriggerType && !showAuthModal) {
|
||||
const needsAuth = !selectedToolkit.no_auth;
|
||||
const hasConnection = projectConfig?.composioConnectedAccounts?.[selectedToolkit.slug]?.status === 'ACTIVE';
|
||||
|
||||
if (!needsAuth || hasConnection) {
|
||||
return (
|
||||
<TriggerConfigForm
|
||||
toolkit={selectedToolkit}
|
||||
triggerType={selectedTriggerType}
|
||||
onBack={handleBackToToolkitSelection}
|
||||
onSubmit={handleTriggerSubmit}
|
||||
isSubmitting={isSubmittingTrigger}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// If no toolkit selected, show toolkit selection
|
||||
if (!selectedToolkit) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
Select a Toolkit to Create Trigger
|
||||
</h3>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleBackToList}
|
||||
>
|
||||
← Back to Triggers
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<SelectComposioToolkit
|
||||
projectId={projectId}
|
||||
tools={[]} // Empty array since we're not using this for tools
|
||||
onSelectToolkit={handleSelectToolkit}
|
||||
initialToolkitSlug={null}
|
||||
filterByTriggers={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If toolkit selected, show trigger types
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<ComposioTriggerTypesPanel
|
||||
toolkit={selectedToolkit}
|
||||
onBack={handleBackToToolkitSelection}
|
||||
onSelectTriggerType={handleSelectTriggerType}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{showCreateFlow ? renderCreateFlow() : renderTriggerList()}
|
||||
|
||||
{/* Auth Modal */}
|
||||
{selectedToolkit && (
|
||||
<ToolkitAuthModal
|
||||
isOpen={showAuthModal}
|
||||
onClose={() => setShowAuthModal(false)}
|
||||
toolkitSlug={selectedToolkit.slug}
|
||||
projectId={projectId}
|
||||
onComplete={handleAuthComplete}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ import { requireActiveBillingSubscription } from '@/app/lib/billing';
|
|||
import { JobRulesTabs } from "./components/job-rules-tabs";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Job Rules",
|
||||
title: "Triggers",
|
||||
};
|
||||
|
||||
export default async function Page(
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|||
import { Link, Spinner } from "@heroui/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Panel } from "@/components/common/panel-common";
|
||||
import { listScheduledJobRules } from "@/app/actions/scheduled-job-rules.actions";
|
||||
import { listScheduledJobRules, deleteScheduledJobRule } from "@/app/actions/scheduled-job-rules.actions";
|
||||
import { z } from "zod";
|
||||
import { ListedRuleItem } from "@/src/application/repositories/scheduled-job-rules.repository.interface";
|
||||
import { isToday, isThisWeek, isThisMonth } from "@/lib/utils/date";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { PlusIcon, Trash2 } from "lucide-react";
|
||||
|
||||
type ListedItem = z.infer<typeof ListedRuleItem>;
|
||||
|
||||
|
|
@ -18,6 +18,7 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
|
|||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [loadingMore, setLoadingMore] = useState<boolean>(false);
|
||||
const [hasMore, setHasMore] = useState<boolean>(false);
|
||||
const [deletingRule, setDeletingRule] = useState<string | null>(null);
|
||||
|
||||
const fetchPage = useCallback(async (cursorArg?: string | null) => {
|
||||
const res = await listScheduledJobRules({ projectId, cursor: cursorArg ?? undefined, limit: 20 });
|
||||
|
|
@ -48,6 +49,24 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
|
|||
setLoadingMore(false);
|
||||
}, [cursor, fetchPage]);
|
||||
|
||||
const handleDeleteRule = async (ruleId: string) => {
|
||||
if (!window.confirm('Are you sure you want to delete this one-time trigger?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setDeletingRule(ruleId);
|
||||
await deleteScheduledJobRule({ projectId, ruleId });
|
||||
// Remove the deleted item from the list
|
||||
setItems(prev => prev.filter(item => item.id !== ruleId));
|
||||
} catch (err: any) {
|
||||
console.error('Error deleting one-time trigger:', err);
|
||||
alert('Failed to delete one-time trigger. Please try again.');
|
||||
} finally {
|
||||
setDeletingRule(null);
|
||||
}
|
||||
};
|
||||
|
||||
const sections = useMemo(() => {
|
||||
const groups: Record<string, ListedItem[]> = {
|
||||
Today: [],
|
||||
|
|
@ -87,18 +106,15 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
|
|||
return (
|
||||
<Panel
|
||||
title={
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
SCHEDULED JOB RULES
|
||||
</div>
|
||||
<div className="text-base font-normal text-gray-900 dark:text-gray-100">
|
||||
Schedule a single job to run your assistant workflow at a specific date and time.
|
||||
</div>
|
||||
}
|
||||
rightActions={
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href={`/projects/${projectId}/job-rules/scheduled/new`}>
|
||||
<Button size="sm" className="flex items-center gap-2">
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
New Rule
|
||||
<Button size="sm" className="whitespace-nowrap" startContent={<PlusIcon className="w-4 h-4" />}>
|
||||
New One-time Trigger
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
|
@ -123,30 +139,40 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
|
|||
</h3>
|
||||
<div className="grid gap-3">
|
||||
{sectionItems.map((item) => (
|
||||
<Link
|
||||
<div
|
||||
key={item.id}
|
||||
href={`/projects/${projectId}/job-rules/scheduled/${item.id}`}
|
||||
className="block p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className={`text-sm font-medium ${getStatusColor(item.status, item.processedAt || null)}`}>
|
||||
{getStatusText(item.status, item.processedAt || null)}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Next run: {formatNextRunAt(item.nextRunAt)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Created: {new Date(item.createdAt).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{new Date(item.createdAt).toLocaleDateString()}
|
||||
<Link
|
||||
href={`/projects/${projectId}/job-rules/scheduled/${item.id}`}
|
||||
className="block"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className={`text-sm font-medium ${getStatusColor(item.status, item.processedAt || null)}`}>
|
||||
{getStatusText(item.status, item.processedAt || null)}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Next run: {formatNextRunAt(item.nextRunAt)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Created: {new Date(item.createdAt).toLocaleDateString()}
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
size="sm"
|
||||
isLoading={deletingRule === item.id}
|
||||
onClick={() => handleDeleteRule(item.id)}
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:text-red-400 dark:hover:text-red-300 dark:hover:bg-red-950"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -154,7 +180,7 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
|
|||
})}
|
||||
{items.length === 0 && !loading && (
|
||||
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
No scheduled job rules found. Create your first rule to get started.
|
||||
No one-time triggers yet. Create your first one-time trigger to get started.
|
||||
</div>
|
||||
)}
|
||||
{hasMore && (
|
||||
|
|
@ -183,3 +209,4 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
|
|||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <ComposioTriggerDeploymentView projectId={params.projectId} deploymentId={params.deploymentId} />;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ interface TopBarProps {
|
|||
localProjectName: string;
|
||||
projectNameError: string | null;
|
||||
onProjectNameChange: (value: string) => void;
|
||||
onProjectNameCommit: (value: string) => void;
|
||||
onProjectNameCommit: (value: string) => Promise<void>;
|
||||
publishing: boolean;
|
||||
isLive: boolean;
|
||||
showCopySuccess: boolean;
|
||||
|
|
@ -23,7 +23,6 @@ interface TopBarProps {
|
|||
onRevertToLive: () => void;
|
||||
onToggleCopilot: () => void;
|
||||
onSettingsModalOpen: () => void;
|
||||
onTriggersModalOpen: () => void;
|
||||
}
|
||||
|
||||
export function TopBar({
|
||||
|
|
@ -45,7 +44,6 @@ export function TopBar({
|
|||
onRevertToLive,
|
||||
onToggleCopilot,
|
||||
onSettingsModalOpen,
|
||||
onTriggersModalOpen,
|
||||
}: TopBarProps) {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
|
|
@ -168,16 +166,9 @@ export function TopBar({
|
|||
<DropdownItem
|
||||
key="manage-triggers"
|
||||
startContent={<ZapIcon size={16} />}
|
||||
onPress={onTriggersModalOpen}
|
||||
>
|
||||
Manage triggers
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="go-to-schedule-runs"
|
||||
startContent={<Clock size={16} />}
|
||||
onPress={() => { if (projectId) { router.push(`/projects/${projectId}/job-rules`); } }}
|
||||
>
|
||||
Go to schedule runs
|
||||
Manage triggers
|
||||
</DropdownItem>
|
||||
{!isLive ? (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -1,360 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button, Spinner, Card, CardBody, CardHeader } from '@heroui/react';
|
||||
import { Plus, Trash2, ZapIcon } from 'lucide-react';
|
||||
import { z } from 'zod';
|
||||
import { ComposioTriggerDeployment } from '@/src/entities/models/composio-trigger-deployment';
|
||||
import { ComposioTriggerType } from '@/src/entities/models/composio-trigger-type';
|
||||
import { listComposioTriggerDeployments, deleteComposioTriggerDeployment, createComposioTriggerDeployment } from '@/app/actions/composio.actions';
|
||||
import { SelectComposioToolkit } from '../../tools/components/SelectComposioToolkit';
|
||||
import { ComposioTriggerTypesPanel } from './ComposioTriggerTypesPanel';
|
||||
import { TriggerConfigForm } from './TriggerConfigForm';
|
||||
import { ToolkitAuthModal } from '../../tools/components/ToolkitAuthModal';
|
||||
import { ZToolkit } from "@/src/application/lib/composio/types";
|
||||
import { Project } from "@/src/entities/models/project";
|
||||
|
||||
interface TriggersModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
projectId: string;
|
||||
projectConfig: z.infer<typeof Project>;
|
||||
onProjectConfigUpdated?: () => void;
|
||||
}
|
||||
|
||||
type TriggerDeployment = z.infer<typeof ComposioTriggerDeployment>;
|
||||
|
||||
export function TriggersModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
projectId,
|
||||
projectConfig,
|
||||
onProjectConfigUpdated,
|
||||
}: TriggersModalProps) {
|
||||
const [triggers, setTriggers] = useState<TriggerDeployment[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showCreateFlow, setShowCreateFlow] = useState(false);
|
||||
const [selectedToolkit, setSelectedToolkit] = useState<z.infer<typeof ZToolkit> | null>(null);
|
||||
const [selectedTriggerType, setSelectedTriggerType] = useState<z.infer<typeof ComposioTriggerType> | null>(null);
|
||||
const [showAuthModal, setShowAuthModal] = useState(false);
|
||||
const [isSubmittingTrigger, setIsSubmittingTrigger] = useState(false);
|
||||
const [deletingTrigger, setDeletingTrigger] = useState<string | null>(null);
|
||||
|
||||
const loadTriggers = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await listComposioTriggerDeployments({ projectId });
|
||||
setTriggers(response.items);
|
||||
} catch (err: any) {
|
||||
console.error('Error loading triggers:', err);
|
||||
setError('Failed to load triggers. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
const handleDeleteTrigger = async (deploymentId: string) => {
|
||||
if (!window.confirm('Are you sure you want to delete this trigger?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setDeletingTrigger(deploymentId);
|
||||
await deleteComposioTriggerDeployment({ projectId, deploymentId });
|
||||
await loadTriggers(); // Reload the list
|
||||
} catch (err: any) {
|
||||
console.error('Error deleting trigger:', err);
|
||||
setError('Failed to delete trigger. Please try again.');
|
||||
} finally {
|
||||
setDeletingTrigger(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateNew = () => {
|
||||
setShowCreateFlow(true);
|
||||
};
|
||||
|
||||
const handleBackToList = () => {
|
||||
setShowCreateFlow(false);
|
||||
setSelectedToolkit(null);
|
||||
setSelectedTriggerType(null);
|
||||
setShowAuthModal(false);
|
||||
setIsSubmittingTrigger(false);
|
||||
loadTriggers(); // Reload in case any triggers were created
|
||||
};
|
||||
|
||||
const handleSelectToolkit = (toolkit: z.infer<typeof ZToolkit>) => {
|
||||
setSelectedToolkit(toolkit);
|
||||
};
|
||||
|
||||
const handleBackToToolkitSelection = () => {
|
||||
setSelectedToolkit(null);
|
||||
setSelectedTriggerType(null);
|
||||
setIsSubmittingTrigger(false);
|
||||
};
|
||||
|
||||
const handleSelectTriggerType = (triggerType: z.infer<typeof ComposioTriggerType>) => {
|
||||
if (!selectedToolkit) return;
|
||||
|
||||
setSelectedTriggerType(triggerType);
|
||||
|
||||
// Check if toolkit requires auth and if connected account exists
|
||||
const needsAuth = !selectedToolkit.no_auth;
|
||||
const hasConnection = projectConfig?.composioConnectedAccounts?.[selectedToolkit.slug]?.status === 'ACTIVE';
|
||||
|
||||
if (needsAuth && !hasConnection) {
|
||||
// Show auth modal
|
||||
setShowAuthModal(true);
|
||||
} else {
|
||||
// Proceed to trigger configuration
|
||||
// For now this is just the placeholder, but will be actual config later
|
||||
}
|
||||
};
|
||||
|
||||
const handleAuthComplete = async () => {
|
||||
setShowAuthModal(false);
|
||||
onProjectConfigUpdated?.();
|
||||
};
|
||||
|
||||
const handleTriggerSubmit = async (triggerConfig: Record<string, unknown>) => {
|
||||
if (!selectedToolkit || !selectedTriggerType) return;
|
||||
|
||||
try {
|
||||
setIsSubmittingTrigger(true);
|
||||
|
||||
// Get the connected account ID for this toolkit
|
||||
const connectedAccountId = projectConfig?.composioConnectedAccounts?.[selectedToolkit.slug]?.id;
|
||||
|
||||
if (!connectedAccountId) {
|
||||
throw new Error('No connected account found for this toolkit');
|
||||
}
|
||||
|
||||
// Create the trigger deployment
|
||||
await createComposioTriggerDeployment({
|
||||
projectId,
|
||||
toolkitSlug: selectedToolkit.slug,
|
||||
triggerTypeSlug: selectedTriggerType.slug,
|
||||
connectedAccountId,
|
||||
triggerConfig,
|
||||
});
|
||||
|
||||
// Success! Go back to triggers list and reload
|
||||
handleBackToList();
|
||||
} catch (err: any) {
|
||||
console.error('Error creating trigger:', err);
|
||||
setError('Failed to create trigger. Please try again.');
|
||||
} finally {
|
||||
setIsSubmittingTrigger(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && !showCreateFlow) {
|
||||
loadTriggers();
|
||||
}
|
||||
}, [isOpen, showCreateFlow, loadTriggers]);
|
||||
|
||||
const renderTriggerList = () => {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Spinner size="lg" />
|
||||
<span className="ml-2">Loading triggers...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-red-500 mb-4">{error}</p>
|
||||
<Button variant="flat" onPress={loadTriggers}>
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (triggers.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<ZapIcon className="w-16 h-16 mx-auto text-gray-400 mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
||||
No triggers configured
|
||||
</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400 mb-6">
|
||||
Set up your first trigger to listen for events from your connected apps.
|
||||
</p>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="solid"
|
||||
startContent={<Plus className="w-4 h-4" />}
|
||||
onPress={handleCreateNew}
|
||||
>
|
||||
Create your first trigger
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
Active Triggers ({triggers.length})
|
||||
</h3>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="solid"
|
||||
startContent={<Plus className="w-4 h-4" />}
|
||||
onPress={handleCreateNew}
|
||||
>
|
||||
Create New Trigger
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{triggers.map((trigger) => (
|
||||
<Card key={trigger.id} className="w-full">
|
||||
<CardHeader className="flex justify-between items-start">
|
||||
<div>
|
||||
<h4 className="text-base font-medium text-gray-900 dark:text-gray-100">
|
||||
{trigger.triggerTypeSlug}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Created {new Date(trigger.createdAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
isIconOnly
|
||||
variant="light"
|
||||
color="danger"
|
||||
size="sm"
|
||||
isLoading={deletingTrigger === trigger.id}
|
||||
onPress={() => handleDeleteTrigger(trigger.id)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardBody className="pt-0">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-300">
|
||||
<p><strong>Trigger ID:</strong> {trigger.triggerId}</p>
|
||||
<p><strong>Connected Account:</strong> {trigger.connectedAccountId}</p>
|
||||
{Object.keys(trigger.triggerConfig).length > 0 && (
|
||||
<div className="mt-2">
|
||||
<strong>Configuration:</strong>
|
||||
<pre className="mt-1 text-xs bg-gray-100 dark:bg-gray-800 p-2 rounded">
|
||||
{JSON.stringify(trigger.triggerConfig, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderCreateFlow = () => {
|
||||
// If trigger type is selected and auth is complete, show config
|
||||
if (selectedToolkit && selectedTriggerType && !showAuthModal) {
|
||||
const needsAuth = !selectedToolkit.no_auth;
|
||||
const hasConnection = projectConfig?.composioConnectedAccounts?.[selectedToolkit.slug]?.status === 'ACTIVE';
|
||||
|
||||
if (!needsAuth || hasConnection) {
|
||||
return (
|
||||
<TriggerConfigForm
|
||||
toolkit={selectedToolkit}
|
||||
triggerType={selectedTriggerType}
|
||||
onBack={handleBackToToolkitSelection}
|
||||
onSubmit={handleTriggerSubmit}
|
||||
isSubmitting={isSubmittingTrigger}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// If no toolkit selected, show toolkit selection
|
||||
if (!selectedToolkit) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
Select a Toolkit to Create Trigger
|
||||
</h3>
|
||||
<Button
|
||||
variant="flat"
|
||||
onPress={handleBackToList}
|
||||
>
|
||||
← Back to Triggers
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<SelectComposioToolkit
|
||||
projectId={projectId}
|
||||
tools={[]} // Empty array since we're not using this for tools
|
||||
onSelectToolkit={handleSelectToolkit}
|
||||
initialToolkitSlug={null}
|
||||
filterByTriggers={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If toolkit selected, show trigger types
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<ComposioTriggerTypesPanel
|
||||
toolkit={selectedToolkit}
|
||||
onBack={handleBackToToolkitSelection}
|
||||
onSelectTriggerType={handleSelectTriggerType}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
size="5xl"
|
||||
scrollBehavior="inside"
|
||||
>
|
||||
<ModalContent className="max-h-[90vh]">
|
||||
<ModalHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<ZapIcon className="w-5 h-5" />
|
||||
<span>Manage Triggers</span>
|
||||
</div>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
{showCreateFlow ? renderCreateFlow() : renderTriggerList()}
|
||||
</ModalBody>
|
||||
{!showCreateFlow && (
|
||||
<ModalFooter>
|
||||
<Button variant="light" onPress={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
{/* Auth Modal */}
|
||||
{selectedToolkit && (
|
||||
<ToolkitAuthModal
|
||||
isOpen={showAuthModal}
|
||||
onClose={() => setShowAuthModal(false)}
|
||||
toolkitSlug={selectedToolkit.slug}
|
||||
projectId={projectId}
|
||||
onComplete={handleAuthComplete}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -37,7 +37,6 @@ import { Button as CustomButton } from "@/components/ui/button";
|
|||
import { ConfigApp } from "../config/app";
|
||||
import { InputField } from "@/app/lib/components/input-field";
|
||||
import { VoiceSection } from "../config/components/voice";
|
||||
import { TriggersModal } from "./components/TriggersModal";
|
||||
import { TopBar } from "./components/TopBar";
|
||||
|
||||
enablePatches();
|
||||
|
|
@ -882,9 +881,6 @@ export function WorkflowEditor({
|
|||
// Modal state for chat widget configuration
|
||||
const { isOpen: isChatWidgetModalOpen, onOpen: onChatWidgetModalOpen, onClose: onChatWidgetModalClose } = useDisclosure();
|
||||
|
||||
// Modal state for triggers management
|
||||
const { isOpen: isTriggersModalOpen, onOpen: onTriggersModalOpen, onClose: onTriggersModalClose } = useDisclosure();
|
||||
|
||||
// Project name state
|
||||
const [localProjectName, setLocalProjectName] = useState<string>(projectConfig.name || '');
|
||||
const [projectNameError, setProjectNameError] = useState<string | null>(null);
|
||||
|
|
@ -1285,7 +1281,6 @@ export function WorkflowEditor({
|
|||
onRevertToLive={handleRevertToLive}
|
||||
onToggleCopilot={() => setShowCopilot(!showCopilot)}
|
||||
onSettingsModalOpen={onSettingsModalOpen}
|
||||
onTriggersModalOpen={onTriggersModalOpen}
|
||||
/>
|
||||
|
||||
{/* Content Area */}
|
||||
|
|
@ -1565,14 +1560,6 @@ export function WorkflowEditor({
|
|||
</Modal>
|
||||
*/}
|
||||
|
||||
{/* Triggers Management Modal */}
|
||||
<TriggersModal
|
||||
isOpen={isTriggersModalOpen}
|
||||
onClose={onTriggersModalClose}
|
||||
projectId={projectId}
|
||||
projectConfig={projectConfig}
|
||||
onProjectConfigUpdated={onProjectConfigUpdated}
|
||||
/>
|
||||
</div>
|
||||
</EntitySelectionContext.Provider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { useState, useRef, useEffect } from "react";
|
||||
import { listTemplates, listProjects } from "@/app/actions/project.actions";
|
||||
import { createProjectWithOptions, createProjectFromJsonWithOptions, createProjectFromTemplate } from "../lib/project-creation-utils";
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import clsx from 'clsx';
|
||||
import Image from 'next/image';
|
||||
import mascotImage from '@/public/mascot.png';
|
||||
|
|
@ -36,6 +36,8 @@ export function BuildAssistantSection() {
|
|||
const [selectedTab, setSelectedTab] = useState('new');
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [autoCreateLoading, setAutoCreateLoading] = useState(false);
|
||||
|
||||
const totalPages = Math.ceil(projects.length / ITEMS_PER_PAGE);
|
||||
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
|
||||
|
|
@ -103,6 +105,29 @@ export function BuildAssistantSection() {
|
|||
fetchProjects();
|
||||
}, []);
|
||||
|
||||
// Handle URL parameters for auto-creation and direct redirect to build view
|
||||
useEffect(() => {
|
||||
const urlPrompt = searchParams.get('prompt');
|
||||
const urlTemplate = searchParams.get('template');
|
||||
|
||||
if (urlPrompt || urlTemplate) {
|
||||
setAutoCreateLoading(true);
|
||||
createProjectWithOptions({
|
||||
template: urlTemplate || undefined,
|
||||
prompt: urlPrompt || undefined,
|
||||
router,
|
||||
onError: (error) => {
|
||||
console.error('Error auto-creating project:', error);
|
||||
setAutoCreateLoading(false);
|
||||
// Fall back to showing the form with the prompt pre-filled
|
||||
if (urlPrompt) {
|
||||
setUserPrompt(urlPrompt);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [searchParams, router]);
|
||||
|
||||
const handleCreateAssistant = async () => {
|
||||
setIsCreating(true);
|
||||
try {
|
||||
|
|
@ -170,6 +195,15 @@ export function BuildAssistantSection() {
|
|||
className="hidden"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
{autoCreateLoading && (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-500 mb-4"></div>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Creating your assistant...
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{!autoCreateLoading && (
|
||||
<div className="px-8 py-16">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Main Headline */}
|
||||
|
|
@ -445,6 +479,7 @@ export function BuildAssistantSection() {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,7 +2,8 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import logoImage from '@/public/logo-only.png';
|
||||
import logo from '@/public/logo.png';
|
||||
import logoOnly from '@/public/logo-only.png';
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Tooltip, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/react";
|
||||
import { UserButton } from "@/app/lib/components/user_button";
|
||||
|
|
@ -17,7 +18,8 @@ import {
|
|||
HelpCircle,
|
||||
MessageSquareIcon,
|
||||
LogsIcon,
|
||||
Clock
|
||||
Clock,
|
||||
ZapIcon
|
||||
} from "lucide-react";
|
||||
import { fetchProject } from "@/app/actions/project.actions";
|
||||
import { createProjectWithOptions } from "../../lib/project-creation-utils";
|
||||
|
|
@ -102,6 +104,12 @@ export default function Sidebar({ projectId, useAuth, collapsed = false, onToggl
|
|||
icon: WorkflowIcon,
|
||||
requiresProject: true
|
||||
},
|
||||
{
|
||||
href: 'job-rules',
|
||||
label: 'Triggers',
|
||||
icon: ZapIcon,
|
||||
requiresProject: true
|
||||
},
|
||||
{
|
||||
href: 'conversations',
|
||||
label: 'Conversations',
|
||||
|
|
@ -114,12 +122,6 @@ export default function Sidebar({ projectId, useAuth, collapsed = false, onToggl
|
|||
icon: LogsIcon,
|
||||
requiresProject: true
|
||||
},
|
||||
{
|
||||
href: 'job-rules',
|
||||
label: 'Job Rules',
|
||||
icon: Clock,
|
||||
requiresProject: true
|
||||
},
|
||||
{
|
||||
href: 'config',
|
||||
label: 'Settings',
|
||||
|
|
@ -154,18 +156,17 @@ export default function Sidebar({ projectId, useAuth, collapsed = false, onToggl
|
|||
${collapsed ? 'py-3' : 'gap-3 px-4 py-2.5 justify-start'}
|
||||
`}
|
||||
>
|
||||
<Image
|
||||
src={logoImage}
|
||||
{collapsed && <Image
|
||||
src={logoOnly}
|
||||
alt="Rowboat"
|
||||
width={collapsed ? 24 : 24}
|
||||
height={collapsed ? 24 : 24}
|
||||
className="rounded-full transition-all duration-200 flex-shrink-0"
|
||||
/>
|
||||
{!collapsed && (
|
||||
<span className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
Rowboat
|
||||
</span>
|
||||
)}
|
||||
width={24}
|
||||
height={24}
|
||||
/>}
|
||||
{!collapsed && <Image
|
||||
src={logo}
|
||||
alt="Rowboat"
|
||||
height={32}
|
||||
/>}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -74,17 +74,18 @@ export function BillingUpgradeModal({ isOpen, onClose, errorMessage }: BillingUp
|
|||
plan: "starter" as const,
|
||||
description: "Great for your personal projects",
|
||||
features: [
|
||||
"1000 playground chat requests",
|
||||
"500 copilot requests"
|
||||
"2,000 credits",
|
||||
"Latest models like gpt-5, claude-4 and others",
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Pro",
|
||||
plan: "pro" as const,
|
||||
description: "Great for enterprise teams",
|
||||
description: "Great for power users or teams",
|
||||
features: [
|
||||
"10000 playground chat requests",
|
||||
"2000 copilot requests"
|
||||
"20,000 credits",
|
||||
"o3 and o3-pro",
|
||||
"Priority support",
|
||||
],
|
||||
recommended: true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -212,4 +212,9 @@ export async function listTriggersTypes(toolkitSlug: string, cursor?: string): P
|
|||
|
||||
// fetch
|
||||
return composioApiCall(ZListResponse(ZTriggerType), url.toString());
|
||||
}
|
||||
|
||||
export async function getTriggersType(triggerTypeSlug: string): Promise<z.infer<typeof ZTriggerType>> {
|
||||
const url = new URL(`${BASE_URL}/triggers_types/${triggerTypeSlug}`);
|
||||
return composioApiCall(ZTriggerType, url.toString());
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ export const CreateDeploymentSchema = ComposioTriggerDeployment
|
|||
toolkitSlug: true,
|
||||
logo: true,
|
||||
triggerTypeSlug: true,
|
||||
triggerTypeName: true,
|
||||
triggerConfig: true,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -2,18 +2,20 @@ import { BadRequestError, 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 { CreateDeploymentSchema, IComposioTriggerDeploymentsRepository } from '../../repositories/composio-trigger-deployments.repository.interface';
|
||||
import { IComposioTriggerDeploymentsRepository } from '../../repositories/composio-trigger-deployments.repository.interface';
|
||||
import { IProjectsRepository } from '../../repositories/projects.repository.interface';
|
||||
import { composio, getToolkit } from '../../lib/composio/composio';
|
||||
import { composio, getTriggersType } from '../../lib/composio/composio';
|
||||
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(),
|
||||
data: CreateDeploymentSchema.omit({
|
||||
triggerId: true,
|
||||
logo: true,
|
||||
projectId: z.string(),
|
||||
data: ComposioTriggerDeployment.pick({
|
||||
triggerTypeSlug: true,
|
||||
connectedAccountId: true,
|
||||
triggerConfig: true,
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
@ -46,7 +48,7 @@ export class CreateComposioTriggerDeploymentUseCase implements ICreateComposioTr
|
|||
|
||||
async execute(request: z.infer<typeof inputSchema>): Promise<z.infer<typeof ComposioTriggerDeployment>> {
|
||||
// extract projectid from conversation
|
||||
const { projectId } = request.data;
|
||||
const { projectId } = request;
|
||||
|
||||
// authz check
|
||||
await this.projectActionAuthorizationPolicy.authorize({
|
||||
|
|
@ -59,8 +61,11 @@ export class CreateComposioTriggerDeploymentUseCase implements ICreateComposioTr
|
|||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
|
||||
// get trigger type info
|
||||
const triggerType = await getTriggersType(request.data.triggerTypeSlug);
|
||||
|
||||
// get toolkit info
|
||||
const toolkit = await getToolkit(request.data.toolkitSlug);
|
||||
const toolkit = triggerType.toolkit;
|
||||
|
||||
// ensure that connected account exists on project
|
||||
const project = await this.projectsRepository.fetch(projectId);
|
||||
|
|
@ -69,7 +74,7 @@ export class CreateComposioTriggerDeploymentUseCase implements ICreateComposioTr
|
|||
}
|
||||
|
||||
// ensure connected account exists
|
||||
const account = project.composioConnectedAccounts?.[request.data.toolkitSlug];
|
||||
const account = project.composioConnectedAccounts?.[toolkit.slug];
|
||||
if (!account || account.id !== request.data.connectedAccountId) {
|
||||
throw new BadRequestError('Invalid connected account');
|
||||
}
|
||||
|
|
@ -81,7 +86,7 @@ export class CreateComposioTriggerDeploymentUseCase implements ICreateComposioTr
|
|||
}
|
||||
|
||||
// create trigger on composio
|
||||
const result = await composio.triggers.create(request.data.projectId, request.data.triggerTypeSlug, {
|
||||
const result = await composio.triggers.create(projectId, request.data.triggerTypeSlug, {
|
||||
connectedAccountId: request.data.connectedAccountId,
|
||||
triggerConfig: request.data.triggerConfig,
|
||||
});
|
||||
|
|
@ -89,11 +94,12 @@ export class CreateComposioTriggerDeploymentUseCase implements ICreateComposioTr
|
|||
// create trigger deployment in db
|
||||
return await this.composioTriggerDeploymentsRepository.create({
|
||||
projectId,
|
||||
toolkitSlug: request.data.toolkitSlug,
|
||||
logo: toolkit.meta.logo,
|
||||
toolkitSlug: toolkit.slug,
|
||||
logo: toolkit.logo,
|
||||
triggerId: result.triggerId,
|
||||
connectedAccountId: request.data.connectedAccountId,
|
||||
triggerTypeSlug: request.data.triggerTypeSlug,
|
||||
triggerTypeName: triggerType.name,
|
||||
triggerConfig: request.data.triggerConfig,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<typeof inputSchema>): Promise<z.infer<typeof ComposioTriggerDeployment>>;
|
||||
}
|
||||
|
||||
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<typeof inputSchema>): Promise<z.infer<typeof ComposioTriggerDeployment>> {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -6,6 +6,7 @@ export const ComposioTriggerDeployment = z.object({
|
|||
triggerId: z.string(),
|
||||
toolkitSlug: z.string(),
|
||||
triggerTypeSlug: z.string(),
|
||||
triggerTypeName: z.string(),
|
||||
connectedAccountId: z.string(),
|
||||
triggerConfig: z.record(z.string(), z.unknown()),
|
||||
logo: z.string(),
|
||||
|
|
|
|||
|
|
@ -2,15 +2,16 @@ import { BadRequestError } from "@/src/entities/errors/common";
|
|||
import z from "zod";
|
||||
import { ICreateComposioTriggerDeploymentUseCase } from "@/src/application/use-cases/composio-trigger-deployments/create-composio-trigger-deployment.use-case";
|
||||
import { ComposioTriggerDeployment } from "@/src/entities/models/composio-trigger-deployment";
|
||||
import { CreateDeploymentSchema } from "@/src/application/repositories/composio-trigger-deployments.repository.interface";
|
||||
|
||||
const inputSchema = z.object({
|
||||
caller: z.enum(["user", "api"]),
|
||||
userId: z.string().optional(),
|
||||
apiKey: z.string().optional(),
|
||||
data: CreateDeploymentSchema.omit({
|
||||
triggerId: true,
|
||||
logo: true,
|
||||
projectId: z.string(),
|
||||
data: ComposioTriggerDeployment.pick({
|
||||
triggerTypeSlug: true,
|
||||
connectedAccountId: true,
|
||||
triggerConfig: true,
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
@ -35,13 +36,14 @@ export class CreateComposioTriggerDeploymentController implements ICreateComposi
|
|||
if (!result.success) {
|
||||
throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`);
|
||||
}
|
||||
const { caller, userId, apiKey, data } = result.data;
|
||||
const { caller, userId, apiKey, projectId, data } = result.data;
|
||||
|
||||
// execute use case
|
||||
return await this.createComposioTriggerDeploymentUseCase.execute({
|
||||
caller,
|
||||
userId,
|
||||
apiKey,
|
||||
projectId,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<typeof inputSchema>): Promise<z.infer<typeof ComposioTriggerDeployment>>;
|
||||
}
|
||||
|
||||
export class FetchComposioTriggerDeploymentController implements IFetchComposioTriggerDeploymentController {
|
||||
private readonly fetchComposioTriggerDeploymentUseCase: IFetchComposioTriggerDeploymentUseCase;
|
||||
|
||||
constructor({
|
||||
fetchComposioTriggerDeploymentUseCase,
|
||||
}: {
|
||||
fetchComposioTriggerDeploymentUseCase: IFetchComposioTriggerDeploymentUseCase,
|
||||
}) {
|
||||
this.fetchComposioTriggerDeploymentUseCase = fetchComposioTriggerDeploymentUseCase;
|
||||
}
|
||||
|
||||
async execute(request: z.infer<typeof inputSchema>): Promise<z.infer<typeof ComposioTriggerDeployment>> {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue