fixing Agent Table phase 1

This commit is contained in:
Abhimanyu Yadav
2025-04-24 17:30:46 +05:30
parent fda57a423a
commit dfa29b9b22
7 changed files with 273 additions and 269 deletions

View File

@@ -1,19 +1,28 @@
import type { Meta, StoryObj } from "@storybook/react";
import { AgentTable } from "./AgentTable";
import { AgentTableRowProps } from "./AgentTableRow";
import { userEvent, within, expect } from "@storybook/test";
import { userEvent, within, expect, fn } from "@storybook/test";
import { StatusType } from "./Status";
const meta: Meta<typeof AgentTable> = {
const meta = {
title: "AGPT UI/Agent Table",
component: AgentTable,
parameters: {
layout: "fullscreen",
},
tags: ["autodocs"],
};
decorators: [
(Story) => (
<div className="container mx-auto p-4">
<Story />
</div>
),
],
} satisfies Meta<typeof AgentTable>;
export default meta;
type Story = StoryObj<typeof AgentTable>;
type Story = StoryObj<typeof meta>;
const sampleAgents: AgentTableRowProps[] = [
const sampleAgents = [
{
id: 43,
agentName: "Super Coder",
@@ -22,17 +31,13 @@ const sampleAgents: AgentTableRowProps[] = [
"https://ddz4ak4pa3d19.cloudfront.net/cache/53/b2/53b2bc7d7900f0e1e60bf64ebf38032d.jpg",
],
dateSubmitted: "2023-05-15",
status: "approved",
status: "approved" as StatusType,
runs: 1500,
rating: 4.8,
agent_id: "43",
agent_version: 1,
sub_heading: "Super Coder",
date_submitted: "2023-05-15",
onEditSubmission: () => console.log("Edit Super Coder"),
onDeleteSubmission: () => console.log("Delete Super Coder"),
selectedAgents: new Set(),
setSelectedAgents: () => {},
},
{
id: 44,
@@ -42,17 +47,13 @@ const sampleAgents: AgentTableRowProps[] = [
"https://ddz4ak4pa3d19.cloudfront.net/cache/40/f7/40f7bc97c952f8df0f9c88d29defe8d4.jpg",
],
dateSubmitted: "2023-05-10",
status: "awaiting_review",
status: "awaiting_review" as StatusType,
runs: 1200,
rating: 4.5,
agent_id: "44",
agent_version: 1,
sub_heading: "Data Analyzer",
date_submitted: "2023-05-10",
onEditSubmission: () => console.log("Edit Data Analyzer"),
onDeleteSubmission: () => console.log("Delete Data Analyzer"),
selectedAgents: new Set(),
setSelectedAgents: () => {},
},
{
id: 45,
@@ -62,48 +63,118 @@ const sampleAgents: AgentTableRowProps[] = [
"https://ddz4ak4pa3d19.cloudfront.net/cache/14/9e/149ebb9014aa8c0097e72ed89845af0e.jpg",
],
dateSubmitted: "2023-05-05",
status: "draft",
status: "draft" as StatusType,
runs: 800,
rating: 4.2,
agent_id: "45",
agent_version: 1,
sub_heading: "UI Designer",
date_submitted: "2023-05-05",
onEditSubmission: () => console.log("Edit UI Designer"),
onDeleteSubmission: () => console.log("Delete UI Designer"),
selectedAgents: new Set(),
setSelectedAgents: () => {},
},
];
export const Default: Story = {
args: {
agents: sampleAgents,
onEditSubmission: fn(),
onDeleteSubmission: fn(),
},
};
export const EmptyTable: Story = {
args: {
agents: [],
onEditSubmission: fn(),
onDeleteSubmission: fn(),
},
};
// Tests
export const InteractionTest: Story = {
...Default,
play: async ({ canvasElement }) => {
export const LongAgentNames: Story = {
args: {
agents: [
{
...sampleAgents[0],
agentName:
"Super Advanced Artificial Intelligence Code Generator and Optimizer with Machine Learning Capabilities",
sub_heading:
"A very advanced AI system that can generate and optimize code using cutting-edge machine learning techniques",
},
...sampleAgents.slice(1),
],
onEditSubmission: fn(),
onDeleteSubmission: fn(),
},
};
export const ManyAgents: Story = {
args: {
agents: Array(20)
.fill(null)
.map((_, index) => ({
...sampleAgents[index % 3],
id: 100 + index,
agent_id: `${100 + index}`,
agentName: `Test Agent ${index + 1}`,
})),
onEditSubmission: fn(),
onDeleteSubmission: fn(),
},
};
export const WithInteraction: Story = {
args: {
agents: sampleAgents,
onEditSubmission: fn(),
onDeleteSubmission: fn(),
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const editButtons = await canvas.findAllByText("Edit");
await userEvent.click(editButtons[0]);
// You would typically assert something here, but console.log is used in the mocked function
const table = canvas.getByRole("table");
await expect(table).toBeInTheDocument();
const checkboxes = canvas.getAllByTestId("dropdown-button");
await expect(checkboxes.length).toBeGreaterThan(0);
},
};
export const EmptyTableTest: Story = {
...EmptyTable,
args: {
agents: [],
onEditSubmission: fn(),
onDeleteSubmission: fn(),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const emptyMessage = canvas.getByText("No agents found");
expect(emptyMessage).toBeTruthy();
const emptyMessages = canvas.getAllByText(
"No agents available. Create your first agent to get started!",
);
await expect(emptyMessages.length).toBeGreaterThan(0);
await expect(emptyMessages[0]).toBeInTheDocument();
},
};
export const ResponsiveTest: Story = {
args: {
agents: sampleAgents,
onEditSubmission: fn(),
onDeleteSubmission: fn(),
},
parameters: {
viewport: {
defaultViewport: "mobile2",
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// In mobile view, cards should be visible instead of table
// Check for at least one card
const cards = canvas.getAllByTestId("agent-table-card");
await expect(cards.length).toBe(3);
// Table should be hidden
const tables = canvasElement.querySelectorAll("table");
await expect(tables.length).toBe(1);
},
};

View File

@@ -4,6 +4,15 @@ import * as React from "react";
import { AgentTableRow, AgentTableRowProps } from "./AgentTableRow";
import { AgentTableCard } from "./AgentTableCard";
import { StoreSubmissionRequest } from "@/lib/autogpt-server-api/types";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Checkbox } from "@/components/ui/checkbox";
export interface AgentTableProps {
agents: Omit<
@@ -41,78 +50,90 @@ export const AgentTable: React.FC<AgentTableProps> = ({
return (
<div className="w-full">
{/* Table header - Hide on mobile */}
<div className="hidden flex-col md:flex">
<div className="border-t border-neutral-300 dark:border-neutral-700" />
<div className="flex items-center px-4 py-2">
<div className="flex items-center">
<div className="flex min-w-[120px] items-center">
<input
type="checkbox"
id="selectAllAgents"
aria-label="Select all agents"
className="mr-4 h-5 w-5 rounded border-2 border-neutral-400 dark:border-neutral-600"
checked={
selectedAgents.size === agents.length && agents.length > 0
}
onChange={handleSelectAll}
/>
<label
htmlFor="selectAllAgents"
className="text-sm font-medium text-neutral-800 dark:text-neutral-200"
>
Select all
</label>
</div>
</div>
<div className="ml-2 grid w-full grid-cols-[400px,150px,150px,100px,100px,50px] items-center">
<div className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
Agent info
</div>
<div className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
Date submitted
</div>
<div className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
Status
</div>
<div className="text-right text-sm font-medium text-neutral-800 dark:text-neutral-200">
Runs
</div>
<div className="text-right text-sm font-medium text-neutral-800 dark:text-neutral-200">
Reviews
</div>
<div></div>
</div>
</div>
<div className="border-b border-neutral-300 dark:border-neutral-700" />
{/* Table for desktop view */}
<div className="hidden md:block">
<Table>
<TableHeader>
<TableRow>
<TableHead>
<Checkbox
id="selectAllAgents"
aria-label="Select all agents"
checked={
selectedAgents.size === agents.length && agents.length > 0
}
onCheckedChange={(checked) => {
if (checked) {
setSelectedAgents(
new Set(agents.map((agent) => agent.agent_id)),
);
} else {
setSelectedAgents(new Set());
}
}}
/>
</TableHead>
<TableHead className="font-sans text-sm font-medium text-neutral-800">
Agent info
</TableHead>
<TableHead className="font-sans text-sm font-medium text-neutral-800">
Date submitted
</TableHead>
<TableHead className="font-sans text-sm font-medium text-neutral-800">
Status
</TableHead>
<TableHead className="font-sans text-sm font-medium text-neutral-800">
Runs
</TableHead>
<TableHead className="font-sans text-sm font-medium text-neutral-800">
Reviews
</TableHead>
<TableHead className="font-sans text-sm font-medium text-neutral-800"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{agents.length > 0 ? (
agents.map((agent) => (
<AgentTableRow
key={agent.id}
{...agent}
selectedAgents={selectedAgents}
setSelectedAgents={setSelectedAgents}
onEditSubmission={onEditSubmission}
onDeleteSubmission={onDeleteSubmission}
/>
))
) : (
<TableRow>
<TableCell colSpan={7} className="py-4 text-center">
<span className="font-sans text-base text-neutral-600 dark:text-neutral-400">
No agents available. Create your first agent to get started!
</span>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{/* Table body */}
{agents.length > 0 ? (
<div className="flex flex-col">
{agents.map((agent, index) => (
<div key={agent.id} className="md:block">
<AgentTableRow
{/* Mobile view with cards */}
<div className="md:hidden">
{agents.length > 0 ? (
<div className="flex flex-col">
{agents.map((agent) => (
<AgentTableCard
key={agent.id}
{...agent}
selectedAgents={selectedAgents}
setSelectedAgents={setSelectedAgents}
onEditSubmission={onEditSubmission}
onDeleteSubmission={onDeleteSubmission}
/>
<div className="block md:hidden">
<AgentTableCard
{...agent}
onEditSubmission={onEditSubmission}
/>
</div>
</div>
))}
</div>
) : (
<div className="py-4 text-center font-sans text-base text-neutral-600 dark:text-neutral-400">
No agents available. Create your first agent to get started!
</div>
)}
))}
</div>
) : (
<div className="py-4 text-center font-sans text-base text-neutral-600 dark:text-neutral-400">
No agents available. Create your first agent to get started!
</div>
)}
</div>
</div>
);
};

View File

@@ -1,65 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react";
import { AgentTableCard } from "./AgentTableCard";
import { userEvent, within, expect } from "@storybook/test";
import { type StatusType } from "./Status";
const meta: Meta<typeof AgentTableCard> = {
title: "AGPT UI/Agent Table Card",
component: AgentTableCard,
tags: ["autodocs"],
};
export default meta;
type Story = StoryObj<typeof AgentTableCard>;
export const Default: Story = {
args: {
agentName: "Super Coder",
description: "An AI agent that writes clean, efficient code",
imageSrc: [
"https://ddz4ak4pa3d19.cloudfront.net/cache/53/b2/53b2bc7d7900f0e1e60bf64ebf38032d.jpg",
],
dateSubmitted: "2023-05-15",
status: "ACTIVE" as StatusType,
runs: 1500,
rating: 4.8,
},
};
export const NoRating: Story = {
args: {
...Default.args,
rating: undefined,
},
};
export const NoRuns: Story = {
args: {
...Default.args,
runs: undefined,
},
};
export const InactiveAgent: Story = {
args: {
...Default.args,
status: "INACTIVE" as StatusType,
},
};
export const LongDescription: Story = {
args: {
...Default.args,
description:
"This is a very long description that should wrap to multiple lines. It contains detailed information about the agent and its capabilities.",
},
};
export const InteractionTest: Story = {
...Default,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const moreButton = canvas.getByRole("button");
await userEvent.click(moreButton);
},
};

View File

@@ -50,7 +50,10 @@ export const AgentTableCard: React.FC<AgentTableCardProps> = ({
};
return (
<div className="border-b border-neutral-300 p-4 dark:border-neutral-700">
<div
className="border-b border-neutral-300 p-4 dark:border-neutral-700"
data-testid="agent-table-card"
>
<div className="flex gap-4">
<div className="relative h-[56px] w-[100px] overflow-hidden rounded-lg bg-[#d9d9d9] dark:bg-neutral-800">
<Image
@@ -61,10 +64,10 @@ export const AgentTableCard: React.FC<AgentTableCardProps> = ({
/>
</div>
<div className="flex-1">
<h3 className="text-[15px] font-medium text-neutral-800 dark:text-neutral-200">
<h3 className="font-sans text-sm font-medium text-neutral-600">
{agentName}
</h3>
<p className="line-clamp-2 text-sm text-neutral-600 dark:text-neutral-400">
<p className="font-sans text-sm font-normal text-neutral-600">
{description}
</p>
</div>
@@ -76,16 +79,16 @@ export const AgentTableCard: React.FC<AgentTableCardProps> = ({
</button>
</div>
<div className="mt-4 flex flex-wrap gap-4">
<div className="mt-4 flex flex-wrap items-center gap-4">
<Status status={status} />
<div className="text-sm text-neutral-600 dark:text-neutral-400">
<div className="font-sans text-sm font-normal text-neutral-600">
{dateSubmitted}
</div>
<div className="text-sm text-neutral-600 dark:text-neutral-400">
<div className="font-sans text-sm font-normal text-neutral-600">
{runs.toLocaleString()} runs
</div>
<div className="flex items-center gap-1">
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
<span className="font-sans text-sm font-normal text-neutral-600">
{rating.toFixed(1)}
</span>
<IconStarFilled className="h-4 w-4 text-neutral-800 dark:text-neutral-200" />

View File

@@ -7,6 +7,8 @@ import { Status, StatusType } from "./Status";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { TrashIcon } from "@radix-ui/react-icons";
import { StoreSubmissionRequest } from "@/lib/autogpt-server-api/types";
import { TableCell, TableRow } from "../ui/table";
import { Checkbox } from "../ui/checkbox";
export interface AgentTableRowProps {
agent_id: string;
@@ -82,28 +84,22 @@ export const AgentTableRow: React.FC<AgentTableRowProps> = ({
}, [agent_id, selectedAgents, setSelectedAgents]);
return (
<div className="hidden items-center border-b border-neutral-300 px-4 py-4 hover:bg-neutral-50 dark:border-neutral-700 dark:hover:bg-neutral-800 md:flex">
<div className="flex items-center">
<div className="flex items-center">
<input
type="checkbox"
id={checkboxId}
aria-label={`Select ${agentName}`}
className="mr-4 h-5 w-5 rounded border-2 border-neutral-400 dark:border-neutral-600"
checked={selectedAgents.has(agent_id)}
onChange={handleCheckboxChange}
/>
{/* Single label instead of multiple */}
<label htmlFor={checkboxId} className="sr-only">
Select {agentName}
</label>
</div>
</div>
<TableRow className="space-x-2.5 hover:bg-neutral-50 dark:hover:bg-neutral-800">
<TableCell className="w-[40px]">
<Checkbox
id={checkboxId}
aria-label={`Select ${agentName}`}
checked={selectedAgents.has(agent_id)}
onCheckedChange={handleCheckboxChange}
/>
<label htmlFor={checkboxId} className="sr-only">
Select {agentName}
</label>
</TableCell>
<div className="grid w-full grid-cols-[minmax(400px,1fr),180px,140px,100px,100px,40px] items-center gap-4">
{/* Agent info column */}
<TableCell>
<div className="flex items-center gap-4">
<div className="relative h-[70px] w-[125px] overflow-hidden rounded-[10px] bg-[#d9d9d9] dark:bg-neutral-700">
<div className="relative aspect-video w-[125px] overflow-hidden rounded-[10px] bg-[#d9d9d9] dark:bg-neutral-700">
<Image
src={imageSrc?.[0] ?? "/nada.png"}
alt={agentName}
@@ -112,74 +108,69 @@ export const AgentTableRow: React.FC<AgentTableRowProps> = ({
/>
</div>
<div className="flex flex-col">
<h3 className="text-[15px] font-medium text-neutral-800 dark:text-neutral-200">
<h3 className="font-sans text-sm font-medium text-neutral-800">
{agentName}
</h3>
<p className="line-clamp-2 text-sm text-neutral-600 dark:text-neutral-400">
<p className="line-clamp-2 font-sans text-sm font-normal text-neutral-600">
{description}
</p>
</div>
</div>
</TableCell>
{/* Date column */}
<div className="pl-14 text-sm text-neutral-600 dark:text-neutral-400">
{dateSubmitted}
</div>
<TableCell className="font-sans text-sm font-normal text-neutral-600">
{dateSubmitted}
</TableCell>
{/* Status column */}
<div>
<Status status={status} />
</div>
<TableCell>
<Status status={status} />
</TableCell>
{/* Runs column */}
<div className="text-right text-sm text-neutral-600 dark:text-neutral-400">
{runs?.toLocaleString() ?? "0"}
</div>
<TableCell className="text-right font-sans text-sm font-normal text-neutral-600">
{runs?.toLocaleString() ?? "-"}
</TableCell>
{/* Reviews column */}
<div className="text-right">
{rating ? (
<div className="flex items-center justify-end gap-1">
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
{rating.toFixed(1)}
</span>
<IconStarFilled className="h-4 w-4 text-neutral-800 dark:text-neutral-200" />
</div>
) : (
<span className="text-sm text-neutral-600 dark:text-neutral-400">
No reviews
<TableCell className="text-right font-sans text-sm font-normal text-neutral-600">
{rating ? (
<div className="flex items-center justify-end gap-1">
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
{rating.toFixed(1)}
</span>
)}
</div>
<IconStarFilled className="h-4 w-4 text-neutral-800 dark:text-neutral-200" />
</div>
) : (
<span className="text-sm text-neutral-600 dark:text-neutral-400">
No reviews
</span>
)}
</TableCell>
{/* Actions - Three dots menu */}
<div className="flex justify-end">
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<button className="rounded-full p-1 hover:bg-neutral-100 dark:hover:bg-neutral-700">
<IconMore className="h-5 w-5 text-neutral-800 dark:text-neutral-200" />
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Content className="z-10 rounded-xl border bg-white p-1 shadow-md dark:bg-gray-800">
<DropdownMenu.Item
onSelect={handleEdit}
className="flex cursor-pointer items-center rounded-md px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700"
>
<IconEdit className="mr-2 h-5 w-5 dark:text-gray-100" />
<span className="dark:text-gray-100">Edit</span>
</DropdownMenu.Item>
<DropdownMenu.Separator className="my-1 h-px bg-gray-300 dark:bg-gray-600" />
<DropdownMenu.Item
onSelect={handleDelete}
className="flex cursor-pointer items-center rounded-md px-3 py-2 text-red-500 hover:bg-gray-100 dark:hover:bg-gray-700"
>
<TrashIcon className="mr-2 h-5 w-5 text-red-500 dark:text-red-400" />
<span className="dark:text-red-400">Delete</span>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
</div>
</div>
<TableCell className="text-right font-sans text-sm font-normal text-neutral-600">
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<button className="rounded-full p-1 hover:bg-neutral-100 dark:hover:bg-neutral-700">
<IconMore className="h-5 w-5 text-neutral-800 dark:text-neutral-200" />
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Content className="z-10 rounded-xl border bg-white p-1 shadow-md dark:bg-gray-800">
<DropdownMenu.Item
onSelect={handleEdit}
className="flex cursor-pointer items-center rounded-md px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700"
>
<IconEdit className="mr-2 h-5 w-5 dark:text-gray-100" />
<span className="dark:text-gray-100">Edit</span>
</DropdownMenu.Item>
<DropdownMenu.Separator className="my-1 h-px bg-gray-300 dark:bg-gray-600" />
<DropdownMenu.Item
onSelect={handleDelete}
className="flex cursor-pointer items-center rounded-md px-3 py-2 text-red-500 hover:bg-gray-100 dark:hover:bg-gray-700"
>
<TrashIcon className="mr-2 h-5 w-5 text-red-500 dark:text-red-400" />
<span className="dark:text-red-400">Delete</span>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</TableCell>
</TableRow>
);
};

View File

@@ -12,46 +12,31 @@ const statusConfig: Record<
bgColor: string;
dotColor: string;
text: string;
darkBgColor: string;
darkDotColor: string;
}
> = {
draft: {
bgColor: "bg-blue-50",
dotColor: "bg-blue-500",
text: "Draft",
darkBgColor: "dark:bg-blue-900",
darkDotColor: "dark:bg-blue-300",
},
awaiting_review: {
bgColor: "bg-amber-50",
dotColor: "bg-amber-500",
text: "Awaiting review",
darkBgColor: "dark:bg-amber-900",
darkDotColor: "dark:bg-amber-300",
},
approved: {
bgColor: "bg-green-50",
dotColor: "bg-green-500",
text: "Approved",
darkBgColor: "dark:bg-green-900",
darkDotColor: "dark:bg-green-300",
},
rejected: {
bgColor: "bg-red-50",
dotColor: "bg-red-500",
text: "Rejected",
darkBgColor: "dark:bg-red-900",
darkDotColor: "dark:bg-red-300",
},
};
export const Status: React.FC<StatusProps> = ({ status }) => {
/**
* Status component displays a badge with a colored dot and text indicating the agent's status
* @param status - The current status of the agent
* Valid values: 'draft', 'awaiting_review', 'approved', 'rejected'
*/
if (!status) {
return <Status status="awaiting_review" />;
} else if (!statusConfig[status]) {
@@ -62,12 +47,10 @@ export const Status: React.FC<StatusProps> = ({ status }) => {
return (
<div
className={`px-2.5 py-1 ${config.bgColor} ${config.darkBgColor} flex items-center gap-1.5 rounded-[26px]`}
className={`px-2.5 py-1 ${config.bgColor} flex w-fit items-center gap-1.5 rounded-3xl`}
>
<div
className={`h-3 w-3 ${config.dotColor} ${config.darkDotColor} rounded-full`}
/>
<div className="font-sans text-sm font-normal leading-tight text-neutral-600 dark:text-neutral-300">
<div className={`h-3 w-3 ${config.dotColor} rounded-full`} />
<div className="font-sans text-sm font-normal text-neutral-600">
{config.text}
</div>
</div>

View File

@@ -73,7 +73,7 @@ const TableHead = React.forwardRef<
<th
ref={ref}
className={cn(
"h-10 px-2 text-left align-middle font-medium text-neutral-500 dark:text-neutral-400 [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
"h-10 pr-2.5 text-left align-middle font-medium text-neutral-500 dark:text-neutral-400 [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className,
)}
{...props}
@@ -88,7 +88,7 @@ const TableCell = React.forwardRef<
<td
ref={ref}
className={cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
"py-3 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className,
)}
{...props}