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
3 changes: 2 additions & 1 deletion src/components/dashboard/ServerRosterRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

import { useIntl } from "react-intl";

import { TruncatedText } from "@/components/ui/truncated-text";
import { formatLastSeen } from "@/utils/format";
import type { ClassifiedServer } from "./mcpServerRoster";
import { rowTone } from "./mcpServerRoster";
Expand Down Expand Up @@ -110,7 +111,7 @@ export function ServerRosterRowStacked({ classified }: ServerRosterRowProps) {
return (
<li className="flex flex-col gap-y-0.5 text-sm">
<StatusDot tone={tone} className="flex min-w-0">
<span className="min-w-0 truncate text-foreground">{name}</span>
<TruncatedText className="min-w-0 text-foreground">{name}</TruncatedText>
</StatusDot>
{showDetails && <span className="text-muted-foreground">{componentsText}</span>}
{showDetails && <span className="text-muted-foreground">{transportText}</span>}
Expand Down
5 changes: 3 additions & 2 deletions src/components/gateways/SourceSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { MCPIcon } from "@/components/icons/MCPIcon";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Loading } from "@/components/ui/loading";
import { TruncatedText } from "@/components/ui/truncated-text";
import type { ActionCard } from "@/components/gateways/types";
import { useQuery } from "@/hooks/useQuery";
import { cn } from "@/lib/utils";
Expand Down Expand Up @@ -404,9 +405,9 @@ export function SourceSelection({
<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>
<span className="min-w-0 truncate font-medium text-foreground">
<TruncatedText className="min-w-0 font-medium text-foreground">
{server.name}
</span>
</TruncatedText>
</div>
<div className="flex min-w-0 items-center gap-3 text-muted-foreground">
<span className="flex items-center gap-1.5">
Expand Down
11 changes: 7 additions & 4 deletions src/components/gateways/VirtualServerCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import { TruncatedText } from "@/components/ui/truncated-text";
import type { VirtualServer } from "@/types/server";
import { Box, EllipsisVertical, MessageSquareCode, Plus, Wrench } from "lucide-react";
import { useIntl } from "react-intl";
Expand Down Expand Up @@ -63,7 +64,9 @@ export function VirtualServerCard({
<MCPIcon className="size-3.5 [&_path]:fill-current" />
</span>
<div className="flex min-w-0 flex-1 items-center gap-2">
<CardTitle className="truncate font-semibold">{server.name}</CardTitle>
<CardTitle className="min-w-0 font-semibold">
<TruncatedText>{server.name}</TruncatedText>
</CardTitle>
<span
className={cn(
"size-1.5 shrink-0 rounded-full",
Expand Down Expand Up @@ -182,15 +185,15 @@ export function VirtualServerCard({
</Badge>
))}
</div>
<span
className="shrink-0 truncate text-[13px] text-muted-foreground"
<TruncatedText
className="shrink-0 text-[13px] text-muted-foreground"
data-testid="last-updated"
>
{formatServerTimestamp(
server.updatedAt || server.createdAt,
intl.formatMessage({ id: "gateways.card.notSyncedYet" }),
)}
</span>
</TruncatedText>
</div>
</CardContent>
)}
Expand Down
46 changes: 31 additions & 15 deletions src/components/gateways/VirtualServerDetailsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ 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 { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { TruncatedText } from "@/components/ui/truncated-text";
import { getTruncatedMiddle } from "@/components/ui/truncated-middle-text";
import { cn } from "@/lib/utils";
import type { MCPServer, VirtualServer } from "@/types/server";
import type { ComponentFilter } from "@/components/gateways/types";
Expand All @@ -31,7 +34,6 @@ import {
formatServerDateTime,
getTagDisplay,
getVirtualServerEndpoint,
truncateMiddle,
} from "@/components/gateways/utils";
import { useQuery } from "@/hooks/useQuery";

Expand Down Expand Up @@ -299,11 +301,16 @@ export function VirtualServerDetailsPanel({
const serverById = new Map(getMCPServers(sourcesData).map((source) => [source.id, source]));
return sourceIds.map((id) => {
const source = serverById.get(id);
return {
id,
label: source?.name ?? truncateMiddle(id, 32),
source,
};
if (source?.name) {
return {
id,
label: source.name,
isTruncated: false,
fullValue: undefined as string | undefined,
};
}
const { display, isTruncated } = getTruncatedMiddle(id, 32);
return { id, label: display, isTruncated, fullValue: id };
});
}, [sourceIds, sourcesData]);

Expand Down Expand Up @@ -410,12 +417,12 @@ export function VirtualServerDetailsPanel({
</span>
<div className="min-w-0">
<div className="flex min-w-0 items-center gap-2">
<span
<TruncatedText
aria-hidden="true"
className="truncate text-xl font-semibold text-foreground"
className="text-xl font-semibold text-foreground"
>
{server.name}
</span>
</TruncatedText>
</div>
</div>
</div>
Expand Down Expand Up @@ -450,14 +457,14 @@ export function VirtualServerDetailsPanel({
{
id: "all",
label: intl.formatMessage({ id: "gateways.details.filter.allSources" }),
isTruncated: false,
fullValue: undefined as string | undefined,
},
...sourceTabs,
].map((source, index, sources) => {
const isSelected = sourceFilter === source.id;

return (
const tabButton = (
<Button
key={source.id}
id={`source-tab-${index}`}
type="button"
variant="ghost"
Expand All @@ -477,6 +484,13 @@ export function VirtualServerDetailsPanel({
{source.label}
</Button>
);

return (
<Tooltip key={source.id}>
<TooltipTrigger asChild>{tabButton}</TooltipTrigger>
{source.isTruncated && <TooltipContent>{source.fullValue}</TooltipContent>}
</Tooltip>
);
})}
</div>
)}
Expand Down Expand Up @@ -582,9 +596,11 @@ export function VirtualServerDetailsPanel({
</Badge>
{title ? (
<>
<span className="min-w-0 truncate text-muted-foreground">{title}</span>
<TruncatedText className="min-w-0 text-muted-foreground">
{title}
</TruncatedText>
<span className="flex min-w-0 items-center gap-2 font-mono text-[13px] text-muted-foreground">
<span className="truncate">{identifier}</span>
<TruncatedText>{identifier}</TruncatedText>
<CopyButton
value={identifier}
label={intl.formatMessage(
Expand All @@ -598,7 +614,7 @@ export function VirtualServerDetailsPanel({
) : (
<>
<span className="flex min-w-0 items-center gap-2 font-mono text-[13px] text-muted-foreground">
<span className="truncate">{identifier}</span>
<TruncatedText>{identifier}</TruncatedText>
<CopyButton
value={identifier}
label={intl.formatMessage(
Expand Down
5 changes: 4 additions & 1 deletion src/components/layout/HeaderQuickNav.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { I18nProvider } from "@/i18n";
import { TooltipProvider } from "@/components/ui/tooltip";
import { searchEntities } from "@/api/search";
import { useAuthContext } from "@/auth/AuthContext";
import { useRouter } from "@/router";
Expand Down Expand Up @@ -30,7 +31,9 @@ function dropdown() {
function renderQuickNav() {
return render(
<I18nProvider>
<HeaderQuickNav />
<TooltipProvider>
<HeaderQuickNav />
</TooltipProvider>
</I18nProvider>,
);
}
Expand Down
9 changes: 7 additions & 2 deletions src/components/layout/HeaderQuickNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Input } from "../ui/input";
import { Button } from "@/components/ui/button";
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Separator } from "@/components/ui/separator";
import { TruncatedText } from "@/components/ui/truncated-text";

const SEARCH_DEBOUNCE_MS = 250;
const MIN_QUERY_LENGTH = 2;
Expand Down Expand Up @@ -406,9 +407,13 @@ export function HeaderQuickNav() {
onMouseDown={(event) => event.preventDefault()}
onClick={() => handleResultSelect(group.entity_type, item)}
>
<span className="truncate text-sm font-medium text-foreground">{item.name}</span>
<TruncatedText className="w-full text-sm font-medium text-foreground">
{item.name}
</TruncatedText>
{item.summary && item.summary !== item.name ? (
<span className="truncate text-xs text-muted-foreground">{item.summary}</span>
<TruncatedText className="w-full text-xs text-muted-foreground">
{item.summary}
</TruncatedText>
) : null}
</Button>
);
Expand Down
28 changes: 16 additions & 12 deletions src/components/prompts/PromptDefinitionTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { truncateMiddle } from "@/components/gateways/utils";
import { TruncatedText } from "@/components/ui/truncated-text";
import { TruncatedMiddleText } from "@/components/ui/truncated-middle-text";

export interface PromptDefinitionTableProps {
prompts: NonNullable<PromptRead>[];
Expand Down Expand Up @@ -67,21 +68,24 @@ export function PromptDefinitionTable({
className="border-0 bg-neutral-50 hover:bg-neutral-50 data-[state=selected]:bg-neutral-50 dark:bg-neutral-800/50 dark:hover:bg-neutral-800/50 dark:data-[state=selected]:bg-neutral-800/50 [&>td:first-child]:rounded-l-lg [&>td:last-child]:rounded-r-lg"
>
<TableCell className="px-4 py-3 text-sm text-foreground">
<button
type="button"
onClick={() => onSelectPrompt(prompt)}
className="block max-w-full truncate rounded-sm text-left transition-colors hover:underline focus-visible:underline focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
title={prompt.displayName || prompt.name}
>
{prompt.displayName || prompt.name}
</button>
<TruncatedText asChild tooltipContent={prompt.displayName || prompt.name}>
<button
type="button"
onClick={() => onSelectPrompt(prompt)}
className="block max-w-full truncate rounded-sm text-left transition-colors hover:underline focus-visible:underline focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
{prompt.displayName || prompt.name}
</button>
</TruncatedText>
</TableCell>

<TableCell className="px-4 py-3">
<div className="group flex min-w-0 items-center">
<span className="min-w-0 truncate font-mono text-xs text-muted-foreground transition-colors group-hover:text-foreground">
{truncateMiddle(prompt.id, 40)}
</span>
<TruncatedMiddleText
value={prompt.id}
maxLength={40}
className="min-w-0 font-mono text-xs text-muted-foreground transition-colors group-hover:text-foreground"
/>
<CopyButton
value={prompt.id}
label={intl.formatMessage(
Expand Down
5 changes: 3 additions & 2 deletions src/components/prompts/PromptDetailsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Button } from "@/components/ui/button";
import { InlineTagAdd } from "@/components/ui/inline-tag-add";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { CopyValue } from "@/components/ui/copy-value";
import { TruncatedText } from "@/components/ui/truncated-text";
import { cn } from "@/lib/utils";
import { getTagDisplay } from "@/components/gateways/utils";
import { formatDateTime } from "@/utils/format";
Expand Down Expand Up @@ -170,9 +171,9 @@ export function PromptDetailsPanel({
<span className="mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-sm bg-prompt-icon-bg text-neutral-950">
<MessageSquareCode className="size-4" />
</span>
<span aria-hidden="true" className="truncate text-xl font-semibold text-foreground">
<TruncatedText aria-hidden="true" className="text-xl font-semibold text-foreground">
{title}
</span>
</TruncatedText>
</div>

{selected && (
Expand Down
24 changes: 20 additions & 4 deletions src/components/resources/ResourcesTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,17 +228,33 @@ describe("ResourcesTable", () => {
expect(rows.length).toBeGreaterThan(1); // Header row + data rows
});

it("truncates a very long resource name to a single line instead of overflowing the table", () => {
it("truncates a very long resource name to a single line instead of overflowing the table", async () => {
Object.defineProperty(HTMLElement.prototype, "scrollWidth", {
configurable: true,
value: 200,
});
Object.defineProperty(HTMLElement.prototype, "clientWidth", {
configurable: true,
value: 100,
});

const user = userEvent.setup();
const longTitle =
"This is a very long resource title that should be truncated to one line, not wrapped or overflowed";
const resources = [createMockResource(1, { title: longTitle })];
render(<ResourcesTable resources={resources} onSelectResource={mockOnSelectResource} />);

// The button's accessible name is computed from its full text content
// regardless of CSS truncation, so `getByRole` still matches on longTitle.
const nameButton = screen.getByRole("button", { name: longTitle });
expect(nameButton).toHaveClass("truncate");
// The full name stays available (e.g. via native tooltip) even though
// it's visually clipped.
expect(nameButton).toHaveAttribute("title", longTitle);

// The full name stays available via a hover tooltip (not a native
// `title`, which doesn't satisfy WCAG 1.4.13) even though it's visually
// clipped.
expect(nameButton).not.toHaveAttribute("title");
await user.hover(nameButton);
expect(await screen.findByRole("tooltip")).toHaveTextContent(longTitle);

// table-fixed + a percentage column width is what actually stops an
// unbreakable long name from forcing the whole table to scroll — a
Expand Down
36 changes: 21 additions & 15 deletions src/components/resources/ResourcesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import {
TableRow,
} from "@/components/ui/table";
import type { ResourceRead } from "@/generated/types";
import { truncateMiddle } from "@/components/gateways/utils";
import { TruncatedText } from "@/components/ui/truncated-text";
import { TruncatedMiddleText } from "@/components/ui/truncated-middle-text";

export function ResourcesTable({
resources,
Expand Down Expand Up @@ -60,21 +61,24 @@ export function ResourcesTable({
className="border-0 bg-neutral-50 hover:bg-neutral-50 data-[state=selected]:bg-neutral-50 dark:bg-neutral-800/50 dark:hover:bg-neutral-800/50 dark:data-[state=selected]:bg-neutral-800/50 [&>td:first-child]:rounded-l-lg [&>td:last-child]:rounded-r-lg"
>
<TableCell className="px-4 py-3 text-sm text-foreground">
<button
type="button"
onClick={() => onSelectResource(resource)}
className="block max-w-full truncate rounded-sm text-left transition-colors hover:underline focus-visible:underline focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
title={resource.title || resource.name}
>
{resource.title || resource.name}
</button>
<TruncatedText asChild tooltipContent={resource.title || resource.name}>
<button
type="button"
onClick={() => onSelectResource(resource)}
className="block max-w-full truncate rounded-sm text-left transition-colors hover:underline focus-visible:underline focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
{resource.title || resource.name}
</button>
</TruncatedText>
</TableCell>

<TableCell className="px-4 py-3">
<div className="group flex min-w-0 items-center">
<span className="min-w-0 truncate font-mono text-xs text-muted-foreground transition-colors group-hover:text-foreground">
{truncateMiddle(resource.uriTemplate || resource.uri, 28)}
</span>
<TruncatedMiddleText
value={resource.uriTemplate || resource.uri}
maxLength={28}
className="min-w-0 font-mono text-xs text-muted-foreground transition-colors group-hover:text-foreground"
/>
<CopyButton
value={resource.uriTemplate || resource.uri}
label={intl.formatMessage(
Expand All @@ -89,9 +93,11 @@ export function ResourcesTable({

<TableCell className="px-4 py-3">
<div className="group flex min-w-0 items-center">
<span className="min-w-0 truncate font-mono text-xs text-muted-foreground transition-colors group-hover:text-foreground">
{truncateMiddle(resource.id, 18)}
</span>
<TruncatedMiddleText
value={resource.id}
maxLength={18}
className="min-w-0 font-mono text-xs text-muted-foreground transition-colors group-hover:text-foreground"
/>
<CopyButton
value={resource.id}
label={intl.formatMessage({ id: "resources.table.copyResourceId" })}
Expand Down
Loading
Loading