mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-24 20:28:16 +02:00
Update all triggers to display standard cards for existing triggers with delete buttons
This commit is contained in:
parent
d592354080
commit
e448601046
4 changed files with 381 additions and 153 deletions
|
|
@ -21,13 +21,13 @@ export function JobRulesTabs({ projectId }: { projectId: string }) {
|
||||||
aria-label="Job Rules"
|
aria-label="Job Rules"
|
||||||
fullWidth
|
fullWidth
|
||||||
>
|
>
|
||||||
<Tab key="triggers" title="Triggers">
|
<Tab key="triggers" title="External Triggers">
|
||||||
<TriggersTab projectId={projectId} />
|
<TriggersTab projectId={projectId} />
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab key="scheduled" title="One-time">
|
<Tab key="scheduled" title="One-Time Triggers">
|
||||||
<ScheduledJobRulesList projectId={projectId} />
|
<ScheduledJobRulesList projectId={projectId} />
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab key="recurring" title="Recurring">
|
<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 && (
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import { Button, Spinner, Card, CardBody, CardHeader } from '@heroui/react';
|
import { Spinner } from '@heroui/react';
|
||||||
import { Plus, Trash2, ZapIcon } from 'lucide-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 { z } from 'zod';
|
||||||
import { ComposioTriggerDeployment } from '@/src/entities/models/composio-trigger-deployment';
|
import { ComposioTriggerDeployment } from '@/src/entities/models/composio-trigger-deployment';
|
||||||
import { ComposioTriggerType } from '@/src/entities/models/composio-trigger-type';
|
import { ComposioTriggerType } from '@/src/entities/models/composio-trigger-type';
|
||||||
import { listComposioTriggerDeployments, deleteComposioTriggerDeployment, createComposioTriggerDeployment } from '@/app/actions/composio.actions';
|
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 { SelectComposioToolkit } from '../../tools/components/SelectComposioToolkit';
|
||||||
import { ComposioTriggerTypesPanel } from '../../workflow/components/ComposioTriggerTypesPanel';
|
import { ComposioTriggerTypesPanel } from '../../workflow/components/ComposioTriggerTypesPanel';
|
||||||
import { TriggerConfigForm } from '../../workflow/components/TriggerConfigForm';
|
import { TriggerConfigForm } from '../../workflow/components/TriggerConfigForm';
|
||||||
|
|
@ -28,6 +31,11 @@ export function TriggersTab({ projectId }: { projectId: string }) {
|
||||||
const [isSubmittingTrigger, setIsSubmittingTrigger] = useState(false);
|
const [isSubmittingTrigger, setIsSubmittingTrigger] = useState(false);
|
||||||
const [deletingTrigger, setDeletingTrigger] = useState<string | null>(null);
|
const [deletingTrigger, setDeletingTrigger] = useState<string | null>(null);
|
||||||
const [projectConfig, setProjectConfig] = useState<z.infer<typeof Project> | 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 () => {
|
const loadProjectConfig = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -38,12 +46,56 @@ export function TriggersTab({ projectId }: { projectId: string }) {
|
||||||
}
|
}
|
||||||
}, [projectId]);
|
}, [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 () => {
|
const loadTriggers = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
const response = await listComposioTriggerDeployments({ projectId });
|
const response = await listComposioTriggerDeployments({ projectId });
|
||||||
setTriggers(response.items);
|
setTriggers(response.items);
|
||||||
|
setCursor(response.nextCursor);
|
||||||
|
setHasMore(Boolean(response.nextCursor));
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Error loading triggers:', err);
|
console.error('Error loading triggers:', err);
|
||||||
setError('Failed to load triggers. Please try again.');
|
setError('Failed to load triggers. Please try again.');
|
||||||
|
|
@ -52,6 +104,21 @@ export function TriggersTab({ projectId }: { projectId: string }) {
|
||||||
}
|
}
|
||||||
}, [projectId]);
|
}, [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) => {
|
const handleDeleteTrigger = async (deploymentId: string) => {
|
||||||
if (!window.confirm('Are you sure you want to delete this trigger?')) {
|
if (!window.confirm('Are you sure you want to delete this trigger?')) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -79,6 +146,7 @@ export function TriggersTab({ projectId }: { projectId: string }) {
|
||||||
setSelectedTriggerType(null);
|
setSelectedTriggerType(null);
|
||||||
setShowAuthModal(false);
|
setShowAuthModal(false);
|
||||||
setIsSubmittingTrigger(false);
|
setIsSubmittingTrigger(false);
|
||||||
|
setExpandedTrigger(null); // Reset expanded state
|
||||||
loadTriggers(); // Reload in case any triggers were created
|
loadTriggers(); // Reload in case any triggers were created
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -157,106 +225,217 @@ export function TriggersTab({ projectId }: { projectId: string }) {
|
||||||
}
|
}
|
||||||
}, [showCreateFlow, loadTriggers]);
|
}, [showCreateFlow, loadTriggers]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (triggers.length > 0) {
|
||||||
|
loadTriggerTypeNames();
|
||||||
|
}
|
||||||
|
}, [triggers, loadTriggerTypeNames]);
|
||||||
|
|
||||||
const renderTriggerList = () => {
|
const renderTriggerList = () => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center py-8">
|
<Panel
|
||||||
<Spinner size="lg" />
|
title={
|
||||||
<span className="ml-2">Loading triggers...</span>
|
<div className="text-base font-normal text-gray-900 dark:text-gray-100">
|
||||||
</div>
|
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) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-8">
|
<Panel
|
||||||
<p className="text-red-500 mb-4">{error}</p>
|
title={
|
||||||
<Button variant="flat" onPress={loadTriggers}>
|
<div className="text-base font-normal text-gray-900 dark:text-gray-100">
|
||||||
Try Again
|
Error loading your triggers
|
||||||
</Button>
|
</div>
|
||||||
</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) {
|
if (triggers.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-12">
|
<Panel
|
||||||
<ZapIcon className="w-16 h-16 mx-auto text-gray-400 mb-4" />
|
title={
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
<div className="text-base font-normal text-gray-900 dark:text-gray-100">
|
||||||
No triggers configured
|
Listen for events from connected apps to run your assistant workflow automatically.
|
||||||
</h3>
|
</div>
|
||||||
<p className="text-gray-500 dark:text-gray-400 mb-6">
|
}
|
||||||
Set up your first trigger to listen for events from your connected apps.
|
rightActions={
|
||||||
</p>
|
<Button
|
||||||
<Button
|
variant="primary"
|
||||||
color="primary"
|
startContent={<Plus className="w-4 h-4" />}
|
||||||
variant="solid"
|
onClick={handleCreateNew}
|
||||||
startContent={<Plus className="w-4 h-4" />}
|
className="whitespace-nowrap"
|
||||||
onPress={handleCreateNew}
|
>
|
||||||
>
|
New External Trigger
|
||||||
Create your first trigger
|
</Button>
|
||||||
</Button>
|
}
|
||||||
</div>
|
>
|
||||||
|
<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 (
|
return (
|
||||||
<div className="space-y-4">
|
<Panel
|
||||||
<div className="flex justify-between items-center">
|
title={
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
<div className="text-base font-normal text-gray-900 dark:text-gray-100">
|
||||||
Active Triggers ({triggers.length})
|
Listen for events from connected apps to run your assistant workflow automatically.
|
||||||
</h3>
|
</div>
|
||||||
|
}
|
||||||
|
rightActions={
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
variant="primary"
|
||||||
variant="solid"
|
|
||||||
startContent={<Plus className="w-4 h-4" />}
|
startContent={<Plus className="w-4 h-4" />}
|
||||||
onPress={handleCreateNew}
|
onClick={handleCreateNew}
|
||||||
|
className="whitespace-nowrap"
|
||||||
>
|
>
|
||||||
Create New Trigger
|
New External Trigger
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
}
|
||||||
|
>
|
||||||
<div className="space-y-3">
|
<div className="h-full overflow-auto px-4 py-4">
|
||||||
{triggers.map((trigger) => (
|
<div className="max-w-[1024px] mx-auto">
|
||||||
<Card key={trigger.id} className="w-full">
|
<div className="flex flex-col gap-6">
|
||||||
<CardHeader className="flex justify-between items-start">
|
{Object.entries(sections).map(([sectionName, sectionTriggers]) => {
|
||||||
<div>
|
if (sectionTriggers.length === 0) return null;
|
||||||
<h4 className="text-base font-medium text-gray-900 dark:text-gray-100">
|
return (
|
||||||
{trigger.triggerTypeSlug}
|
<div key={sectionName} className="space-y-3">
|
||||||
</h4>
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
{sectionName}
|
||||||
Created {new Date(trigger.createdAt).toLocaleDateString()}
|
</h3>
|
||||||
</p>
|
<div className="grid gap-3">
|
||||||
</div>
|
{sectionTriggers.map((trigger) => (
|
||||||
<Button
|
<div
|
||||||
isIconOnly
|
key={trigger.id}
|
||||||
variant="light"
|
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"
|
||||||
color="danger"
|
>
|
||||||
size="sm"
|
<div className="flex items-start justify-between">
|
||||||
isLoading={deletingTrigger === trigger.id}
|
<div className="flex-1">
|
||||||
onPress={() => handleDeleteTrigger(trigger.id)}
|
<div className="flex items-center gap-3 mb-2">
|
||||||
>
|
<span className="text-sm font-medium text-green-600 dark:text-green-400">
|
||||||
<Trash2 className="w-4 h-4" />
|
Active
|
||||||
</Button>
|
</span>
|
||||||
</CardHeader>
|
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
<CardBody className="pt-0">
|
{triggerTypeNames[trigger.triggerTypeSlug] || trigger.triggerTypeSlug}
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-300">
|
</span>
|
||||||
<p><strong>Trigger ID:</strong> {trigger.triggerId}</p>
|
</div>
|
||||||
<p><strong>Connected Account:</strong> {trigger.connectedAccountId}</p>
|
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{Object.keys(trigger.triggerConfig).length > 0 && (
|
Created: {new Date(trigger.createdAt).toLocaleDateString()}
|
||||||
<div className="mt-2">
|
</div>
|
||||||
<strong>Configuration:</strong>
|
{Object.keys(trigger.triggerConfig).length > 0 && (
|
||||||
<pre className="mt-1 text-xs bg-gray-100 dark:bg-gray-800 p-2 rounded">
|
<div className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||||
{JSON.stringify(trigger.triggerConfig, null, 2)}
|
Configuration: {Object.keys(trigger.triggerConfig).length} settings
|
||||||
</pre>
|
</div>
|
||||||
|
)}
|
||||||
|
</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>
|
||||||
)}
|
</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>
|
||||||
</CardBody>
|
)}
|
||||||
</Card>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Panel>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -288,8 +467,8 @@ export function TriggersTab({ projectId }: { projectId: string }) {
|
||||||
Select a Toolkit to Create Trigger
|
Select a Toolkit to Create Trigger
|
||||||
</h3>
|
</h3>
|
||||||
<Button
|
<Button
|
||||||
variant="flat"
|
variant="secondary"
|
||||||
onPress={handleBackToList}
|
onClick={handleBackToList}
|
||||||
>
|
>
|
||||||
← Back to Triggers
|
← Back to Triggers
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -320,11 +499,7 @@ export function TriggersTab({ projectId }: { projectId: string }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="h-full overflow-auto px-4 py-4">
|
{showCreateFlow ? renderCreateFlow() : renderTriggerList()}
|
||||||
<div className="max-w-[1024px] mx-auto">
|
|
||||||
{showCreateFlow ? renderCreateFlow() : renderTriggerList()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Auth Modal */}
|
{/* Auth Modal */}
|
||||||
{selectedToolkit && (
|
{selectedToolkit && (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue