212 lines
7.2 KiB
TypeScript
212 lines
7.2 KiB
TypeScript
"use client";
|
|
|
|
import { Button } from "@kandev/ui/button";
|
|
import { Label } from "@kandev/ui/label";
|
|
import { Input } from "@kandev/ui/input";
|
|
import { RadioGroup, RadioGroupItem } from "@kandev/ui/radio-group";
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@kandev/ui/dialog";
|
|
import { Textarea } from "@kandev/ui/textarea";
|
|
import { cn } from "@/lib/utils";
|
|
import type { WorkflowTemplate } from "@/lib/types/http";
|
|
|
|
type ImportWorkflowsDialogProps = {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
importYaml: string;
|
|
onImportYamlChange: (value: string) => void;
|
|
onFileUpload: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
fileInputRef: React.RefObject<HTMLInputElement | null>;
|
|
onImport: () => void;
|
|
importLoading: boolean;
|
|
};
|
|
|
|
export function ImportWorkflowsDialog({
|
|
open,
|
|
onOpenChange,
|
|
importYaml,
|
|
onImportYamlChange,
|
|
onFileUpload,
|
|
fileInputRef,
|
|
onImport,
|
|
importLoading,
|
|
}: ImportWorkflowsDialogProps) {
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Import Workflows</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label>Upload YAML file</Label>
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
accept=".yml,.yaml"
|
|
onChange={onFileUpload}
|
|
className="block w-full text-sm text-muted-foreground file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-medium file:bg-primary file:text-primary-foreground file:cursor-pointer cursor-pointer"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Or paste YAML</Label>
|
|
<Textarea
|
|
placeholder={
|
|
"version: 1\ntype: kandev_workflow\nworkflows:\n - name: My Workflow\n steps: [...]"
|
|
}
|
|
value={importYaml}
|
|
onChange={(e) => onImportYamlChange(e.target.value)}
|
|
className="font-mono text-xs max-h-96 overflow-y-auto"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => onOpenChange(false)} className="cursor-pointer">
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
onClick={onImport}
|
|
disabled={!importYaml.trim() || importLoading}
|
|
className="cursor-pointer"
|
|
>
|
|
{importLoading ? "Importing..." : "Import"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
function TemplateRadioItem({
|
|
template,
|
|
isSelected,
|
|
}: {
|
|
template: WorkflowTemplate;
|
|
isSelected: boolean;
|
|
}) {
|
|
return (
|
|
<label
|
|
htmlFor={template.id}
|
|
className={cn(
|
|
"flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors",
|
|
isSelected ? "border-primary bg-primary/5" : "border-border hover:border-primary/50",
|
|
)}
|
|
>
|
|
<RadioGroupItem value={template.id} id={template.id} className="mt-0.5" />
|
|
<div className="flex flex-col gap-1.5 min-w-0">
|
|
<span className="font-medium">{template.name}</span>
|
|
{template.description && (
|
|
<span className="text-sm text-muted-foreground">{template.description}</span>
|
|
)}
|
|
{template.default_steps && template.default_steps.length > 0 && (
|
|
<div className="flex items-center gap-1.5 flex-wrap mt-0.5">
|
|
{template.default_steps.map((step, i) => (
|
|
<div key={i} className="flex items-center gap-1">
|
|
{i > 0 && <span className="text-muted-foreground/40 text-xs">→</span>}
|
|
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
<div className={cn("w-2 h-2 rounded-full", step.color ?? "bg-slate-500")} />
|
|
{step.name}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</label>
|
|
);
|
|
}
|
|
|
|
type CreateWorkflowDialogProps = {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
workflowName: string;
|
|
onWorkflowNameChange: (value: string) => void;
|
|
selectedTemplateId: string | null;
|
|
onSelectedTemplateChange: (value: string | null) => void;
|
|
workflowTemplates: WorkflowTemplate[];
|
|
onCreate: () => void;
|
|
};
|
|
|
|
export function CreateWorkflowDialog({
|
|
open,
|
|
onOpenChange,
|
|
workflowName,
|
|
onWorkflowNameChange,
|
|
selectedTemplateId,
|
|
onSelectedTemplateChange,
|
|
workflowTemplates,
|
|
onCreate,
|
|
}: CreateWorkflowDialogProps) {
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent
|
|
className="sm:w-[900px] sm:max-w-none max-h-[90vh] flex flex-col"
|
|
data-testid="create-workflow-dialog"
|
|
>
|
|
<DialogHeader>
|
|
<DialogTitle>Add Workflow</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-6 overflow-y-auto">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="workflowName">Name</Label>
|
|
<Input
|
|
id="workflowName"
|
|
placeholder="My Project Workflow"
|
|
value={workflowName}
|
|
onChange={(e) => onWorkflowNameChange(e.target.value)}
|
|
data-testid="workflow-name-input"
|
|
/>
|
|
</div>
|
|
{workflowTemplates.length > 0 && (
|
|
<div className="space-y-2">
|
|
<Label>Template</Label>
|
|
<RadioGroup
|
|
value={selectedTemplateId ?? "custom"}
|
|
onValueChange={(v) => onSelectedTemplateChange(v === "custom" ? null : v)}
|
|
>
|
|
<div className="grid gap-3">
|
|
{workflowTemplates.map((template) => (
|
|
<TemplateRadioItem
|
|
key={template.id}
|
|
template={template}
|
|
isSelected={selectedTemplateId === template.id}
|
|
/>
|
|
))}
|
|
<label
|
|
htmlFor="custom"
|
|
className={cn(
|
|
"flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors",
|
|
selectedTemplateId === null
|
|
? "border-primary bg-primary/5"
|
|
: "border-border hover:border-primary/50",
|
|
)}
|
|
>
|
|
<RadioGroupItem value="custom" id="custom" className="mt-0.5" />
|
|
<div className="flex flex-col gap-1.5">
|
|
<span className="font-medium">Custom</span>
|
|
<span className="text-sm text-muted-foreground">
|
|
Create your own agentic workflow from scratch.
|
|
</span>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</RadioGroup>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => onOpenChange(false)} className="cursor-pointer">
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
onClick={onCreate}
|
|
className="cursor-pointer"
|
|
data-testid="confirm-create-workflow"
|
|
>
|
|
Add Workflow
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|