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
7 changes: 5 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AuthProvider } from "./auth/AuthContext";
import { ThemeProvider } from "./hooks/useTheme";
import { Toaster } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { RouterProvider, Route, Redirect, AuthGuard, useRouter } from "./router";
import { AppShell } from "./components/layout/AppShell";
import { Login } from "./pages/Login";
Expand Down Expand Up @@ -97,8 +98,10 @@ export function App() {
<RouterProvider>
<ThemeProvider>
<AuthProvider>
<Routes />
<Toaster />
<TooltipProvider>
<Routes />
<Toaster />
</TooltipProvider>
</AuthProvider>
</ThemeProvider>
</RouterProvider>
Expand Down
35 changes: 15 additions & 20 deletions src/components/gateways/VirtualServerDetailsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { useIntl } from "react-intl";
import {
Activity,
Box,
Copy,
EllipsisVertical,
Loader2,
MessageSquareCode,
Expand All @@ -20,13 +19,13 @@ import {
import { MCPIcon } from "@/components/icons/MCPIcon";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { CopyButton } from "@/components/ui/copy-button";
import { InlineTagAdd } from "@/components/ui/inline-tag-add";
import { CopyValue } from "@/components/ui/copy-value";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import type { MCPServer, VirtualServer } from "@/types/server";
import type { ComponentFilter } from "@/components/gateways/types";
import { copyToClipboard } from "@/lib/clipboard";
import {
buildComponentItems,
formatServerDateTime,
Expand Down Expand Up @@ -586,32 +585,28 @@ export function VirtualServerDetailsPanel({
<span className="min-w-0 truncate text-muted-foreground">{title}</span>
<span className="flex min-w-0 items-center gap-2 font-mono text-[13px] text-muted-foreground">
<span className="truncate">{identifier}</span>
<Button
type="button"
variant="ghost"
size="icon-xs"
aria-label={`Copy ${title}`}
<CopyButton
value={identifier}
label={intl.formatMessage(
{ id: "common.copyValue" },
{ label: title },
)}
className="size-5 text-muted-foreground"
onClick={() => copyToClipboard(identifier)}
>
<Copy className="size-3.5" />
</Button>
/>
</span>
</>
) : (
<>
<span className="flex min-w-0 items-center gap-2 font-mono text-[13px] text-muted-foreground">
<span className="truncate">{identifier}</span>
<Button
type="button"
variant="ghost"
size="icon-xs"
aria-label={`Copy ${identifier}`}
<CopyButton
value={identifier}
label={intl.formatMessage(
{ id: "common.copyValue" },
{ label: identifier },
)}
className="size-5 text-muted-foreground"
onClick={() => copyToClipboard(identifier)}
>
<Copy className="size-3.5" />
</Button>
/>
</span>
<span aria-hidden="true" />
</>
Expand Down
21 changes: 7 additions & 14 deletions src/components/prompts/PromptDefinitionTable.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Copy, MoreHorizontal } from "lucide-react";
import { MoreHorizontal } from "lucide-react";
import { useIntl } from "react-intl";

import type { PromptRead } from "@/generated/types";
import { Button } from "@/components/ui/button";
import { CopyButton } from "@/components/ui/copy-button";
import {
DropdownMenu,
DropdownMenuContent,
Expand All @@ -17,7 +18,6 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { copyToClipboard } from "@/lib/clipboard";
import { truncateMiddle } from "@/components/gateways/utils";

export interface PromptDefinitionTableProps {
Expand Down Expand Up @@ -88,22 +88,15 @@ export function PromptDefinitionTable({
<span className="min-w-0 truncate font-mono text-xs text-muted-foreground">
{truncateMiddle(prompt.id, 40)}
</span>
<Button
type="button"
variant="ghost"
size="icon-xs"
aria-label={intl.formatMessage(
<CopyButton
value={prompt.id}
label={intl.formatMessage(
{ id: "prompts.details.table.copyPromptId" },
{ name: prompt.name },
)}
iconClassName="size-3"
className="ml-4 size-4 shrink-0 text-muted-foreground hover:text-foreground"
onClick={(e) => {
e.stopPropagation();
copyToClipboard(prompt.id);
}}
>
<Copy className="size-3" />
</Button>
/>
</div>
</TableCell>

Expand Down
3 changes: 1 addition & 2 deletions src/components/prompts/PromptPreviewResult.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@ export function PromptPreviewResult({ preview }: PromptPreviewResultProps) {
<CodeBlock
code={JSON.stringify({ messages: result.rendered.messages ?? [] }, null, 2)}
language="json"
copyLabel="JSON"
copiedLabel={intl.formatMessage({ id: "prompts.details.code.copySuccess" })}
copyLabel={intl.formatMessage({ id: "common.copyValue" }, { label: "JSON" })}
/>
)}

Expand Down
3 changes: 0 additions & 3 deletions src/components/prompts/PromptSnippetTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,6 @@ export function PromptSnippetTabs({
[promptName, args],
);

const copiedLabel = intl.formatMessage({ id: "prompts.details.code.copySuccess" });

return (
<Tabs value={value} onValueChange={onValueChange}>
<div className="mb-2 flex items-center justify-between gap-4">
Expand All @@ -96,7 +94,6 @@ export function PromptSnippetTabs({
{ id: "prompts.details.code.copyAriaLabel" },
{ language: snippet.language },
)}
copiedLabel={copiedLabel}
/>
</TabsContent>
))}
Expand Down
2 changes: 0 additions & 2 deletions src/components/resources/ResourcePreviewResult.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,13 +159,11 @@ function ResourceContentPreview({
}

if (kind === "code") {
const copiedLabel = intl.formatMessage({ id: "resources.details.code.copySuccess" });
return (
<CodeBlock
code={content.text ?? ""}
language={codeLanguageFor(mimeType)}
copyLabel={mimeType || "content"}
copiedLabel={copiedLabel}
/>
);
}
Expand Down
2 changes: 0 additions & 2 deletions src/components/resources/ResourceTryItTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ function ResourcePreviewPane({ resource }: { resource: NonNullable<ResourceRead>
[preview],
);

const copiedLabel = intl.formatMessage({ id: "resources.details.code.copySuccess" });
const rendered = useMemo(
() => RESOURCE_SNIPPETS.map((spec) => ({ ...spec, text: spec.build({ uri: resolvedUri }) })),
[resolvedUri],
Expand Down Expand Up @@ -154,7 +153,6 @@ function ResourcePreviewPane({ resource }: { resource: NonNullable<ResourceRead>
{ id: "resources.details.code.copyAriaLabel" },
{ language: snippet.language },
)}
copiedLabel={copiedLabel}
/>
</TabsContent>
))}
Expand Down
2 changes: 1 addition & 1 deletion src/components/resources/ResourcesTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe("ResourcesTable", () => {
beforeEach(() => {
vi.clearAllMocks();
mockOnSelectResource.mockClear();
vi.spyOn(clipboardUtils, "copyToClipboard").mockImplementation(() => {});
vi.spyOn(clipboardUtils, "copyToClipboard").mockResolvedValue(true);
});

it("renders table with correct headers", () => {
Expand Down
38 changes: 12 additions & 26 deletions src/components/resources/ResourcesTable.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useIntl } from "react-intl";
import { Copy, MoreHorizontal } from "lucide-react";
import { MoreHorizontal } from "lucide-react";
import { Button } from "@/components/ui/button";
import { CopyButton } from "@/components/ui/copy-button";
import {
DropdownMenu,
DropdownMenuContent,
Expand All @@ -16,7 +17,6 @@ import {
TableRow,
} from "@/components/ui/table";
import type { ResourceRead } from "@/generated/types";
import { copyToClipboard } from "@/lib/clipboard";
import { truncateMiddle } from "@/components/gateways/utils";

export function ResourcesTable({
Expand Down Expand Up @@ -78,22 +78,15 @@ export function ResourcesTable({
<span className="min-w-0 truncate font-mono text-xs text-muted-foreground">
{truncateMiddle(resource.uriTemplate || resource.uri, 28)}
</span>
<Button
type="button"
variant="ghost"
size="icon-xs"
aria-label={intl.formatMessage(
<CopyButton
value={resource.uriTemplate || resource.uri}
label={intl.formatMessage(
{ id: "resources.table.copyUri" },
{ uri: resource.uriTemplate || resource.uri },
)}
iconClassName="size-3"
className="ml-4 size-4 shrink-0 text-muted-foreground hover:text-foreground"
onClick={(e) => {
e.stopPropagation();
copyToClipboard(resource.uriTemplate || resource.uri);
}}
>
<Copy className="size-3" />
</Button>
/>
</div>
</TableCell>

Expand All @@ -102,19 +95,12 @@ export function ResourcesTable({
<span className="min-w-0 truncate font-mono text-xs text-muted-foreground">
{truncateMiddle(resource.id, 18)}
</span>
<Button
type="button"
variant="ghost"
size="icon-xs"
aria-label={intl.formatMessage({ id: "resources.table.copyResourceId" })}
<CopyButton
value={resource.id}
label={intl.formatMessage({ id: "resources.table.copyResourceId" })}
iconClassName="size-3"
className="ml-4 size-4 shrink-0 text-muted-foreground hover:text-foreground"
onClick={(e) => {
e.stopPropagation();
copyToClipboard(resource.id);
}}
>
<Copy className="size-3" />
</Button>
/>
</div>
</TableCell>

Expand Down
35 changes: 15 additions & 20 deletions src/components/servers/MCPServerDetailsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { useIntl } from "react-intl";
import {
Activity,
Box,
Copy,
Globe,
Loader2,
MessageSquareCode,
Expand All @@ -20,12 +19,12 @@ import {
} from "@/components/common/VisibilityInfoPopover";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { CopyButton } from "@/components/ui/copy-button";
import { InlineTagAdd } from "@/components/ui/inline-tag-add";
import { CopyValue } from "@/components/ui/copy-value";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import type { MCPServer as BaseMCPServer, VirtualServerTag } from "@/types/server";
import { copyToClipboard } from "@/lib/clipboard";
import { useQuery } from "@/hooks/useQuery";
import { TestConnectionPanel } from "./TestConnectionPanel";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
Expand Down Expand Up @@ -489,32 +488,28 @@ export function MCPServerDetailsPanel({
</span>
<span className="flex min-w-0 items-center gap-2 font-mono text-[13px] text-muted-foreground">
<span className="truncate">{identifier}</span>
<Button
type="button"
variant="ghost"
size="icon-xs"
aria-label={`Copy ${title}`}
<CopyButton
value={identifier}
label={intl.formatMessage(
{ id: "common.copyValue" },
{ label: title },
)}
className="size-5 text-muted-foreground"
onClick={() => copyToClipboard(identifier)}
>
<Copy className="size-3.5" />
</Button>
/>
</span>
</>
) : (
<>
<span className="flex min-w-0 items-center gap-2 font-mono text-[13px] text-muted-foreground">
<span className="truncate">{identifier}</span>
<Button
type="button"
variant="ghost"
size="icon-xs"
aria-label={`Copy ${identifier}`}
<CopyButton
value={identifier}
label={intl.formatMessage(
{ id: "common.copyValue" },
{ label: identifier },
)}
className="size-5 text-muted-foreground"
onClick={() => copyToClipboard(identifier)}
>
<Copy className="size-3.5" />
</Button>
/>
</span>
<span aria-hidden="true" />
</>
Expand Down
19 changes: 12 additions & 7 deletions src/components/servers/ServersTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@ import { render, screen, fireEvent, waitFor, act } from "@testing-library/react"
import userEvent from "@testing-library/user-event";
import { ServersTable } from "./ServersTable";
import { I18nProvider } from "@/i18n";
import { TooltipProvider } from "@/components/ui/tooltip";
import type { MCPServer } from "../../types/server";
import type { ReactElement } from "react";

function renderTable(ui: ReactElement) {
return render(<I18nProvider>{ui}</I18nProvider>);
return render(
<I18nProvider>
<TooltipProvider>{ui}</TooltipProvider>
</I18nProvider>,
);
}

// Minimal server factory
Expand Down Expand Up @@ -182,7 +187,7 @@ describe("ServersTable", () => {
await act(async () => {
await Promise.resolve();
});
expect(screen.getByText("Copied!")).toBeInTheDocument();
expect(screen.getByRole("status")).toHaveTextContent("Copied!");

// A second copy before the timeout clears the pending timer first.
fireEvent.click(copyBtn);
Expand All @@ -195,13 +200,12 @@ describe("ServersTable", () => {
act(() => {
vi.advanceTimersByTime(1500);
});
expect(screen.queryByText("Copied!")).not.toBeInTheDocument();
expect(screen.queryByRole("status")).not.toBeInTheDocument();

vi.useRealTimers();
});

it("logs an error when copying to the clipboard fails", async () => {
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
it("shows a failed indicator instead of throwing when copying to the clipboard fails", async () => {
const writeText = vi.fn().mockRejectedValue(new Error("denied"));
Object.defineProperty(navigator, "clipboard", {
value: { writeText },
Expand All @@ -216,8 +220,9 @@ describe("ServersTable", () => {

fireEvent.click(screen.getByRole("button", { name: /copy uuid for test server/i }));

await waitFor(() => expect(consoleError).toHaveBeenCalled());
consoleError.mockRestore();
await waitFor(() => {
expect(screen.getByRole("status")).toHaveTextContent("Copy failed");
});
});

// ── Visibility cell ─────────────────────────────────────────────────────────
Expand Down
Loading
Loading