mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 14:17:40 +00:00
fix(ui): constrain detail page card width and add team task/delegation detail views
- Add max-w-4xl to agent/channel/team detail card so tabs hug content - Add TaskDetailDialog for viewing full task info on click - Add click-to-detail on Team Delegations tab reusing DelegationDetailDialog - Add overflow-x-auto + min-w on task list for mobile Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
07d8b4efa6
commit
851cdb2391
6 files changed
+201
-74
No files matched your search
@@ -109,7 +109,7 @@ export function AgentDetailPage({ agentId, onBack }: AgentDetailPageProps) {
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="rounded-xl border bg-card p-3 shadow-sm sm:p-4">
|
||||
<div className="max-w-4xl rounded-xl border bg-card p-3 shadow-sm sm:p-4">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="w-full justify-start overflow-x-auto">
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
|
||||
@@ -93,7 +93,7 @@ export function ChannelDetailPage({ instanceId, onBack }: ChannelDetailPageProps
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="rounded-xl border bg-card p-3 shadow-sm sm:p-4">
|
||||
<div className="max-w-4xl rounded-xl border bg-card p-3 shadow-sm sm:p-4">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="w-full justify-start overflow-x-auto">
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { formatDate } from "@/lib/format";
|
||||
import type { TeamTaskData } from "@/types/team";
|
||||
import { taskStatusBadgeVariant } from "./task-utils";
|
||||
|
||||
interface TaskDetailDialogProps {
|
||||
task: TeamTaskData;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function TaskDetailDialog({ task, onClose }: TaskDetailDialogProps) {
|
||||
return (
|
||||
<Dialog open onOpenChange={() => onClose()}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Task Detail</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Subject */}
|
||||
<div className="rounded-md border p-3">
|
||||
<p className="mb-1 text-xs font-medium text-muted-foreground">Subject</p>
|
||||
<p className="text-sm font-medium">{task.subject}</p>
|
||||
</div>
|
||||
|
||||
{/* Summary grid */}
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Status:</span>{" "}
|
||||
<Badge variant={taskStatusBadgeVariant(task.status)} className="text-xs">
|
||||
{task.status.replace("_", " ")}
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Priority:</span>{" "}
|
||||
<span className="font-medium">{task.priority}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Owner:</span>{" "}
|
||||
<span className="font-medium">{task.owner_agent_key || "—"}</span>
|
||||
</div>
|
||||
{task.created_at && (
|
||||
<div>
|
||||
<span className="text-muted-foreground">Created:</span>{" "}
|
||||
{formatDate(task.created_at)}
|
||||
</div>
|
||||
)}
|
||||
{task.updated_at && (
|
||||
<div>
|
||||
<span className="text-muted-foreground">Updated:</span>{" "}
|
||||
{formatDate(task.updated_at)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Blocked by */}
|
||||
{task.blocked_by && task.blocked_by.length > 0 && (
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">Blocked by:</span>{" "}
|
||||
<span className="font-mono text-xs">{task.blocked_by.join(", ")}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Description */}
|
||||
{task.description && (
|
||||
<div className="rounded-md border p-3">
|
||||
<p className="mb-1 text-xs font-medium text-muted-foreground">Description</p>
|
||||
<pre className="whitespace-pre-wrap break-words text-sm">{task.description}</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Result */}
|
||||
{task.result && (
|
||||
<div className="rounded-md border p-3">
|
||||
<p className="mb-1 text-xs font-medium text-muted-foreground">Result</p>
|
||||
<pre className="max-h-[40vh] overflow-y-auto whitespace-pre-wrap break-words text-sm">
|
||||
{task.result}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useState } from "react";
|
||||
import { ClipboardList } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import type { TeamTaskData } from "@/types/team";
|
||||
import { taskStatusBadgeVariant } from "./task-utils";
|
||||
import { TaskDetailDialog } from "./task-detail-dialog";
|
||||
|
||||
interface TaskListProps {
|
||||
tasks: TeamTaskData[];
|
||||
@@ -9,6 +11,8 @@ interface TaskListProps {
|
||||
}
|
||||
|
||||
export function TaskList({ tasks, loading }: TaskListProps) {
|
||||
const [selectedTask, setSelectedTask] = useState<TeamTaskData | null>(null);
|
||||
|
||||
if (loading && tasks.length === 0) {
|
||||
return (
|
||||
<div className="py-8 text-center text-sm text-muted-foreground">
|
||||
@@ -30,42 +34,52 @@ export function TaskList({ tasks, loading }: TaskListProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border">
|
||||
<div className="grid grid-cols-[1fr_90px_100px_60px] items-center gap-2 border-b bg-muted/50 px-4 py-2.5 text-xs font-medium text-muted-foreground">
|
||||
<span>Subject</span>
|
||||
<span>Status</span>
|
||||
<span>Owner</span>
|
||||
<span>Priority</span>
|
||||
</div>
|
||||
{tasks.map((task) => (
|
||||
<div
|
||||
key={task.id}
|
||||
className="grid grid-cols-[1fr_90px_100px_60px] items-center gap-2 border-b px-4 py-3 last:border-0"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium">{task.subject}</p>
|
||||
{task.description && (
|
||||
<p className="truncate text-xs text-muted-foreground/70">
|
||||
{task.description}
|
||||
</p>
|
||||
)}
|
||||
{task.result && (
|
||||
<p className="mt-0.5 line-clamp-1 text-xs text-emerald-600 dark:text-emerald-400">
|
||||
{task.result}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Badge variant={taskStatusBadgeVariant(task.status)}>
|
||||
{task.status.replace("_", " ")}
|
||||
</Badge>
|
||||
<span className="truncate text-sm text-muted-foreground">
|
||||
{task.owner_agent_key || "—"}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{task.priority}
|
||||
</span>
|
||||
<>
|
||||
<div className="overflow-x-auto rounded-lg border">
|
||||
<div className="grid min-w-[400px] grid-cols-[1fr_90px_100px_60px] items-center gap-2 border-b bg-muted/50 px-4 py-2.5 text-xs font-medium text-muted-foreground">
|
||||
<span>Subject</span>
|
||||
<span>Status</span>
|
||||
<span>Owner</span>
|
||||
<span>Priority</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{tasks.map((task) => (
|
||||
<div
|
||||
key={task.id}
|
||||
className="grid min-w-[400px] cursor-pointer grid-cols-[1fr_90px_100px_60px] items-center gap-2 border-b px-4 py-3 last:border-0 hover:bg-muted/30"
|
||||
onClick={() => setSelectedTask(task)}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium">{task.subject}</p>
|
||||
{task.description && (
|
||||
<p className="truncate text-xs text-muted-foreground/70">
|
||||
{task.description}
|
||||
</p>
|
||||
)}
|
||||
{task.result && (
|
||||
<p className="mt-0.5 line-clamp-1 text-xs text-emerald-600 dark:text-emerald-400">
|
||||
{task.result}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Badge variant={taskStatusBadgeVariant(task.status)}>
|
||||
{task.status.replace("_", " ")}
|
||||
</Badge>
|
||||
<span className="truncate text-sm text-muted-foreground">
|
||||
{task.owner_agent_key || "—"}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{task.priority}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selectedTask && (
|
||||
<TaskDetailDialog
|
||||
task={selectedTask}
|
||||
onClose={() => setSelectedTask(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
import { useState } from "react";
|
||||
import { ArrowRightLeft } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { EmptyState } from "@/components/shared/empty-state";
|
||||
import { DeferredSpinner } from "@/components/shared/loading-skeleton";
|
||||
import { formatDate, formatDuration } from "@/lib/format";
|
||||
import { useTeamDelegations } from "./hooks/use-team-delegations";
|
||||
import { useDelegations } from "@/pages/delegations/hooks/use-delegations";
|
||||
import { useTraces } from "@/pages/traces/hooks/use-traces";
|
||||
import { DelegationDetailDialog } from "@/pages/delegations/delegation-detail-dialog";
|
||||
|
||||
interface TeamDelegationsTabProps {
|
||||
teamId: string;
|
||||
@@ -11,6 +15,9 @@ interface TeamDelegationsTabProps {
|
||||
|
||||
export function TeamDelegationsTab({ teamId }: TeamDelegationsTabProps) {
|
||||
const { records, loading } = useTeamDelegations(teamId);
|
||||
const { getDelegation } = useDelegations();
|
||||
const { getTrace } = useTraces();
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
if (loading) return <DeferredSpinner />;
|
||||
|
||||
@@ -25,42 +32,57 @@ export function TeamDelegationsTab({ teamId }: TeamDelegationsTabProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-md border overflow-x-auto">
|
||||
<table className="w-full min-w-[600px] text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50">
|
||||
<th className="px-4 py-3 text-left font-medium">Source / Target</th>
|
||||
<th className="px-4 py-3 text-left font-medium">Task</th>
|
||||
<th className="px-4 py-3 text-left font-medium">Status</th>
|
||||
<th className="px-4 py-3 text-left font-medium">Duration</th>
|
||||
<th className="px-4 py-3 text-left font-medium">Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{records.map((d) => (
|
||||
<tr key={d.id} className="border-b last:border-0">
|
||||
<td className="px-4 py-3">
|
||||
<span className="font-medium">{d.source_agent_key || d.source_agent_id.slice(0, 8)}</span>
|
||||
<span className="mx-1 text-muted-foreground">→</span>
|
||||
<span className="font-medium">{d.target_agent_key || d.target_agent_id.slice(0, 8)}</span>
|
||||
</td>
|
||||
<td className="max-w-[250px] truncate px-4 py-3 text-muted-foreground">
|
||||
{d.task}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<StatusBadge status={d.status} />
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
{formatDuration(d.duration_ms)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
{formatDate(d.created_at)}
|
||||
</td>
|
||||
<>
|
||||
<div className="rounded-md border overflow-x-auto">
|
||||
<table className="w-full min-w-[600px] text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50">
|
||||
<th className="px-4 py-3 text-left font-medium">Source / Target</th>
|
||||
<th className="px-4 py-3 text-left font-medium">Task</th>
|
||||
<th className="px-4 py-3 text-left font-medium">Status</th>
|
||||
<th className="px-4 py-3 text-left font-medium">Duration</th>
|
||||
<th className="px-4 py-3 text-left font-medium">Time</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</thead>
|
||||
<tbody>
|
||||
{records.map((d) => (
|
||||
<tr
|
||||
key={d.id}
|
||||
className="cursor-pointer border-b last:border-0 hover:bg-muted/30"
|
||||
onClick={() => setSelectedId(d.id)}
|
||||
>
|
||||
<td className="px-4 py-3">
|
||||
<span className="font-medium">{d.source_agent_key || d.source_agent_id.slice(0, 8)}</span>
|
||||
<span className="mx-1 text-muted-foreground">→</span>
|
||||
<span className="font-medium">{d.target_agent_key || d.target_agent_id.slice(0, 8)}</span>
|
||||
</td>
|
||||
<td className="max-w-[250px] truncate px-4 py-3 text-muted-foreground">
|
||||
{d.task}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<StatusBadge status={d.status} />
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
{formatDuration(d.duration_ms)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
{formatDate(d.created_at)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{selectedId && (
|
||||
<DelegationDetailDialog
|
||||
delegationId={selectedId}
|
||||
onClose={() => setSelectedId(null)}
|
||||
getDelegation={getDelegation}
|
||||
getTrace={getTrace}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ export function TeamDetailPage({ teamId, onBack }: TeamDetailPageProps) {
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="rounded-xl border bg-card p-3 shadow-sm sm:p-4">
|
||||
<div className="max-w-4xl rounded-xl border bg-card p-3 shadow-sm sm:p-4">
|
||||
<Tabs defaultValue="members">
|
||||
<TabsList className="w-full justify-start overflow-x-auto">
|
||||
<TabsTrigger value="members">Members</TabsTrigger>
|
||||
|
||||
Reference in new issue
Block a user