[taskboard] migrate fleet console to nextjs
This commit is contained in:
178
components/tasks-client.tsx
Normal file
178
components/tasks-client.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select } from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import type { FleetAgent, TaskPriority, TaskRecord, TaskStatus } from "@/lib/types";
|
||||
|
||||
const COLUMNS: TaskStatus[] = ["Backlog", "Todo", "In Progress", "Review", "Done"];
|
||||
const PRIORITIES: TaskPriority[] = ["Low", "Medium", "High", "Critical"];
|
||||
|
||||
export function TasksClient({
|
||||
initialTasks,
|
||||
agents,
|
||||
}: {
|
||||
initialTasks: TaskRecord[];
|
||||
agents: FleetAgent[];
|
||||
}) {
|
||||
const [tasks, setTasks] = useState(initialTasks);
|
||||
const [formState, setFormState] = useState({
|
||||
title: "",
|
||||
description: "",
|
||||
assignee: "",
|
||||
priority: "Medium" as TaskPriority,
|
||||
tags: "",
|
||||
});
|
||||
|
||||
async function refreshTasks() {
|
||||
const response = await fetch("/api/tasks");
|
||||
const nextTasks = (await response.json()) as TaskRecord[];
|
||||
setTasks(nextTasks);
|
||||
}
|
||||
|
||||
async function createTask(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
await fetch("/api/tasks", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
title: formState.title,
|
||||
description: formState.description,
|
||||
assignee: formState.assignee,
|
||||
priority: formState.priority,
|
||||
tags: formState.tags
|
||||
.split(",")
|
||||
.map((tag) => tag.trim())
|
||||
.filter(Boolean),
|
||||
}),
|
||||
});
|
||||
|
||||
setFormState({
|
||||
title: "",
|
||||
description: "",
|
||||
assignee: "",
|
||||
priority: "Medium",
|
||||
tags: "",
|
||||
});
|
||||
await refreshTasks();
|
||||
}
|
||||
|
||||
async function moveToDone(taskId: number) {
|
||||
await fetch(`/api/tasks/${taskId}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ status: "Done" }),
|
||||
});
|
||||
await refreshTasks();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Unified Task Intake</CardTitle>
|
||||
<CardDescription>
|
||||
Assign work to OpenClaw swarm agents or ZeroClaw host runtimes from a single board.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="grid gap-3 md:grid-cols-2" onSubmit={createTask}>
|
||||
<Input
|
||||
placeholder="Task title"
|
||||
required
|
||||
value={formState.title}
|
||||
onChange={(event) => setFormState((current) => ({ ...current, title: event.target.value }))}
|
||||
/>
|
||||
<Select
|
||||
value={formState.assignee}
|
||||
onChange={(event) => setFormState((current) => ({ ...current, assignee: event.target.value }))}
|
||||
>
|
||||
<option value="">Select agent</option>
|
||||
{agents.map((agent) => (
|
||||
<option key={agent.slug} value={agent.assignmentKey}>
|
||||
{agent.name} • {agent.family} • {agent.host}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
value={formState.priority}
|
||||
onChange={(event) =>
|
||||
setFormState((current) => ({ ...current, priority: event.target.value as TaskPriority }))
|
||||
}
|
||||
>
|
||||
{PRIORITIES.map((priority) => (
|
||||
<option key={priority} value={priority}>
|
||||
{priority}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<Input
|
||||
placeholder="Tags (comma-separated)"
|
||||
value={formState.tags}
|
||||
onChange={(event) => setFormState((current) => ({ ...current, tags: event.target.value }))}
|
||||
/>
|
||||
<div className="md:col-span-2">
|
||||
<Textarea
|
||||
placeholder="Describe the task, host target, and expected outcome"
|
||||
value={formState.description}
|
||||
onChange={(event) =>
|
||||
setFormState((current) => ({ ...current, description: event.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<Button type="submit">Create Task</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid gap-4 xl:grid-cols-5">
|
||||
{COLUMNS.map((column) => {
|
||||
const columnTasks = tasks.filter((task) => task.status === column);
|
||||
return (
|
||||
<Card className="min-h-[420px]" key={column}>
|
||||
<CardHeader className="pb-4">
|
||||
<CardTitle className="flex items-center justify-between text-base">
|
||||
<span>{column}</span>
|
||||
<Badge variant="secondary">{columnTasks.length}</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{columnTasks.map((task) => (
|
||||
<div className="rounded-xl border border-white/10 bg-slate-950/40 p-4" key={task.id}>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<h3 className="font-medium text-white">{task.title}</h3>
|
||||
<Badge variant={task.priority === "Critical" ? "warning" : "outline"}>
|
||||
{task.priority}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-slate-300">{task.description || "No description"}</p>
|
||||
<div className="mt-3 flex flex-wrap gap-2 text-xs text-slate-400">
|
||||
<Badge variant="secondary">{task.assignee || "Unassigned"}</Badge>
|
||||
{task.tags.map((tag) => (
|
||||
<Badge key={tag} variant="outline">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
{task.status !== "Done" ? (
|
||||
<Button className="mt-4 w-full" size="sm" variant="outline" onClick={() => moveToDone(task.id)}>
|
||||
Mark Done
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user