fix: Consistency across cards

This commit is contained in:
Dries Augustyns
2026-04-24 12:54:43 +02:00
parent ab3edd7dc9
commit 517753c420
5 changed files with 431 additions and 563 deletions
+94 -107
View File
@@ -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 && (