Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 17 additions & 14 deletions e2e/virtual-servers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ test.describe("Virtual Servers page", () => {
await expect(page.getByText("testVS")).toBeVisible();

const card = page.getByTestId("virtual-server-card").filter({ hasText: "testVS" });
await expect(card.getByTestId("status-indicator")).toHaveClass(/bg-emerald-500/);
await expect(card.getByTestId("status-indicator")).toHaveClass(/bg-tool-status-active/);
await expect(card.getByLabel("Enabled")).toBeVisible();

await expect(card.getByTestId("tool-count")).toHaveText("2");
Expand Down Expand Up @@ -435,15 +435,15 @@ test.describe("Virtual Servers page", () => {
await page.waitForLoadState("networkidle");

const card = page.getByTestId("virtual-server-card").filter({ hasText: "testVS" });
await expect(card.getByTestId("status-indicator")).toHaveClass(/bg-red-500/);
await expect(card.getByTestId("status-indicator")).toHaveClass(/bg-tool-status-inactive/);
await expect(card.getByLabel("Disabled")).toBeVisible();

await page.getByRole("button", { name: "Actions for testVS" }).click();
await page.getByRole("menuitem", { name: "Activate" }).click();

await expect.poll(() => stateRequestCount).toBe(1);
await expect(page.getByRole("dialog")).toHaveCount(0);
await expect(card.getByTestId("status-indicator")).toHaveClass(/bg-emerald-500/);
await expect(card.getByTestId("status-indicator")).toHaveClass(/bg-tool-status-active/);
await expect(card.getByLabel("Enabled")).toBeVisible();
await expect(
page.locator("[data-sonner-toast]").filter({ hasText: "testVS activated." }),
Expand Down Expand Up @@ -485,7 +485,7 @@ test.describe("Virtual Servers page", () => {
await dialog.getByRole("button", { name: "Cancel" }).click();
await expect(dialog).toHaveCount(0);
expect(stateRequestCount).toBe(0);
await expect(card.getByTestId("status-indicator")).toHaveClass(/bg-emerald-500/);
await expect(card.getByTestId("status-indicator")).toHaveClass(/bg-tool-status-active/);

await page.getByRole("button", { name: "Actions for testVS" }).click();
await page.getByRole("menuitem", { name: "Deactivate" }).click();
Expand All @@ -496,7 +496,7 @@ test.describe("Virtual Servers page", () => {

await expect.poll(() => stateRequestCount).toBe(1);
await expect(dialog).toHaveCount(0);
await expect(card.getByTestId("status-indicator")).toHaveClass(/bg-red-500/);
await expect(card.getByTestId("status-indicator")).toHaveClass(/bg-tool-status-inactive/);
await expect(card.getByLabel("Disabled")).toBeVisible();
await expect(
page.locator("[data-sonner-toast]").filter({ hasText: "testVS deactivated." }),
Expand Down Expand Up @@ -1116,7 +1116,7 @@ test.describe("Virtual Servers page", () => {
);
});

test("disables the Upload action button on virtual server cards", async ({ page }) => {
test("shows only the actions menu in the virtual server card header", async ({ page }) => {
await page.route("**/servers?*", async (route) => {
await route.fulfill({
status: 200,
Expand All @@ -1128,7 +1128,8 @@ test.describe("Virtual Servers page", () => {
await page.goto(APP.GATEWAYS);
await page.waitForLoadState("networkidle");

await expect(page.getByRole("button", { name: /Open testVS \(coming soon\)/ })).toBeDisabled();
await expect(page.getByRole("button", { name: /Open testVS \(coming soon\)/ })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Actions for testVS" })).toBeVisible();
});

test("navigates to create server UI from the create server card", async ({ page }) => {
Expand Down Expand Up @@ -1183,7 +1184,7 @@ test.describe("Virtual Servers page", () => {

const card = page.getByTestId("virtual-server-card").filter({ hasText: "testVS" });

await expect(card.getByTestId("status-indicator")).toHaveClass(/bg-red-500/);
await expect(card.getByTestId("status-indicator")).toHaveClass(/bg-tool-status-inactive/);
await expect(card.getByRole("img", { name: "Disabled" })).toBeVisible();

await expect(card.getByText("disabled")).toBeVisible();
Expand Down Expand Up @@ -1220,7 +1221,7 @@ test.describe("Virtual Servers page", () => {
await expect(card2.getByText("private")).toBeVisible();
});

test("places empty virtual servers after servers with components", async ({ page }) => {
test("renders virtual servers in the order the API returned them", async ({ page }) => {
const emptyServer = {
...MOCK_VIRTUAL_SERVER,
id: "empty-server-id",
Expand All @@ -1244,13 +1245,15 @@ test.describe("Virtual Servers page", () => {
await page.goto(APP.GATEWAYS);
await page.waitForLoadState("networkidle");

// Cards are uniform now, so empty servers are no longer sorted to the end
// to keep a full-width row from splitting the grid.
const cards = page.getByTestId("virtual-server-card");
await expect(cards).toHaveCount(2);
await expect(cards.nth(0)).toHaveAttribute("data-server-name", "testVS");
await expect(cards.nth(1)).toHaveAttribute("data-server-name", "peach-thistle-shark");
await expect(cards.nth(1)).toHaveClass(/col-span-full/);
await expect(cards.nth(0)).toHaveAttribute("data-server-name", "peach-thistle-shark");
await expect(cards.nth(1)).toHaveAttribute("data-server-name", "testVS");
await expect(cards.nth(0)).not.toHaveClass(/col-span-full/);
await expect(
cards.nth(1).getByRole("button", { name: "Add sources and components" }),
cards.nth(0).getByRole("button", { name: "Add sources and components" }),
).toBeVisible();
});

Expand Down Expand Up @@ -1322,7 +1325,7 @@ test.describe("Virtual Servers page", () => {

const card = page.getByTestId("virtual-server-card").filter({ hasText: "testVS" });

await expect(card).toHaveClass(/col-span-full/);
await expect(card).not.toHaveClass(/col-span-full/);
await expect(card.getByRole("button", { name: "Add sources and components" })).toBeVisible();
await expect(card.getByTestId("tool-count")).toHaveCount(0);
await expect(card.getByTestId("resource-count")).toHaveCount(0);
Expand Down
8 changes: 4 additions & 4 deletions src/components/gateways/ConnectSourceCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function ConnectSourceCard({ onAction }: { onAction: () => void }) {
size="sm"
role="button"
tabIndex={0}
className="min-h-35 cursor-pointer justify-center transition-colors hover:bg-muted/40"
className="cursor-pointer transition-opacity hover:opacity-90"
onClick={onAction}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
Expand All @@ -21,14 +21,14 @@ export function ConnectSourceCard({ onAction }: { onAction: () => void }) {
>
<CardHeader className="gap-3">
<div className="flex items-center gap-3">
<span className="flex size-6 items-center justify-center rounded-sm bg-primary text-primary-foreground">
<Plus className="size-4" />
<span className="flex size-6 shrink-0 items-center justify-center rounded bg-tool-add-icon-bg text-tool-add-icon-fg shadow-sm">
<Plus className="size-3.5" />
</span>
<CardTitle>{intl.formatMessage({ id: "gateways.createServer.card.title" })}</CardTitle>
</div>
</CardHeader>
<CardContent>
<CardDescription className="text-[13px] leading-4">
<CardDescription className="leading-relaxed">
{intl.formatMessage({ id: "gateways.createServer.card.description" })}
</CardDescription>
</CardContent>
Expand Down
8 changes: 4 additions & 4 deletions src/components/gateways/GatewayCards.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,14 @@ describe("VirtualServerCard", () => {
expect(screen.getByText("My Test Server")).toBeTruthy();
});

it("shows green status indicator for enabled server", () => {
it("shows an active status indicator for enabled server", () => {
renderWithProviders(<VirtualServerCard server={mockServer} onViewDetails={vi.fn()} />);
expect(screen.getByTestId("status-indicator")).toHaveClass("bg-emerald-500");
expect(screen.getByTestId("status-indicator")).toHaveClass("bg-tool-status-active");
});

it("shows red status indicator for disabled server", () => {
it("shows an inactive status indicator for disabled server", () => {
renderWithProviders(<VirtualServerCard server={emptyServer} onViewDetails={vi.fn()} />);
expect(screen.getByTestId("status-indicator")).toHaveClass("bg-red-500");
expect(screen.getByTestId("status-indicator")).toHaveClass("bg-tool-status-inactive");
});

it("calls onViewDetails when card is clicked", () => {
Expand Down
13 changes: 7 additions & 6 deletions src/components/gateways/VirtualServerCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,19 +77,19 @@ describe("VirtualServerCard", () => {
expect(onViewDetails).toHaveBeenCalledWith(expect.objectContaining({ id: "vs-1" }));
});

it("shows a green enabled indicator for enabled server", () => {
it("shows an active enabled indicator for enabled server", () => {
renderWithProviders(
<VirtualServerCard server={makeServer({ enabled: true })} onViewDetails={vi.fn()} />,
);
expect(screen.getByTestId("status-indicator")).toHaveClass("bg-emerald-500");
expect(screen.getByTestId("status-indicator")).toHaveClass("bg-tool-status-active");
expect(screen.getByRole("img", { name: "Enabled" })).toBeTruthy();
});

it("shows a red disabled indicator for disabled server", () => {
it("shows an inactive disabled indicator for disabled server", () => {
renderWithProviders(
<VirtualServerCard server={makeServer({ enabled: false })} onViewDetails={vi.fn()} />,
);
expect(screen.getByTestId("status-indicator")).toHaveClass("bg-red-500");
expect(screen.getByTestId("status-indicator")).toHaveClass("bg-tool-status-inactive");
expect(screen.getByRole("img", { name: "Disabled" })).toBeTruthy();
});

Expand Down Expand Up @@ -231,13 +231,14 @@ describe("VirtualServerCard", () => {
expect(document.querySelector(".custom-class")).toBeTruthy();
});

it("shows upload button for non-empty server", () => {
it("shows only the actions menu in the card header", () => {
renderWithProviders(
<VirtualServerCard
server={makeServer({ associatedTools: ["t1"] })}
onViewDetails={vi.fn()}
/>,
);
expect(screen.getByRole("button", { name: /Open.*coming soon/i })).toBeTruthy();
expect(screen.queryByRole("button", { name: /Open.*coming soon/i })).toBeNull();
expect(screen.getByRole("button", { name: /Actions for/i })).toBeTruthy();
});
});
28 changes: 7 additions & 21 deletions src/components/gateways/VirtualServerCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import type { VirtualServer } from "@/types/server";
import { Box, EllipsisVertical, MessageSquareCode, Plus, Upload, Wrench } from "lucide-react";
import { Box, EllipsisVertical, MessageSquareCode, Plus, Wrench } from "lucide-react";
import { useIntl } from "react-intl";

export function VirtualServerCard({
Expand Down Expand Up @@ -52,26 +52,22 @@ export function VirtualServerCard({
return (
<Card
size="sm"
className={cn(
isEmptyComposition ? "min-h-29 justify-center" : "min-h-35 justify-between",
"cursor-pointer transition-colors hover:bg-accent/50",
className,
)}
className={cn("cursor-pointer transition-colors hover:bg-accent/50", className)}
data-testid="virtual-server-card"
data-server-name={server.name}
onClick={() => onViewDetails(server)}
>
<CardHeader className="gap-3">
<div className="flex items-center gap-3">
<span className="flex size-6 shrink-0 items-center justify-center rounded-sm bg-primary text-primary-foreground">
<MCPIcon className="size-4 [&_path]:fill-current" />
<span className="flex size-6 shrink-0 items-center justify-center rounded bg-server-icon-bg text-black">
<MCPIcon className="size-3.5 [&_path]:fill-current" />
</span>
<div className="flex min-w-0 flex-1 items-center gap-2">
<CardTitle className="truncate">{server.name}</CardTitle>
<CardTitle className="truncate font-semibold">{server.name}</CardTitle>
<span
className={cn(
"size-1.5 rounded-full",
server.enabled ? "bg-emerald-500" : "bg-red-500",
"size-1.5 shrink-0 rounded-full",
server.enabled ? "bg-tool-status-active" : "bg-tool-status-inactive",
)}
data-testid="status-indicator"
role="img"
Expand All @@ -81,16 +77,6 @@ export function VirtualServerCard({
/>
</div>
<div className="flex shrink-0 items-center gap-1">
{!isEmptyComposition && (
<Button
variant="ghost"
size="icon-xs"
aria-label={`Open ${server.name} (coming soon)`}
disabled
>
<Upload className="size-4" />
</Button>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
Expand Down
5 changes: 5 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
/* Tool-specific semantic tokens */
--color-tool-icon-bg: var(--tool-icon-bg);
--color-prompt-icon-bg: var(--prompt-icon-bg);
--color-server-icon-bg: var(--server-icon-bg);
--color-tool-status-active: var(--tool-status-active);
--color-tool-status-inactive: var(--tool-status-inactive);
--color-tool-badge-bg: var(--tool-badge-bg);
Expand Down Expand Up @@ -129,6 +130,10 @@
--destructive: oklch(0.577 0.245 27.325);
--tool-icon-bg: var(--color-fuchsia-400);
--prompt-icon-bg: #6fff9f;
/* Brand cyan. Like the two above it this is a chip *background* carrying a
black glyph, so it needs no `.dark` override — only `--status-icon`, which
paints a foreground, has to step down on white. */
--server-icon-bg: #70f9ff;
/* emerald-400 falls under 3:1 on white; .dark restores it */
--tool-status-active: var(--color-emerald-600);
--tool-status-inactive: var(--color-neutral-500);
Expand Down
18 changes: 10 additions & 8 deletions src/pages/Gateways.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ describe("Gateways", () => {
expect(toast.error).toHaveBeenCalledWith("Failed to activate virtual server.");
});
expect(setData).not.toHaveBeenCalled();
expect(screen.getByTestId("status-indicator")).toHaveClass("bg-red-500");
expect(screen.getByTestId("status-indicator")).toHaveClass("bg-tool-status-inactive");
});

it("shows API error detail when activation fails", async () => {
Expand All @@ -502,7 +502,7 @@ describe("Gateways", () => {
});
});

it("renders empty virtual servers as full-width add-components rows", () => {
it("renders empty virtual servers as ordinary grid cards with an add-components row", () => {
const mockServer = makeServer({
id: "gateway-empty",
name: "peach-thistle-shark",
Expand All @@ -522,14 +522,14 @@ describe("Gateways", () => {

renderWithProviders(<Gateways />);

expect(screen.getByTestId("virtual-server-card")).toHaveClass("col-span-full");
expect(screen.getByTestId("virtual-server-card")).not.toHaveClass("col-span-full");
expect(screen.getByText("peach-thistle-shark")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Add sources and components" })).toBeInTheDocument();
expect(screen.queryByTestId("tool-count")).not.toBeInTheDocument();
expect(screen.queryByTestId("last-updated")).not.toBeInTheDocument();
});

it("renders empty virtual servers after servers with components", () => {
it("renders virtual servers in the order the API returned them", () => {
const emptyServer = makeServer({
id: "gateway-empty",
name: "peach-thistle-shark",
Expand All @@ -556,11 +556,13 @@ describe("Gateways", () => {

renderWithProviders(<Gateways />);

// Cards are uniform now, so empty servers are no longer sorted to the end
// to keep a full-width row from splitting the grid.
const renderedCards = screen.getAllByTestId("virtual-server-card");
expect(renderedCards).toHaveLength(2);
expect(renderedCards[0]).toHaveAttribute("data-server-name", "GH repo tasks");
expect(renderedCards[1]).toHaveAttribute("data-server-name", "peach-thistle-shark");
expect(renderedCards[1]).toHaveClass("col-span-full");
expect(renderedCards[0]).toHaveAttribute("data-server-name", "peach-thistle-shark");
expect(renderedCards[1]).toHaveAttribute("data-server-name", "GH repo tasks");
expect(renderedCards[0]).not.toHaveClass("col-span-full");
});

it("navigates to the create server UI when the create server card is clicked", async () => {
Expand Down Expand Up @@ -1161,7 +1163,7 @@ describe("Gateways", () => {
expect(screen.getByText("Sparse server")).toBeInTheDocument();
const card = screen.getByTestId("virtual-server-card");
expect(card).toBeInTheDocument();
expect(card).toHaveClass("col-span-full");
expect(card).not.toHaveClass("col-span-full");
expect(screen.getByRole("button", { name: "Add sources and components" })).toBeInTheDocument();
expect(card.querySelector('[data-testid="tool-count"]')).not.toBeInTheDocument();
expect(card.querySelector('[data-testid="resource-count"]')).not.toBeInTheDocument();
Expand Down
Loading