mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-30 19:06:23 +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"
|
||||
fullWidth
|
||||
>
|
||||
<Tab key="triggers" title="Triggers">
|
||||
<Tab key="triggers" title="External Triggers">
|
||||
<TriggersTab projectId={projectId} />
|
||||
</Tab>
|
||||
<Tab key="scheduled" title="One-time">
|
||||
<Tab key="scheduled" title="One-Time Triggers">
|
||||
<ScheduledJobRulesList projectId={projectId} />
|
||||
</Tab>
|
||||
<Tab key="recurring" title="Recurring">
|
||||
<Tab key="recurring" title="Recurring Triggers">
|
||||
<RecurringJobRulesList projectId={projectId} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|||
import { Link, Spinner } from "@heroui/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Panel } from "@/components/common/panel-common";
|
||||
import { listRecurringJobRules } from "@/app/actions/recurring-job-rules.actions";
|
||||
import { listRecurringJobRules, deleteRecurringJobRule } from "@/app/actions/recurring-job-rules.actions";
|
||||
import { z } from "zod";
|
||||
import { ListedRecurringRuleItem } from "@/src/application/repositories/recurring-job-rules.repository.interface";
|
||||
import { isToday, isThisWeek, isThisMonth } from "@/lib/utils/date";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { PlusIcon, Trash2 } from "lucide-react";
|
||||
|
||||
type ListedItem = z.infer<typeof ListedRecurringRuleItem>;
|
||||
|
||||
|
|
@ -18,6 +18,7 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
|
|||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [loadingMore, setLoadingMore] = useState<boolean>(false);
|
||||
const [hasMore, setHasMore] = useState<boolean>(false);
|
||||
const [deletingRule, setDeletingRule] = useState<string | null>(null);
|
||||
|
||||
const fetchPage = useCallback(async (cursorArg?: string | null) => {
|
||||
const res = await listRecurringJobRules({ projectId, cursor: cursorArg ?? undefined, limit: 20 });
|
||||
|
|
@ -48,6 +49,24 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
|
|||
setLoadingMore(false);
|
||||
}, [cursor, fetchPage]);
|
||||
|
||||
const handleDeleteRule = async (ruleId: string) => {
|
||||
if (!window.confirm('Are you sure you want to delete this recurring trigger?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setDeletingRule(ruleId);
|
||||
await deleteRecurringJobRule({ projectId, ruleId });
|
||||
// Remove the deleted item from the list
|
||||
setItems(prev => prev.filter(item => item.id !== ruleId));
|
||||
} catch (err: any) {
|
||||
console.error('Error deleting recurring trigger:', err);
|
||||
alert('Failed to delete recurring trigger. Please try again.');
|
||||
} finally {
|
||||
setDeletingRule(null);
|
||||
}
|
||||
};
|
||||
|
||||
const sections = useMemo(() => {
|
||||
const groups: Record<string, ListedItem[]> = {
|
||||
Today: [],
|
||||
|
|
@ -109,18 +128,15 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
|
|||
return (
|
||||
<Panel
|
||||
title={
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
RECURRING JOB RULES
|
||||
</div>
|
||||
<div className="text-base font-normal text-gray-900 dark:text-gray-100">
|
||||
Run your assistant workflow on an automated repeating schedule (cron jobs).
|
||||
</div>
|
||||
}
|
||||
rightActions={
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href={`/projects/${projectId}/job-rules/recurring/new`}>
|
||||
<Button size="sm" className="flex items-center gap-2">
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
New Rule
|
||||
<Button size="sm" className="whitespace-nowrap" startContent={<PlusIcon className="w-4 h-4" />}>
|
||||
New Recurring Trigger
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
|
@ -145,38 +161,48 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
|
|||
</h3>
|
||||
<div className="grid gap-3">
|
||||
{sectionItems.map((item) => (
|
||||
<Link
|
||||
<div
|
||||
key={item.id}
|
||||
href={`/projects/${projectId}/job-rules/recurring/${item.id}`}
|
||||
className="block p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className={`text-sm font-medium ${getStatusColor(item.disabled, item.lastError || null)}`}>
|
||||
{getStatusText(item.disabled, item.lastError || null)}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Next run: {formatNextRunAt(item.nextRunAt)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">
|
||||
Schedule: {formatCronExpression(item.cron)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Created: {new Date(item.createdAt).toLocaleDateString()}
|
||||
</div>
|
||||
{item.lastError && (
|
||||
<div className="text-sm text-red-600 dark:text-red-400 mt-1">
|
||||
Last error: {item.lastError}
|
||||
<Link
|
||||
href={`/projects/${projectId}/job-rules/recurring/${item.id}`}
|
||||
className="block"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className={`text-sm font-medium ${getStatusColor(item.disabled, item.lastError || null)}`}>
|
||||
{getStatusText(item.disabled, item.lastError || null)}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Next run: {formatNextRunAt(item.nextRunAt)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{new Date(item.createdAt).toLocaleDateString()}
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">
|
||||
Schedule: {formatCronExpression(item.cron)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Created: {new Date(item.createdAt).toLocaleDateString()}
|
||||
</div>
|
||||
{item.lastError && (
|
||||
<div className="text-sm text-red-600 dark:text-red-400 mt-1">
|
||||
Last error: {item.lastError}
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
size="sm"
|
||||
isLoading={deletingRule === item.id}
|
||||
onClick={() => handleDeleteRule(item.id)}
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:text-red-400 dark:hover:text-red-300 dark:hover:bg-red-950"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -184,7 +210,7 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
|
|||
})}
|
||||
{items.length === 0 && !loading && (
|
||||
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
No recurring job rules found. Create your first rule to get started.
|
||||
No recurring triggers yet. Create your first recurring trigger to get started.
|
||||
</div>
|
||||
)}
|
||||
{hasMore && (
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Button, Spinner, Card, CardBody, CardHeader } from '@heroui/react';
|
||||
import { Plus, Trash2, ZapIcon } from 'lucide-react';
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { Spinner } 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 { 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 { ComposioTriggerTypesPanel } from '../../workflow/components/ComposioTriggerTypesPanel';
|
||||
import { TriggerConfigForm } from '../../workflow/components/TriggerConfigForm';
|
||||
|
|
@ -28,6 +31,11 @@ export function TriggersTab({ projectId }: { projectId: string }) {
|
|||
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 {
|
||||
|
|
@ -38,12 +46,56 @@ export function TriggersTab({ projectId }: { projectId: string }) {
|
|||
}
|
||||
}, [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.');
|
||||
|
|
@ -52,6 +104,21 @@ export function TriggersTab({ projectId }: { projectId: string }) {
|
|||
}
|
||||
}, [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;
|
||||
|
|
@ -79,6 +146,7 @@ export function TriggersTab({ projectId }: { projectId: string }) {
|
|||
setSelectedTriggerType(null);
|
||||
setShowAuthModal(false);
|
||||
setIsSubmittingTrigger(false);
|
||||
setExpandedTrigger(null); // Reset expanded state
|
||||
loadTriggers(); // Reload in case any triggers were created
|
||||
};
|
||||
|
||||
|
|
@ -157,106 +225,217 @@ export function TriggersTab({ projectId }: { projectId: string }) {
|
|||
}
|
||||
}, [showCreateFlow, loadTriggers]);
|
||||
|
||||
useEffect(() => {
|
||||
if (triggers.length > 0) {
|
||||
loadTriggerTypeNames();
|
||||
}
|
||||
}, [triggers, loadTriggerTypeNames]);
|
||||
|
||||
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>
|
||||
<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 (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-red-500 mb-4">{error}</p>
|
||||
<Button variant="flat" onPress={loadTriggers}>
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
<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 (
|
||||
<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>
|
||||
<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 (
|
||||
<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>
|
||||
<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
|
||||
color="primary"
|
||||
variant="solid"
|
||||
variant="primary"
|
||||
startContent={<Plus className="w-4 h-4" />}
|
||||
onPress={handleCreateNew}
|
||||
onClick={handleCreateNew}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
Create New Trigger
|
||||
New External 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 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">
|
||||
<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>
|
||||
)}
|
||||
</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>
|
||||
</CardBody>
|
||||
</Card>
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -288,8 +467,8 @@ export function TriggersTab({ projectId }: { projectId: string }) {
|
|||
Select a Toolkit to Create Trigger
|
||||
</h3>
|
||||
<Button
|
||||
variant="flat"
|
||||
onPress={handleBackToList}
|
||||
variant="secondary"
|
||||
onClick={handleBackToList}
|
||||
>
|
||||
← Back to Triggers
|
||||
</Button>
|
||||
|
|
@ -320,11 +499,7 @@ export function TriggersTab({ projectId }: { projectId: string }) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="h-full overflow-auto px-4 py-4">
|
||||
<div className="max-w-[1024px] mx-auto">
|
||||
{showCreateFlow ? renderCreateFlow() : renderTriggerList()}
|
||||
</div>
|
||||
</div>
|
||||
{showCreateFlow ? renderCreateFlow() : renderTriggerList()}
|
||||
|
||||
{/* Auth Modal */}
|
||||
{selectedToolkit && (
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|||
import { Link, Spinner } from "@heroui/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Panel } from "@/components/common/panel-common";
|
||||
import { listScheduledJobRules } from "@/app/actions/scheduled-job-rules.actions";
|
||||
import { listScheduledJobRules, deleteScheduledJobRule } from "@/app/actions/scheduled-job-rules.actions";
|
||||
import { z } from "zod";
|
||||
import { ListedRuleItem } from "@/src/application/repositories/scheduled-job-rules.repository.interface";
|
||||
import { isToday, isThisWeek, isThisMonth } from "@/lib/utils/date";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { PlusIcon, Trash2 } from "lucide-react";
|
||||
|
||||
type ListedItem = z.infer<typeof ListedRuleItem>;
|
||||
|
||||
|
|
@ -18,6 +18,7 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
|
|||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [loadingMore, setLoadingMore] = useState<boolean>(false);
|
||||
const [hasMore, setHasMore] = useState<boolean>(false);
|
||||
const [deletingRule, setDeletingRule] = useState<string | null>(null);
|
||||
|
||||
const fetchPage = useCallback(async (cursorArg?: string | null) => {
|
||||
const res = await listScheduledJobRules({ projectId, cursor: cursorArg ?? undefined, limit: 20 });
|
||||
|
|
@ -48,6 +49,24 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
|
|||
setLoadingMore(false);
|
||||
}, [cursor, fetchPage]);
|
||||
|
||||
const handleDeleteRule = async (ruleId: string) => {
|
||||
if (!window.confirm('Are you sure you want to delete this one-time trigger?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setDeletingRule(ruleId);
|
||||
await deleteScheduledJobRule({ projectId, ruleId });
|
||||
// Remove the deleted item from the list
|
||||
setItems(prev => prev.filter(item => item.id !== ruleId));
|
||||
} catch (err: any) {
|
||||
console.error('Error deleting one-time trigger:', err);
|
||||
alert('Failed to delete one-time trigger. Please try again.');
|
||||
} finally {
|
||||
setDeletingRule(null);
|
||||
}
|
||||
};
|
||||
|
||||
const sections = useMemo(() => {
|
||||
const groups: Record<string, ListedItem[]> = {
|
||||
Today: [],
|
||||
|
|
@ -87,18 +106,15 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
|
|||
return (
|
||||
<Panel
|
||||
title={
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
SCHEDULED JOB RULES
|
||||
</div>
|
||||
<div className="text-base font-normal text-gray-900 dark:text-gray-100">
|
||||
Schedule a single job to run your assistant workflow at a specific date and time.
|
||||
</div>
|
||||
}
|
||||
rightActions={
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href={`/projects/${projectId}/job-rules/scheduled/new`}>
|
||||
<Button size="sm" className="flex items-center gap-2">
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
New Rule
|
||||
<Button size="sm" className="whitespace-nowrap" startContent={<PlusIcon className="w-4 h-4" />}>
|
||||
New One-time Trigger
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
|
@ -123,30 +139,40 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
|
|||
</h3>
|
||||
<div className="grid gap-3">
|
||||
{sectionItems.map((item) => (
|
||||
<Link
|
||||
<div
|
||||
key={item.id}
|
||||
href={`/projects/${projectId}/job-rules/scheduled/${item.id}`}
|
||||
className="block p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className={`text-sm font-medium ${getStatusColor(item.status, item.processedAt || null)}`}>
|
||||
{getStatusText(item.status, item.processedAt || null)}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Next run: {formatNextRunAt(item.nextRunAt)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Created: {new Date(item.createdAt).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{new Date(item.createdAt).toLocaleDateString()}
|
||||
<Link
|
||||
href={`/projects/${projectId}/job-rules/scheduled/${item.id}`}
|
||||
className="block"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className={`text-sm font-medium ${getStatusColor(item.status, item.processedAt || null)}`}>
|
||||
{getStatusText(item.status, item.processedAt || null)}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Next run: {formatNextRunAt(item.nextRunAt)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Created: {new Date(item.createdAt).toLocaleDateString()}
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
size="sm"
|
||||
isLoading={deletingRule === item.id}
|
||||
onClick={() => handleDeleteRule(item.id)}
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:text-red-400 dark:hover:text-red-300 dark:hover:bg-red-950"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -154,7 +180,7 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
|
|||
})}
|
||||
{items.length === 0 && !loading && (
|
||||
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
No scheduled job rules found. Create your first rule to get started.
|
||||
No one-time triggers yet. Create your first one-time trigger to get started.
|
||||
</div>
|
||||
)}
|
||||
{hasMore && (
|
||||
|
|
@ -183,3 +209,4 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
|
|||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue