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:
viettranxandClaude Opus 4.6 committed 2026-03-05 12:27:53 +07:00
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)}
/>
)}
</>
);
}
+57 -35
View File
@@ -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">&rarr;</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">&rarr;</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}
/>
)}
</>
);
}
+1 -1
View File
@@ -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>