fix: Consistency across cards
This commit is contained in:
@@ -3,9 +3,6 @@ import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Command,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
@@ -26,10 +23,10 @@ import {DashboardLayout} from '../../components/DashboardLayout';
|
||||
import {EmptyState} from '../../components/EmptyState';
|
||||
import {network} from '../../lib/network';
|
||||
import {formatRelativeTime} from '../../lib/dateUtils';
|
||||
import {Calendar, Edit, Plus, Power, PowerOff, Search, Trash2, Workflow as WorkflowIcon} from 'lucide-react';
|
||||
import {Calendar, Edit, Plus, Power, PowerOff, Search, Trash2, Workflow as WorkflowIcon, X, Zap} from 'lucide-react';
|
||||
import {NextSeo} from 'next-seo';
|
||||
import Link from 'next/link';
|
||||
import {useState} from 'react';
|
||||
import {useEffect, useState} from 'react';
|
||||
import {toast} from 'sonner';
|
||||
import useSWR from 'swr';
|
||||
import {WorkflowSchemas} from '@plunk/shared';
|
||||
@@ -47,11 +44,13 @@ export default function WorkflowsPage() {
|
||||
PaginatedResponse<Workflow & {_count?: {steps: number; executions: number}}>
|
||||
>(`/workflows?page=${page}&pageSize=20${search ? `&search=${search}` : ''}`, {revalidateOnFocus: false});
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setSearch(searchInput);
|
||||
setPage(1);
|
||||
};
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setSearch(searchInput);
|
||||
setPage(1);
|
||||
}, 350);
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchInput]);
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!workflowToDelete) return;
|
||||
@@ -100,40 +99,34 @@ export default function WorkflowsPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Search & Filters */}
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<form onSubmit={handleSearch} className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-neutral-500" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search workflows..."
|
||||
value={searchInput}
|
||||
onChange={e => setSearchInput(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit">Search</Button>
|
||||
{search && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setSearch('');
|
||||
setSearchInput('');
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-neutral-400" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search workflows..."
|
||||
value={searchInput}
|
||||
onChange={e => setSearchInput(e.target.value)}
|
||||
className="pl-10 pr-10"
|
||||
/>
|
||||
{searchInput && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Clear search"
|
||||
onClick={() => {
|
||||
setSearchInput('');
|
||||
setSearch('');
|
||||
setPage(1);
|
||||
}}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-neutral-600 transition-colors"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Workflows Grid */}
|
||||
<div className="grid gap-4">
|
||||
{/* Workflows */}
|
||||
<div>
|
||||
{isLoading ? (
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
@@ -162,53 +155,75 @@ export default function WorkflowsPage() {
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
{data?.data.map(workflow => (
|
||||
<Card key={workflow.id} className={workflow.enabled ? 'border-green-200' : ''}>
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3">
|
||||
<CardTitle>{workflow.name}</CardTitle>
|
||||
<Badge variant={workflow.enabled ? 'green' : 'neutral'}>
|
||||
{workflow.enabled ? (
|
||||
<>
|
||||
<Power className="h-3 w-3 mr-1" />
|
||||
Active
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PowerOff className="h-3 w-3 mr-1" />
|
||||
Disabled
|
||||
</>
|
||||
)}
|
||||
</Badge>
|
||||
|
||||
{workflow.triggerConfig &&
|
||||
typeof workflow.triggerConfig === 'object' &&
|
||||
'eventName' in workflow.triggerConfig && (
|
||||
<Badge variant={'info'}>{String(workflow.triggerConfig.eventName)}</Badge>
|
||||
)}
|
||||
</div>
|
||||
{workflow.description && (
|
||||
<CardDescription className="mt-2">{workflow.description}</CardDescription>
|
||||
)}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{data?.data.map(workflow => (
|
||||
<Card key={workflow.id} className="transition-colors hover:border-neutral-300 flex flex-col">
|
||||
<Link
|
||||
href={`/workflows/${workflow.id}`}
|
||||
className="flex-1 block p-6 pb-4 hover:bg-neutral-50/50 transition-colors rounded-t-xl"
|
||||
aria-label={`Open ${workflow.name}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3 mb-3">
|
||||
<h3 className="font-semibold text-neutral-900 leading-snug">{workflow.name}</h3>
|
||||
<Badge variant={workflow.enabled ? 'green' : 'neutral'} className="shrink-0 mt-0.5">
|
||||
{workflow.enabled ? (
|
||||
<><Power className="h-3 w-3 mr-1" />Active</>
|
||||
) : (
|
||||
<><PowerOff className="h-3 w-3 mr-1" />Disabled</>
|
||||
)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
{workflow.triggerConfig &&
|
||||
typeof workflow.triggerConfig === 'object' &&
|
||||
'eventName' in workflow.triggerConfig && (
|
||||
<div className="flex items-center gap-1.5 text-xs text-neutral-500 mb-3">
|
||||
<Zap className="h-3 w-3 shrink-0" />
|
||||
<span>Triggers on</span>
|
||||
<code className="font-mono bg-neutral-100 px-1.5 py-0.5 rounded text-neutral-700">
|
||||
{String(workflow.triggerConfig.eventName)}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<span>
|
||||
<strong className="font-semibold text-neutral-900">{workflow._count?.steps ?? 0}</strong>
|
||||
<span className="text-neutral-400 ml-1 text-xs">steps</span>
|
||||
</span>
|
||||
<span className="h-3 w-px bg-neutral-200" />
|
||||
<span>
|
||||
<strong className="font-semibold text-neutral-900">{workflow._count?.executions ?? 0}</strong>
|
||||
<span className="text-neutral-400 ml-1 text-xs">executions</span>
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="px-6 py-3 border-t border-neutral-100 flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5 text-xs text-neutral-400">
|
||||
<Calendar className="h-3 w-3" />
|
||||
<div className="group relative inline-block cursor-help">
|
||||
<span>Updated {formatRelativeTime(workflow.updatedAt)}</span>
|
||||
<div className="hidden group-hover:block absolute z-10 w-48 p-2 bg-neutral-900 text-white text-xs rounded shadow-lg bottom-full left-0 mb-1 whitespace-nowrap">
|
||||
{dayjs(workflow.updatedAt).format('DD MMMM YYYY, hh:mm')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title={workflow.enabled ? 'Disable workflow' : 'Enable workflow'}
|
||||
onClick={() => handleToggleEnabled(workflow.id, workflow.enabled)}
|
||||
>
|
||||
{workflow.enabled ? <PowerOff className="h-4 w-4" /> : <Power className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Link href={`/workflows/${workflow.id}`}>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Link href={`/workflows/${workflow.id}`} aria-label="Edit workflow">
|
||||
<Button variant="ghost" size="sm" title="Edit workflow">
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title="Delete workflow"
|
||||
onClick={() => {
|
||||
setWorkflowToDelete(workflow.id);
|
||||
setShowDeleteDialog(true);
|
||||
@@ -218,37 +233,9 @@ export default function WorkflowsPage() {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-6 text-sm text-neutral-500">
|
||||
<div>
|
||||
<span className="font-medium text-neutral-900">{workflow._count?.steps ?? 0}</span> steps
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-neutral-900">{workflow._count?.executions ?? 0}</span>{' '}
|
||||
executions
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-xs text-neutral-500 pt-3 border-t border-neutral-100">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Calendar className="h-3 w-3" />
|
||||
<div className="group relative inline-block cursor-help">
|
||||
<span>Created {formatRelativeTime(workflow.createdAt)}</span>
|
||||
<div className="hidden group-hover:block absolute z-10 w-48 p-2 bg-neutral-900 text-white text-xs rounded shadow-lg bottom-full left-0 mb-1 whitespace-nowrap">
|
||||
{dayjs(workflow.createdAt).format('DD MMMM YYYY, hh:mm')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="group relative inline-block cursor-help">
|
||||
<span>• Updated {formatRelativeTime(workflow.updatedAt)}</span>
|
||||
<div className="hidden group-hover:block absolute z-10 w-48 p-2 bg-neutral-900 text-white text-xs rounded shadow-lg bottom-full left-0 mb-1 whitespace-nowrap">
|
||||
{dayjs(workflow.updatedAt).format('DD MMMM YYYY, hh:mm')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{data && data.totalPages > 1 && (
|
||||
|
||||
Reference in New Issue
Block a user