Update all triggers to display standard cards for existing triggers with delete buttons

This commit is contained in:
akhisud3195 2025-08-19 13:03:39 +05:30
parent d592354080
commit e448601046
4 changed files with 381 additions and 153 deletions

View file

@ -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>

View file

@ -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 && (

View file

@ -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 && (

View file

@ -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>
);
}