mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-24 20:28:16 +02:00
commit
64f20ca9bf
27 changed files with 1152 additions and 599 deletions
|
|
@ -12,102 +12,65 @@ pip install rowboat
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Basic Usage with StatefulChat
|
### Basic Usage
|
||||||
|
|
||||||
The easiest way to interact with Rowboat is using the `StatefulChat` class, which maintains conversation state automatically:
|
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 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:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
from rowboat.client import Client
|
||||||
from rowboat.schema import UserMessage
|
from rowboat.schema import UserMessage
|
||||||
|
|
||||||
# Initialize the client
|
# Initialize the client
|
||||||
client = Client(
|
client = Client(
|
||||||
host="<HOST>",
|
host="<HOST>",
|
||||||
project_id="<PROJECT_ID>",
|
projectId="<PROJECT_ID>",
|
||||||
api_key="<API_KEY>"
|
apiKey="<API_KEY>"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create messages
|
# Start a new conversation
|
||||||
messages = [
|
result = client.run_turn(
|
||||||
UserMessage(role='user', content="Hello, how are you?")
|
messages=[
|
||||||
]
|
UserMessage(role='user', content="list my github repos")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
print(result.turn.output[-1].content)
|
||||||
|
print("Conversation ID:", result.conversationId)
|
||||||
|
|
||||||
# Get response
|
# Continue the conversation by passing the conversationId
|
||||||
response = client.chat(messages=messages)
|
result = client.run_turn(
|
||||||
print(response.messages[-1].content)
|
messages=[
|
||||||
|
UserMessage(role='user', content="how many did you find?")
|
||||||
# For subsequent messages, you need to manage the message history and state manually
|
],
|
||||||
messages.extend(response.messages)
|
conversationId=result.conversationId
|
||||||
messages.append(UserMessage(role='user', content="What's your name?"))
|
)
|
||||||
response = client.chat(messages=messages, state=response.state)
|
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]
|
[project]
|
||||||
name = "rowboat"
|
name = "rowboat"
|
||||||
version = "5.0.0"
|
version = "5.0.1"
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "Ramnique Singh", email = "ramnique@rowboatlabs.com" },
|
{ 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 { 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 { 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 { 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 { IDeleteComposioConnectedAccountController } from "@/src/interface-adapters/controllers/projects/delete-composio-connected-account.controller";
|
||||||
import { authCheck } from "./auth.actions";
|
import { authCheck } from "./auth.actions";
|
||||||
import { ICreateComposioManagedConnectedAccountController } from "@/src/interface-adapters/controllers/projects/create-composio-managed-connected-account.controller";
|
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 listComposioTriggerDeploymentsController = container.resolve<IListComposioTriggerDeploymentsController>("listComposioTriggerDeploymentsController");
|
||||||
const deleteComposioTriggerDeploymentController = container.resolve<IDeleteComposioTriggerDeploymentController>("deleteComposioTriggerDeploymentController");
|
const deleteComposioTriggerDeploymentController = container.resolve<IDeleteComposioTriggerDeploymentController>("deleteComposioTriggerDeploymentController");
|
||||||
const listComposioTriggerTypesController = container.resolve<IListComposioTriggerTypesController>("listComposioTriggerTypesController");
|
const listComposioTriggerTypesController = container.resolve<IListComposioTriggerTypesController>("listComposioTriggerTypesController");
|
||||||
|
const fetchComposioTriggerDeploymentController = container.resolve<IFetchComposioTriggerDeploymentController>("fetchComposioTriggerDeploymentController");
|
||||||
const deleteComposioConnectedAccountController = container.resolve<IDeleteComposioConnectedAccountController>("deleteComposioConnectedAccountController");
|
const deleteComposioConnectedAccountController = container.resolve<IDeleteComposioConnectedAccountController>("deleteComposioConnectedAccountController");
|
||||||
const createComposioManagedConnectedAccountController = container.resolve<ICreateComposioManagedConnectedAccountController>("createComposioManagedConnectedAccountController");
|
const createComposioManagedConnectedAccountController = container.resolve<ICreateComposioManagedConnectedAccountController>("createComposioManagedConnectedAccountController");
|
||||||
const createCustomConnectedAccountController = container.resolve<ICreateCustomConnectedAccountController>("createCustomConnectedAccountController");
|
const createCustomConnectedAccountController = container.resolve<ICreateCustomConnectedAccountController>("createCustomConnectedAccountController");
|
||||||
|
|
@ -133,7 +135,6 @@ export async function listComposioTriggerTypes(toolkitSlug: string, cursor?: str
|
||||||
|
|
||||||
export async function createComposioTriggerDeployment(request: {
|
export async function createComposioTriggerDeployment(request: {
|
||||||
projectId: string,
|
projectId: string,
|
||||||
toolkitSlug: string,
|
|
||||||
triggerTypeSlug: string,
|
triggerTypeSlug: string,
|
||||||
connectedAccountId: string,
|
connectedAccountId: string,
|
||||||
triggerConfig?: Record<string, unknown>,
|
triggerConfig?: Record<string, unknown>,
|
||||||
|
|
@ -144,9 +145,8 @@ export async function createComposioTriggerDeployment(request: {
|
||||||
return await createComposioTriggerDeploymentController.execute({
|
return await createComposioTriggerDeploymentController.execute({
|
||||||
caller: 'user',
|
caller: 'user',
|
||||||
userId: user._id,
|
userId: user._id,
|
||||||
|
projectId: request.projectId,
|
||||||
data: {
|
data: {
|
||||||
projectId: request.projectId,
|
|
||||||
toolkitSlug: request.toolkitSlug,
|
|
||||||
triggerTypeSlug: request.triggerTypeSlug,
|
triggerTypeSlug: request.triggerTypeSlug,
|
||||||
connectedAccountId: request.connectedAccountId,
|
connectedAccountId: request.connectedAccountId,
|
||||||
triggerConfig: request.triggerConfig ?? {},
|
triggerConfig: request.triggerConfig ?? {},
|
||||||
|
|
@ -183,3 +183,12 @@ export async function deleteComposioTriggerDeployment(request: {
|
||||||
deploymentId: request.deploymentId,
|
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';
|
'use client';
|
||||||
|
|
||||||
import { Progress, Badge, Chip } from "@heroui/react";
|
import { Progress, Badge, Chip, Spinner } from "@heroui/react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Label } from "@/app/lib/components/label";
|
import { Label } from "@/app/lib/components/label";
|
||||||
import { Customer, UsageResponse } from "@/app/lib/types/billing_types";
|
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 { WithStringId } from "@/app/lib/types/types";
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { getCustomerPortalUrl } from "../actions/billing.actions";
|
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 = {
|
const planDetails = {
|
||||||
free: {
|
free: {
|
||||||
|
|
@ -46,6 +49,14 @@ export function BillingPage({ customer, usage }: BillingPageProps) {
|
||||||
const plan = customer.subscriptionPlan || "free";
|
const plan = customer.subscriptionPlan || "free";
|
||||||
const displayStatus = getDisplayStatus(customer.subscriptionStatus);
|
const displayStatus = getDisplayStatus(customer.subscriptionStatus);
|
||||||
const planInfo = planDetails[plan];
|
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
|
// Prepare usage metrics data
|
||||||
const usageData = Object.entries(usage.usage)
|
const usageData = Object.entries(usage.usage)
|
||||||
|
|
@ -57,6 +68,7 @@ export function BillingPage({ customer, usage }: BillingPageProps) {
|
||||||
.sort((a, b) => b.credits - a.credits);
|
.sort((a, b) => b.credits - a.credits);
|
||||||
|
|
||||||
async function handleManageSubscription() {
|
async function handleManageSubscription() {
|
||||||
|
setLoading(true);
|
||||||
const returnUrl = new URL('/billing/callback', window.location.origin);
|
const returnUrl = new URL('/billing/callback', window.location.origin);
|
||||||
returnUrl.searchParams.set('redirect', window.location.href);
|
returnUrl.searchParams.set('redirect', window.location.href);
|
||||||
const url = await getCustomerPortalUrl(returnUrl.toString());
|
const url = await getCustomerPortalUrl(returnUrl.toString());
|
||||||
|
|
@ -105,15 +117,34 @@ export function BillingPage({ customer, usage }: BillingPageProps) {
|
||||||
</Chip>
|
</Chip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form action={handleManageSubscription}>
|
<div className="flex flex-col items-end gap-2 min-w-[200px]">
|
||||||
<Button
|
{(plan === "free" || plan === "starter") && (
|
||||||
variant="primary"
|
<Button
|
||||||
size="md"
|
variant="primary"
|
||||||
type="submit"
|
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
|
Manage Subscription
|
||||||
</Button>
|
</a>}
|
||||||
</form>
|
{loading && <Spinner size="sm" />}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -136,7 +167,7 @@ export function BillingPage({ customer, usage }: BillingPageProps) {
|
||||||
tokens.colors.light.text.primary,
|
tokens.colors.light.text.primary,
|
||||||
tokens.colors.dark.text.primary
|
tokens.colors.dark.text.primary
|
||||||
)}>
|
)}>
|
||||||
{usage.sanctionedCredits.toLocaleString()}
|
{sanctionedCredits.toLocaleString()}
|
||||||
</p>
|
</p>
|
||||||
<p className={clsx(
|
<p className={clsx(
|
||||||
tokens.typography.sizes.sm,
|
tokens.typography.sizes.sm,
|
||||||
|
|
@ -154,7 +185,7 @@ export function BillingPage({ customer, usage }: BillingPageProps) {
|
||||||
tokens.colors.light.text.primary,
|
tokens.colors.light.text.primary,
|
||||||
tokens.colors.dark.text.primary
|
tokens.colors.dark.text.primary
|
||||||
)}>
|
)}>
|
||||||
{(usage.sanctionedCredits - usage.availableCredits).toLocaleString()}
|
{usedCredits.toLocaleString()}
|
||||||
</p>
|
</p>
|
||||||
<p className={clsx(
|
<p className={clsx(
|
||||||
tokens.typography.sizes.sm,
|
tokens.typography.sizes.sm,
|
||||||
|
|
@ -174,7 +205,7 @@ export function BillingPage({ customer, usage }: BillingPageProps) {
|
||||||
tokens.colors.dark.text.primary
|
tokens.colors.dark.text.primary
|
||||||
)
|
)
|
||||||
)}>
|
)}>
|
||||||
{usage.availableCredits.toLocaleString()}
|
{availableCredits.toLocaleString()}
|
||||||
</p>
|
</p>
|
||||||
<p className={clsx(
|
<p className={clsx(
|
||||||
tokens.typography.sizes.sm,
|
tokens.typography.sizes.sm,
|
||||||
|
|
@ -237,7 +268,7 @@ export function BillingPage({ customer, usage }: BillingPageProps) {
|
||||||
<section className="card">
|
<section className="card">
|
||||||
<div className="px-4 pt-4 pb-6">
|
<div className="px-4 pt-4 pb-6">
|
||||||
<SectionHeading>
|
<SectionHeading>
|
||||||
Usage data
|
Usage split
|
||||||
</SectionHeading>
|
</SectionHeading>
|
||||||
</div>
|
</div>
|
||||||
<HorizontalDivider />
|
<HorizontalDivider />
|
||||||
|
|
@ -261,13 +292,13 @@ export function BillingPage({ customer, usage }: BillingPageProps) {
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label label={type.replace(/_/g, ' ')} />
|
<Label label={type.replace(/_/g, ' ')} />
|
||||||
<p className={clsx(
|
{/* <p className={clsx(
|
||||||
tokens.typography.sizes.sm,
|
tokens.typography.sizes.sm,
|
||||||
tokens.colors.light.text.secondary,
|
tokens.colors.light.text.secondary,
|
||||||
tokens.colors.dark.text.secondary
|
tokens.colors.dark.text.secondary
|
||||||
)}>
|
)}>
|
||||||
{credits.toLocaleString()} credits
|
{credits.toLocaleString()} credits
|
||||||
</p>
|
</p> */}
|
||||||
</div>
|
</div>
|
||||||
<span className={clsx(
|
<span className={clsx(
|
||||||
tokens.typography.sizes.sm,
|
tokens.typography.sizes.sm,
|
||||||
|
|
@ -289,6 +320,11 @@ export function BillingPage({ customer, usage }: BillingPageProps) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<BillingUpgradeModal
|
||||||
|
isOpen={upgradeModalOpen}
|
||||||
|
onClose={() => setUpgradeModalOpen(false)}
|
||||||
|
errorMessage={upgradeError}
|
||||||
|
/>
|
||||||
</div>
|
</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 { Tabs, Tab } from "@/components/ui/tabs";
|
||||||
import { ScheduledJobRulesList } from "../scheduled/components/scheduled-job-rules-list";
|
import { ScheduledJobRulesList } from "../scheduled/components/scheduled-job-rules-list";
|
||||||
import { RecurringJobRulesList } from "./recurring-job-rules-list";
|
import { RecurringJobRulesList } from "./recurring-job-rules-list";
|
||||||
|
import { TriggersTab } from "./triggers-tab";
|
||||||
|
|
||||||
export function JobRulesTabs({ projectId }: { projectId: string }) {
|
export function JobRulesTabs({ projectId }: { projectId: string }) {
|
||||||
const [activeTab, setActiveTab] = useState<string>("scheduled");
|
const [activeTab, setActiveTab] = useState<string>("triggers");
|
||||||
|
|
||||||
const handleTabChange = (key: React.Key) => {
|
const handleTabChange = (key: React.Key) => {
|
||||||
setActiveTab(key.toString());
|
setActiveTab(key.toString());
|
||||||
|
|
@ -20,10 +21,13 @@ export function JobRulesTabs({ projectId }: { projectId: string }) {
|
||||||
aria-label="Job Rules"
|
aria-label="Job Rules"
|
||||||
fullWidth
|
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} />
|
<ScheduledJobRulesList projectId={projectId} />
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab key="recurring" title="Recurring Rules">
|
<Tab key="recurring" title="Recurring Triggers">
|
||||||
<RecurringJobRulesList projectId={projectId} />
|
<RecurringJobRulesList projectId={projectId} />
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { Link, Spinner } from "@heroui/react";
|
import { Link, Spinner } from "@heroui/react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Panel } from "@/components/common/panel-common";
|
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 { z } from "zod";
|
||||||
import { ListedRecurringRuleItem } from "@/src/application/repositories/recurring-job-rules.repository.interface";
|
import { ListedRecurringRuleItem } from "@/src/application/repositories/recurring-job-rules.repository.interface";
|
||||||
import { isToday, isThisWeek, isThisMonth } from "@/lib/utils/date";
|
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>;
|
type ListedItem = z.infer<typeof ListedRecurringRuleItem>;
|
||||||
|
|
||||||
|
|
@ -18,6 +18,7 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [loadingMore, setLoadingMore] = useState<boolean>(false);
|
const [loadingMore, setLoadingMore] = useState<boolean>(false);
|
||||||
const [hasMore, setHasMore] = useState<boolean>(false);
|
const [hasMore, setHasMore] = useState<boolean>(false);
|
||||||
|
const [deletingRule, setDeletingRule] = useState<string | null>(null);
|
||||||
|
|
||||||
const fetchPage = useCallback(async (cursorArg?: string | null) => {
|
const fetchPage = useCallback(async (cursorArg?: string | null) => {
|
||||||
const res = await listRecurringJobRules({ projectId, cursor: cursorArg ?? undefined, limit: 20 });
|
const res = await listRecurringJobRules({ projectId, cursor: cursorArg ?? undefined, limit: 20 });
|
||||||
|
|
@ -48,6 +49,24 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
|
||||||
setLoadingMore(false);
|
setLoadingMore(false);
|
||||||
}, [cursor, fetchPage]);
|
}, [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 sections = useMemo(() => {
|
||||||
const groups: Record<string, ListedItem[]> = {
|
const groups: Record<string, ListedItem[]> = {
|
||||||
Today: [],
|
Today: [],
|
||||||
|
|
@ -109,18 +128,15 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
|
||||||
return (
|
return (
|
||||||
<Panel
|
<Panel
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center gap-3">
|
<div className="text-base font-normal text-gray-900 dark:text-gray-100">
|
||||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
Run your assistant workflow on an automated repeating schedule (cron jobs).
|
||||||
RECURRING JOB RULES
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
rightActions={
|
rightActions={
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link href={`/projects/${projectId}/job-rules/recurring/new`}>
|
<Link href={`/projects/${projectId}/job-rules/recurring/new`}>
|
||||||
<Button size="sm" className="flex items-center gap-2">
|
<Button size="sm" className="whitespace-nowrap" startContent={<PlusIcon className="w-4 h-4" />}>
|
||||||
<PlusIcon className="w-4 h-4" />
|
New Recurring Trigger
|
||||||
New Rule
|
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -145,38 +161,48 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid gap-3">
|
<div className="grid gap-3">
|
||||||
{sectionItems.map((item) => (
|
{sectionItems.map((item) => (
|
||||||
<Link
|
<div
|
||||||
key={item.id}
|
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"
|
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-1">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<Link
|
||||||
<span className={`text-sm font-medium ${getStatusColor(item.disabled, item.lastError || null)}`}>
|
href={`/projects/${projectId}/job-rules/recurring/${item.id}`}
|
||||||
{getStatusText(item.disabled, item.lastError || null)}
|
className="block"
|
||||||
</span>
|
>
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
Next run: {formatNextRunAt(item.nextRunAt)}
|
<span className={`text-sm font-medium ${getStatusColor(item.disabled, item.lastError || null)}`}>
|
||||||
</span>
|
{getStatusText(item.disabled, item.lastError || null)}
|
||||||
</div>
|
</span>
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">
|
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
Schedule: {formatCronExpression(item.cron)}
|
Next run: {formatNextRunAt(item.nextRunAt)}
|
||||||
</div>
|
</span>
|
||||||
<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>
|
</div>
|
||||||
)}
|
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">
|
||||||
</div>
|
Schedule: {formatCronExpression(item.cron)}
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
</div>
|
||||||
{new Date(item.createdAt).toLocaleDateString()}
|
<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>
|
</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>
|
</div>
|
||||||
</Link>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -184,7 +210,7 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
|
||||||
})}
|
})}
|
||||||
{items.length === 0 && !loading && (
|
{items.length === 0 && !loading && (
|
||||||
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hasMore && (
|
{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";
|
import { JobRulesTabs } from "./components/job-rules-tabs";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Job Rules",
|
title: "Triggers",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function Page(
|
export default async function Page(
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { Link, Spinner } from "@heroui/react";
|
import { Link, Spinner } from "@heroui/react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Panel } from "@/components/common/panel-common";
|
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 { z } from "zod";
|
||||||
import { ListedRuleItem } from "@/src/application/repositories/scheduled-job-rules.repository.interface";
|
import { ListedRuleItem } from "@/src/application/repositories/scheduled-job-rules.repository.interface";
|
||||||
import { isToday, isThisWeek, isThisMonth } from "@/lib/utils/date";
|
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>;
|
type ListedItem = z.infer<typeof ListedRuleItem>;
|
||||||
|
|
||||||
|
|
@ -18,6 +18,7 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [loadingMore, setLoadingMore] = useState<boolean>(false);
|
const [loadingMore, setLoadingMore] = useState<boolean>(false);
|
||||||
const [hasMore, setHasMore] = useState<boolean>(false);
|
const [hasMore, setHasMore] = useState<boolean>(false);
|
||||||
|
const [deletingRule, setDeletingRule] = useState<string | null>(null);
|
||||||
|
|
||||||
const fetchPage = useCallback(async (cursorArg?: string | null) => {
|
const fetchPage = useCallback(async (cursorArg?: string | null) => {
|
||||||
const res = await listScheduledJobRules({ projectId, cursor: cursorArg ?? undefined, limit: 20 });
|
const res = await listScheduledJobRules({ projectId, cursor: cursorArg ?? undefined, limit: 20 });
|
||||||
|
|
@ -48,6 +49,24 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
|
||||||
setLoadingMore(false);
|
setLoadingMore(false);
|
||||||
}, [cursor, fetchPage]);
|
}, [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 sections = useMemo(() => {
|
||||||
const groups: Record<string, ListedItem[]> = {
|
const groups: Record<string, ListedItem[]> = {
|
||||||
Today: [],
|
Today: [],
|
||||||
|
|
@ -87,18 +106,15 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
|
||||||
return (
|
return (
|
||||||
<Panel
|
<Panel
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center gap-3">
|
<div className="text-base font-normal text-gray-900 dark:text-gray-100">
|
||||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
Schedule a single job to run your assistant workflow at a specific date and time.
|
||||||
SCHEDULED JOB RULES
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
rightActions={
|
rightActions={
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link href={`/projects/${projectId}/job-rules/scheduled/new`}>
|
<Link href={`/projects/${projectId}/job-rules/scheduled/new`}>
|
||||||
<Button size="sm" className="flex items-center gap-2">
|
<Button size="sm" className="whitespace-nowrap" startContent={<PlusIcon className="w-4 h-4" />}>
|
||||||
<PlusIcon className="w-4 h-4" />
|
New One-time Trigger
|
||||||
New Rule
|
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -123,30 +139,40 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid gap-3">
|
<div className="grid gap-3">
|
||||||
{sectionItems.map((item) => (
|
{sectionItems.map((item) => (
|
||||||
<Link
|
<div
|
||||||
key={item.id}
|
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"
|
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-1">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<Link
|
||||||
<span className={`text-sm font-medium ${getStatusColor(item.status, item.processedAt || null)}`}>
|
href={`/projects/${projectId}/job-rules/scheduled/${item.id}`}
|
||||||
{getStatusText(item.status, item.processedAt || null)}
|
className="block"
|
||||||
</span>
|
>
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
Next run: {formatNextRunAt(item.nextRunAt)}
|
<span className={`text-sm font-medium ${getStatusColor(item.status, item.processedAt || null)}`}>
|
||||||
</span>
|
{getStatusText(item.status, item.processedAt || null)}
|
||||||
</div>
|
</span>
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
Created: {new Date(item.createdAt).toLocaleDateString()}
|
Next run: {formatNextRunAt(item.nextRunAt)}
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
{new Date(item.createdAt).toLocaleDateString()}
|
Created: {new Date(item.createdAt).toLocaleDateString()}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</Link>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -154,7 +180,7 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
|
||||||
})}
|
})}
|
||||||
{items.length === 0 && !loading && (
|
{items.length === 0 && !loading && (
|
||||||
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hasMore && (
|
{hasMore && (
|
||||||
|
|
@ -183,3 +209,4 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
|
||||||
</Panel>
|
</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,
|
'Deployment ID': reason.triggerDeploymentId,
|
||||||
},
|
},
|
||||||
payload: reason.payload,
|
payload: reason.payload,
|
||||||
link: null
|
link: reason.triggerDeploymentId ? `/projects/${projectId}/job-rules/triggers/${reason.triggerDeploymentId}` : null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (reason.type === 'scheduled_job_rule') {
|
if (reason.type === 'scheduled_job_rule') {
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ export function JobsList({ projectId, filters, showTitle = true, customTitle }:
|
||||||
return {
|
return {
|
||||||
type: 'Composio Trigger',
|
type: 'Composio Trigger',
|
||||||
display: `Composio: ${reason.triggerTypeSlug}`,
|
display: `Composio: ${reason.triggerTypeSlug}`,
|
||||||
link: null
|
link: reason.triggerDeploymentId ? `/projects/${projectId}/job-rules/triggers/${reason.triggerDeploymentId}` : null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (reason.type === 'scheduled_job_rule') {
|
if (reason.type === 'scheduled_job_rule') {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ interface TopBarProps {
|
||||||
localProjectName: string;
|
localProjectName: string;
|
||||||
projectNameError: string | null;
|
projectNameError: string | null;
|
||||||
onProjectNameChange: (value: string) => void;
|
onProjectNameChange: (value: string) => void;
|
||||||
onProjectNameCommit: (value: string) => void;
|
onProjectNameCommit: (value: string) => Promise<void>;
|
||||||
publishing: boolean;
|
publishing: boolean;
|
||||||
isLive: boolean;
|
isLive: boolean;
|
||||||
showCopySuccess: boolean;
|
showCopySuccess: boolean;
|
||||||
|
|
@ -23,7 +23,6 @@ interface TopBarProps {
|
||||||
onRevertToLive: () => void;
|
onRevertToLive: () => void;
|
||||||
onToggleCopilot: () => void;
|
onToggleCopilot: () => void;
|
||||||
onSettingsModalOpen: () => void;
|
onSettingsModalOpen: () => void;
|
||||||
onTriggersModalOpen: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TopBar({
|
export function TopBar({
|
||||||
|
|
@ -45,7 +44,6 @@ export function TopBar({
|
||||||
onRevertToLive,
|
onRevertToLive,
|
||||||
onToggleCopilot,
|
onToggleCopilot,
|
||||||
onSettingsModalOpen,
|
onSettingsModalOpen,
|
||||||
onTriggersModalOpen,
|
|
||||||
}: TopBarProps) {
|
}: TopBarProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
@ -168,16 +166,9 @@ export function TopBar({
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key="manage-triggers"
|
key="manage-triggers"
|
||||||
startContent={<ZapIcon size={16} />}
|
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`); } }}
|
onPress={() => { if (projectId) { router.push(`/projects/${projectId}/job-rules`); } }}
|
||||||
>
|
>
|
||||||
Go to schedule runs
|
Manage triggers
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
{!isLive ? (
|
{!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 { ConfigApp } from "../config/app";
|
||||||
import { InputField } from "@/app/lib/components/input-field";
|
import { InputField } from "@/app/lib/components/input-field";
|
||||||
import { VoiceSection } from "../config/components/voice";
|
import { VoiceSection } from "../config/components/voice";
|
||||||
import { TriggersModal } from "./components/TriggersModal";
|
|
||||||
import { TopBar } from "./components/TopBar";
|
import { TopBar } from "./components/TopBar";
|
||||||
|
|
||||||
enablePatches();
|
enablePatches();
|
||||||
|
|
@ -882,9 +881,6 @@ export function WorkflowEditor({
|
||||||
// Modal state for chat widget configuration
|
// Modal state for chat widget configuration
|
||||||
const { isOpen: isChatWidgetModalOpen, onOpen: onChatWidgetModalOpen, onClose: onChatWidgetModalClose } = useDisclosure();
|
const { isOpen: isChatWidgetModalOpen, onOpen: onChatWidgetModalOpen, onClose: onChatWidgetModalClose } = useDisclosure();
|
||||||
|
|
||||||
// Modal state for triggers management
|
|
||||||
const { isOpen: isTriggersModalOpen, onOpen: onTriggersModalOpen, onClose: onTriggersModalClose } = useDisclosure();
|
|
||||||
|
|
||||||
// Project name state
|
// Project name state
|
||||||
const [localProjectName, setLocalProjectName] = useState<string>(projectConfig.name || '');
|
const [localProjectName, setLocalProjectName] = useState<string>(projectConfig.name || '');
|
||||||
const [projectNameError, setProjectNameError] = useState<string | null>(null);
|
const [projectNameError, setProjectNameError] = useState<string | null>(null);
|
||||||
|
|
@ -1285,7 +1281,6 @@ export function WorkflowEditor({
|
||||||
onRevertToLive={handleRevertToLive}
|
onRevertToLive={handleRevertToLive}
|
||||||
onToggleCopilot={() => setShowCopilot(!showCopilot)}
|
onToggleCopilot={() => setShowCopilot(!showCopilot)}
|
||||||
onSettingsModalOpen={onSettingsModalOpen}
|
onSettingsModalOpen={onSettingsModalOpen}
|
||||||
onTriggersModalOpen={onTriggersModalOpen}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Content Area */}
|
{/* Content Area */}
|
||||||
|
|
@ -1565,14 +1560,6 @@ export function WorkflowEditor({
|
||||||
</Modal>
|
</Modal>
|
||||||
*/}
|
*/}
|
||||||
|
|
||||||
{/* Triggers Management Modal */}
|
|
||||||
<TriggersModal
|
|
||||||
isOpen={isTriggersModalOpen}
|
|
||||||
onClose={onTriggersModalClose}
|
|
||||||
projectId={projectId}
|
|
||||||
projectConfig={projectConfig}
|
|
||||||
onProjectConfigUpdated={onProjectConfigUpdated}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</EntitySelectionContext.Provider>
|
</EntitySelectionContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
import { listTemplates, listProjects } from "@/app/actions/project.actions";
|
import { listTemplates, listProjects } from "@/app/actions/project.actions";
|
||||||
import { createProjectWithOptions, createProjectFromJsonWithOptions, createProjectFromTemplate } from "../lib/project-creation-utils";
|
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 clsx from 'clsx';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import mascotImage from '@/public/mascot.png';
|
import mascotImage from '@/public/mascot.png';
|
||||||
|
|
@ -36,6 +36,8 @@ export function BuildAssistantSection() {
|
||||||
const [selectedTab, setSelectedTab] = useState('new');
|
const [selectedTab, setSelectedTab] = useState('new');
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const [autoCreateLoading, setAutoCreateLoading] = useState(false);
|
||||||
|
|
||||||
const totalPages = Math.ceil(projects.length / ITEMS_PER_PAGE);
|
const totalPages = Math.ceil(projects.length / ITEMS_PER_PAGE);
|
||||||
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
|
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
|
||||||
|
|
@ -103,6 +105,29 @@ export function BuildAssistantSection() {
|
||||||
fetchProjects();
|
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 () => {
|
const handleCreateAssistant = async () => {
|
||||||
setIsCreating(true);
|
setIsCreating(true);
|
||||||
try {
|
try {
|
||||||
|
|
@ -170,6 +195,15 @@ export function BuildAssistantSection() {
|
||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={handleFileChange}
|
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="px-8 py-16">
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
{/* Main Headline */}
|
{/* Main Headline */}
|
||||||
|
|
@ -445,6 +479,7 @@ export function BuildAssistantSection() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
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 { usePathname } from "next/navigation";
|
||||||
import { Tooltip, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/react";
|
import { Tooltip, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/react";
|
||||||
import { UserButton } from "@/app/lib/components/user_button";
|
import { UserButton } from "@/app/lib/components/user_button";
|
||||||
|
|
@ -17,7 +18,8 @@ import {
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
MessageSquareIcon,
|
MessageSquareIcon,
|
||||||
LogsIcon,
|
LogsIcon,
|
||||||
Clock
|
Clock,
|
||||||
|
ZapIcon
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { fetchProject } from "@/app/actions/project.actions";
|
import { fetchProject } from "@/app/actions/project.actions";
|
||||||
import { createProjectWithOptions } from "../../lib/project-creation-utils";
|
import { createProjectWithOptions } from "../../lib/project-creation-utils";
|
||||||
|
|
@ -102,6 +104,12 @@ export default function Sidebar({ projectId, useAuth, collapsed = false, onToggl
|
||||||
icon: WorkflowIcon,
|
icon: WorkflowIcon,
|
||||||
requiresProject: true
|
requiresProject: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: 'job-rules',
|
||||||
|
label: 'Triggers',
|
||||||
|
icon: ZapIcon,
|
||||||
|
requiresProject: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: 'conversations',
|
href: 'conversations',
|
||||||
label: 'Conversations',
|
label: 'Conversations',
|
||||||
|
|
@ -114,12 +122,6 @@ export default function Sidebar({ projectId, useAuth, collapsed = false, onToggl
|
||||||
icon: LogsIcon,
|
icon: LogsIcon,
|
||||||
requiresProject: true
|
requiresProject: true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
href: 'job-rules',
|
|
||||||
label: 'Job Rules',
|
|
||||||
icon: Clock,
|
|
||||||
requiresProject: true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
href: 'config',
|
href: 'config',
|
||||||
label: 'Settings',
|
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'}
|
${collapsed ? 'py-3' : 'gap-3 px-4 py-2.5 justify-start'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<Image
|
{collapsed && <Image
|
||||||
src={logoImage}
|
src={logoOnly}
|
||||||
alt="Rowboat"
|
alt="Rowboat"
|
||||||
width={collapsed ? 24 : 24}
|
width={24}
|
||||||
height={collapsed ? 24 : 24}
|
height={24}
|
||||||
className="rounded-full transition-all duration-200 flex-shrink-0"
|
/>}
|
||||||
/>
|
{!collapsed && <Image
|
||||||
{!collapsed && (
|
src={logo}
|
||||||
<span className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
|
alt="Rowboat"
|
||||||
Rowboat
|
height={32}
|
||||||
</span>
|
/>}
|
||||||
)}
|
|
||||||
</Link>
|
</Link>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -74,17 +74,18 @@ export function BillingUpgradeModal({ isOpen, onClose, errorMessage }: BillingUp
|
||||||
plan: "starter" as const,
|
plan: "starter" as const,
|
||||||
description: "Great for your personal projects",
|
description: "Great for your personal projects",
|
||||||
features: [
|
features: [
|
||||||
"1000 playground chat requests",
|
"2,000 credits",
|
||||||
"500 copilot requests"
|
"Latest models like gpt-5, claude-4 and others",
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Pro",
|
name: "Pro",
|
||||||
plan: "pro" as const,
|
plan: "pro" as const,
|
||||||
description: "Great for enterprise teams",
|
description: "Great for power users or teams",
|
||||||
features: [
|
features: [
|
||||||
"10000 playground chat requests",
|
"20,000 credits",
|
||||||
"2000 copilot requests"
|
"o3 and o3-pro",
|
||||||
|
"Priority support",
|
||||||
],
|
],
|
||||||
recommended: true
|
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 { 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 { 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 { 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 { 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 { 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";
|
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 { 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 { 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 { 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 { 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 { HandleComposioWebhookRequestController } from "@/src/interface-adapters/controllers/composio/webhook/handle-composio-webhook-request.controller";
|
||||||
import { JobsWorker } from "@/src/application/workers/jobs.worker";
|
import { JobsWorker } from "@/src/application/workers/jobs.worker";
|
||||||
|
|
@ -299,10 +301,12 @@ container.register({
|
||||||
listComposioTriggerTypesUseCase: asClass(ListComposioTriggerTypesUseCase).singleton(),
|
listComposioTriggerTypesUseCase: asClass(ListComposioTriggerTypesUseCase).singleton(),
|
||||||
createComposioTriggerDeploymentUseCase: asClass(CreateComposioTriggerDeploymentUseCase).singleton(),
|
createComposioTriggerDeploymentUseCase: asClass(CreateComposioTriggerDeploymentUseCase).singleton(),
|
||||||
listComposioTriggerDeploymentsUseCase: asClass(ListComposioTriggerDeploymentsUseCase).singleton(),
|
listComposioTriggerDeploymentsUseCase: asClass(ListComposioTriggerDeploymentsUseCase).singleton(),
|
||||||
|
fetchComposioTriggerDeploymentUseCase: asClass(FetchComposioTriggerDeploymentUseCase).singleton(),
|
||||||
deleteComposioTriggerDeploymentUseCase: asClass(DeleteComposioTriggerDeploymentUseCase).singleton(),
|
deleteComposioTriggerDeploymentUseCase: asClass(DeleteComposioTriggerDeploymentUseCase).singleton(),
|
||||||
createComposioTriggerDeploymentController: asClass(CreateComposioTriggerDeploymentController).singleton(),
|
createComposioTriggerDeploymentController: asClass(CreateComposioTriggerDeploymentController).singleton(),
|
||||||
deleteComposioTriggerDeploymentController: asClass(DeleteComposioTriggerDeploymentController).singleton(),
|
deleteComposioTriggerDeploymentController: asClass(DeleteComposioTriggerDeploymentController).singleton(),
|
||||||
listComposioTriggerDeploymentsController: asClass(ListComposioTriggerDeploymentsController).singleton(),
|
listComposioTriggerDeploymentsController: asClass(ListComposioTriggerDeploymentsController).singleton(),
|
||||||
|
fetchComposioTriggerDeploymentController: asClass(FetchComposioTriggerDeploymentController).singleton(),
|
||||||
listComposioTriggerTypesController: asClass(ListComposioTriggerTypesController).singleton(),
|
listComposioTriggerTypesController: asClass(ListComposioTriggerTypesController).singleton(),
|
||||||
|
|
||||||
// conversations
|
// conversations
|
||||||
|
|
|
||||||
|
|
@ -213,3 +213,8 @@ export async function listTriggersTypes(toolkitSlug: string, cursor?: string): P
|
||||||
// fetch
|
// fetch
|
||||||
return composioApiCall(ZListResponse(ZTriggerType), url.toString());
|
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,
|
toolkitSlug: true,
|
||||||
logo: true,
|
logo: true,
|
||||||
triggerTypeSlug: true,
|
triggerTypeSlug: true,
|
||||||
|
triggerTypeName: true,
|
||||||
triggerConfig: true,
|
triggerConfig: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,20 @@ import { BadRequestError, NotFoundError } from '@/src/entities/errors/common';
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { IUsageQuotaPolicy } from '../../policies/usage-quota.policy.interface';
|
import { IUsageQuotaPolicy } from '../../policies/usage-quota.policy.interface';
|
||||||
import { IProjectActionAuthorizationPolicy } from '../../policies/project-action-authorization.policy';
|
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 { 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';
|
import { ComposioTriggerDeployment } from '@/src/entities/models/composio-trigger-deployment';
|
||||||
|
|
||||||
const inputSchema = z.object({
|
const inputSchema = z.object({
|
||||||
caller: z.enum(["user", "api"]),
|
caller: z.enum(["user", "api"]),
|
||||||
userId: z.string().optional(),
|
userId: z.string().optional(),
|
||||||
apiKey: z.string().optional(),
|
apiKey: z.string().optional(),
|
||||||
data: CreateDeploymentSchema.omit({
|
projectId: z.string(),
|
||||||
triggerId: true,
|
data: ComposioTriggerDeployment.pick({
|
||||||
logo: true,
|
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>> {
|
async execute(request: z.infer<typeof inputSchema>): Promise<z.infer<typeof ComposioTriggerDeployment>> {
|
||||||
// extract projectid from conversation
|
// extract projectid from conversation
|
||||||
const { projectId } = request.data;
|
const { projectId } = request;
|
||||||
|
|
||||||
// authz check
|
// authz check
|
||||||
await this.projectActionAuthorizationPolicy.authorize({
|
await this.projectActionAuthorizationPolicy.authorize({
|
||||||
|
|
@ -59,8 +61,11 @@ export class CreateComposioTriggerDeploymentUseCase implements ICreateComposioTr
|
||||||
// assert and consume quota
|
// assert and consume quota
|
||||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||||
|
|
||||||
|
// get trigger type info
|
||||||
|
const triggerType = await getTriggersType(request.data.triggerTypeSlug);
|
||||||
|
|
||||||
// get toolkit info
|
// get toolkit info
|
||||||
const toolkit = await getToolkit(request.data.toolkitSlug);
|
const toolkit = triggerType.toolkit;
|
||||||
|
|
||||||
// ensure that connected account exists on project
|
// ensure that connected account exists on project
|
||||||
const project = await this.projectsRepository.fetch(projectId);
|
const project = await this.projectsRepository.fetch(projectId);
|
||||||
|
|
@ -69,7 +74,7 @@ export class CreateComposioTriggerDeploymentUseCase implements ICreateComposioTr
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure connected account exists
|
// ensure connected account exists
|
||||||
const account = project.composioConnectedAccounts?.[request.data.toolkitSlug];
|
const account = project.composioConnectedAccounts?.[toolkit.slug];
|
||||||
if (!account || account.id !== request.data.connectedAccountId) {
|
if (!account || account.id !== request.data.connectedAccountId) {
|
||||||
throw new BadRequestError('Invalid connected account');
|
throw new BadRequestError('Invalid connected account');
|
||||||
}
|
}
|
||||||
|
|
@ -81,7 +86,7 @@ export class CreateComposioTriggerDeploymentUseCase implements ICreateComposioTr
|
||||||
}
|
}
|
||||||
|
|
||||||
// create trigger on composio
|
// 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,
|
connectedAccountId: request.data.connectedAccountId,
|
||||||
triggerConfig: request.data.triggerConfig,
|
triggerConfig: request.data.triggerConfig,
|
||||||
});
|
});
|
||||||
|
|
@ -89,11 +94,12 @@ export class CreateComposioTriggerDeploymentUseCase implements ICreateComposioTr
|
||||||
// create trigger deployment in db
|
// create trigger deployment in db
|
||||||
return await this.composioTriggerDeploymentsRepository.create({
|
return await this.composioTriggerDeploymentsRepository.create({
|
||||||
projectId,
|
projectId,
|
||||||
toolkitSlug: request.data.toolkitSlug,
|
toolkitSlug: toolkit.slug,
|
||||||
logo: toolkit.meta.logo,
|
logo: toolkit.logo,
|
||||||
triggerId: result.triggerId,
|
triggerId: result.triggerId,
|
||||||
connectedAccountId: request.data.connectedAccountId,
|
connectedAccountId: request.data.connectedAccountId,
|
||||||
triggerTypeSlug: request.data.triggerTypeSlug,
|
triggerTypeSlug: request.data.triggerTypeSlug,
|
||||||
|
triggerTypeName: triggerType.name,
|
||||||
triggerConfig: request.data.triggerConfig,
|
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(),
|
triggerId: z.string(),
|
||||||
toolkitSlug: z.string(),
|
toolkitSlug: z.string(),
|
||||||
triggerTypeSlug: z.string(),
|
triggerTypeSlug: z.string(),
|
||||||
|
triggerTypeName: z.string(),
|
||||||
connectedAccountId: z.string(),
|
connectedAccountId: z.string(),
|
||||||
triggerConfig: z.record(z.string(), z.unknown()),
|
triggerConfig: z.record(z.string(), z.unknown()),
|
||||||
logo: z.string(),
|
logo: z.string(),
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,16 @@ import { BadRequestError } from "@/src/entities/errors/common";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { ICreateComposioTriggerDeploymentUseCase } from "@/src/application/use-cases/composio-trigger-deployments/create-composio-trigger-deployment.use-case";
|
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 { ComposioTriggerDeployment } from "@/src/entities/models/composio-trigger-deployment";
|
||||||
import { CreateDeploymentSchema } from "@/src/application/repositories/composio-trigger-deployments.repository.interface";
|
|
||||||
|
|
||||||
const inputSchema = z.object({
|
const inputSchema = z.object({
|
||||||
caller: z.enum(["user", "api"]),
|
caller: z.enum(["user", "api"]),
|
||||||
userId: z.string().optional(),
|
userId: z.string().optional(),
|
||||||
apiKey: z.string().optional(),
|
apiKey: z.string().optional(),
|
||||||
data: CreateDeploymentSchema.omit({
|
projectId: z.string(),
|
||||||
triggerId: true,
|
data: ComposioTriggerDeployment.pick({
|
||||||
logo: true,
|
triggerTypeSlug: true,
|
||||||
|
connectedAccountId: true,
|
||||||
|
triggerConfig: true,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -35,13 +36,14 @@ export class CreateComposioTriggerDeploymentController implements ICreateComposi
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`);
|
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
|
// execute use case
|
||||||
return await this.createComposioTriggerDeploymentUseCase.execute({
|
return await this.createComposioTriggerDeploymentUseCase.execute({
|
||||||
caller,
|
caller,
|
||||||
userId,
|
userId,
|
||||||
apiKey,
|
apiKey,
|
||||||
|
projectId,
|
||||||
data,
|
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