170 lines
6.7 KiB
TypeScript
170 lines
6.7 KiB
TypeScript
import { GitBranch, Plus, Trash2 } from 'lucide-react'
|
|
|
|
interface WorkflowListItem {
|
|
id: string
|
|
name: string
|
|
isActive: boolean
|
|
presetLabel: string
|
|
presetClassName: string
|
|
familyLabel: string
|
|
familyClassName: string
|
|
executionModeLabel: string
|
|
executionModeClassName: string
|
|
rolloutBadgeLabel: string
|
|
rolloutBadgeClassName: string
|
|
rolloutStatusLabel: string
|
|
rolloutStatusClassName: string
|
|
rolloutSummary: string
|
|
linkedOutputTypeCount: number
|
|
blueprintLabel?: string | null
|
|
isReference?: boolean
|
|
}
|
|
|
|
interface WorkflowListSection {
|
|
key: string
|
|
label: string
|
|
className: string
|
|
items: WorkflowListItem[]
|
|
}
|
|
|
|
interface WorkflowListSidebarProps {
|
|
isLoading: boolean
|
|
sections: WorkflowListSection[]
|
|
selectedId: string | null
|
|
onSelectWorkflow: (workflowId: string) => void
|
|
onCreateWorkflow: () => void
|
|
onDeleteWorkflow: (workflowId: string, workflowName: string) => void
|
|
}
|
|
|
|
export function WorkflowListSidebar({
|
|
isLoading,
|
|
sections,
|
|
selectedId,
|
|
onSelectWorkflow,
|
|
onCreateWorkflow,
|
|
onDeleteWorkflow,
|
|
}: WorkflowListSidebarProps) {
|
|
const workflowCount = sections.reduce((count, section) => count + section.items.length, 0)
|
|
|
|
return (
|
|
<aside className="flex w-56 flex-shrink-0 flex-col border-r border-border-default bg-surface">
|
|
<div className="flex items-center justify-between border-b border-border-default p-3">
|
|
<div className="flex items-center gap-2 text-sm font-semibold text-content-secondary">
|
|
<GitBranch size={16} />
|
|
Workflows
|
|
</div>
|
|
<button
|
|
onClick={onCreateWorkflow}
|
|
className="rounded p-1 text-content-muted hover:bg-surface-hover hover:text-content"
|
|
title="New Workflow"
|
|
>
|
|
<Plus size={16} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex-1 space-y-1 overflow-y-auto p-2">
|
|
{isLoading && (
|
|
<p className="px-2 py-4 text-center text-xs text-content-muted">Loading…</p>
|
|
)}
|
|
{!isLoading && workflowCount === 0 && (
|
|
<div className="px-2 py-4 text-center">
|
|
<p className="text-xs font-medium text-content-secondary">No workflows configured.</p>
|
|
<p className="mt-1 text-xs text-content-muted">
|
|
Workflows define the sequence of pipeline steps for rendering orders.
|
|
</p>
|
|
<button
|
|
onClick={onCreateWorkflow}
|
|
className="mt-2 text-xs text-accent hover:underline"
|
|
>
|
|
+ New Workflow
|
|
</button>
|
|
</div>
|
|
)}
|
|
{sections.map(section => (
|
|
<div key={section.key} className="space-y-1 pb-2">
|
|
<div className="flex items-center justify-between px-2 pt-1">
|
|
<span className={`rounded-full px-2 py-0.5 text-[11px] font-medium ${section.className}`}>
|
|
{section.label}
|
|
</span>
|
|
<span className="text-xs text-content-muted">{section.items.length}</span>
|
|
</div>
|
|
{section.items.map(item => (
|
|
<div
|
|
key={item.id}
|
|
role="button"
|
|
tabIndex={0}
|
|
onClick={() => onSelectWorkflow(item.id)}
|
|
onKeyDown={event => {
|
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
event.preventDefault()
|
|
onSelectWorkflow(item.id)
|
|
}
|
|
}}
|
|
className={`group w-full rounded-lg border px-3 py-2.5 text-left transition-colors focus:outline-none focus:ring-2 focus:ring-accent ${
|
|
selectedId === item.id
|
|
? 'border-accent/30 bg-accent-light'
|
|
: 'border-transparent hover:bg-surface-hover'
|
|
}`}
|
|
>
|
|
<div className="flex items-start justify-between gap-1">
|
|
<div className="flex min-w-0 items-center gap-1.5">
|
|
{item.isActive && (
|
|
<span className="h-2 w-2 flex-shrink-0 rounded-full bg-green-500" title="Active" />
|
|
)}
|
|
<p className="truncate text-sm font-medium text-content">{item.name}</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={event => {
|
|
event.stopPropagation()
|
|
onDeleteWorkflow(item.id, item.name)
|
|
}}
|
|
className="flex-shrink-0 rounded p-0.5 text-content-muted opacity-0 hover:bg-red-100 hover:text-red-600 group-hover:opacity-100"
|
|
title="Delete"
|
|
>
|
|
<Trash2 size={12} />
|
|
</button>
|
|
</div>
|
|
<span className={`mt-1 inline-block rounded-full px-1.5 py-0.5 text-xs font-medium ${item.presetClassName}`}>
|
|
{item.presetLabel}
|
|
</span>
|
|
<span className={`ml-1 mt-1 inline-block rounded-full px-1.5 py-0.5 text-xs font-medium ${item.familyClassName}`}>
|
|
{item.familyLabel}
|
|
</span>
|
|
{item.blueprintLabel && (
|
|
<span className="ml-1 mt-1 inline-block rounded-full bg-slate-100 px-1.5 py-0.5 text-xs font-medium text-slate-700 dark:bg-slate-900/40 dark:text-slate-300">
|
|
{item.blueprintLabel}
|
|
</span>
|
|
)}
|
|
<span className={`ml-1 mt-1 inline-block rounded-full px-1.5 py-0.5 text-xs font-medium ${item.executionModeClassName}`}>
|
|
{item.executionModeLabel}
|
|
</span>
|
|
<span className={`ml-1 mt-1 inline-block rounded-full px-1.5 py-0.5 text-xs font-medium ${item.rolloutBadgeClassName}`}>
|
|
{item.rolloutBadgeLabel}
|
|
</span>
|
|
<span className={`ml-1 mt-1 inline-block rounded-full px-1.5 py-0.5 text-xs font-medium ${item.rolloutStatusClassName}`}>
|
|
{item.rolloutStatusLabel}
|
|
</span>
|
|
<p className="mt-1 text-xs text-content-muted">
|
|
{item.linkedOutputTypeCount} linked output type{item.linkedOutputTypeCount === 1 ? '' : 's'}.
|
|
</p>
|
|
<p className="mt-1 text-xs text-content-muted">
|
|
{item.rolloutSummary}
|
|
</p>
|
|
{item.isReference && (
|
|
<p className="mt-1 text-xs text-content-muted">
|
|
Canonical reference workflow for parity work.
|
|
</p>
|
|
)}
|
|
{!item.isActive && (
|
|
<span className="ml-1 text-xs text-content-muted">(inactive)</span>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</aside>
|
|
)
|
|
}
|