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
97 changes: 73 additions & 24 deletions mcpjam-inspector/client/src/components/mcp-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { SidebarUser } from "@/components/sidebar/sidebar-user";
import { SidebarWorkspaceSelector } from "@/components/sidebar/sidebar-workspace-selector";
import { ShareWorkspaceDialog } from "@/components/workspace/ShareWorkspaceDialog";
import { useUpdateNotification } from "@/hooks/useUpdateNotification";
import { Badge } from "@mcpjam/design-system/badge";
import { Button } from "@mcpjam/design-system/button";
import {
Tooltip,
Expand Down Expand Up @@ -201,7 +202,6 @@ const navigationSections: NavSection[] = [
icon: FlaskConical,
billingFeature: "evals",
evalsSubnav: true,
featureFlag: "evals-enabled",
},
],
},
Expand Down Expand Up @@ -364,6 +364,7 @@ type EvalsSubnavItem = {
icon: typeof Puzzle | typeof GitBranch;
isActive: (activeTab?: string) => boolean;
onClick: () => void;
beta?: boolean;
};

export function getEvalsSubnavItems(options: {
Expand All @@ -376,6 +377,7 @@ export function getEvalsSubnavItems(options: {
icon: Puzzle,
isActive: (activeTab) => activeTab === "evals",
onClick: navigateToEvalsExploreList,
beta: true,
},
];

Expand All @@ -399,31 +401,36 @@ export function SidebarEvalsNavGroup({
disabledTooltip,
activeTab,
showRuns = true,
showPlaygroundBeta = false,
playgroundEnabled = false,
}: {
title: string;
Icon: React.ComponentType<{ className?: string }>;
disabled?: boolean;
disabledTooltip?: string;
activeTab?: string;
showRuns?: boolean;
showPlaygroundBeta?: boolean;
playgroundEnabled?: boolean;
}) {
const isEvalsFamily = activeTab === "evals" || activeTab === "ci-evals";
const isPlaygroundLocked = !playgroundEnabled;
const subnavItems = getEvalsSubnavItems({
evaluateRunsEnabled: showRuns,
});

const parentButton = (
<SidebarMenuButton
tooltip={title}
isActive={!disabled && isEvalsFamily}
isActive={!disabled && !isPlaygroundLocked && isEvalsFamily}
onClick={() => {
if (disabled) return;
if (disabled || isPlaygroundLocked) return;
navigateToEvalsExploreList();
}}
aria-disabled={disabled || undefined}
aria-disabled={disabled || isPlaygroundLocked || undefined}
tabIndex={disabled ? -1 : undefined}
className={
disabled
disabled || isPlaygroundLocked
? "cursor-not-allowed text-muted-foreground opacity-50 hover:bg-transparent hover:text-muted-foreground active:bg-transparent active:text-muted-foreground"
: isEvalsFamily
? "[&[data-active=true]]:bg-accent cursor-pointer"
Expand Down Expand Up @@ -462,25 +469,67 @@ export function SidebarEvalsNavGroup({
<SidebarMenuSub>
{subnavItems.map((item) => {
const ItemIcon = item.icon;
const isItemPlaygroundLocked = item.beta && isPlaygroundLocked;
const isItemDisabled = disabled || isItemPlaygroundLocked;

const subnavButton = (
<SidebarMenuSubButton
isActive={!isItemDisabled && item.isActive(activeTab)}
href={item.href}
onClick={(e) => {
e.preventDefault();
if (isItemDisabled) return;
item.onClick();
}}
aria-disabled={isItemDisabled || undefined}
className={cn(
isItemDisabled &&
"cursor-not-allowed text-muted-foreground opacity-50 hover:bg-transparent hover:text-muted-foreground active:bg-transparent active:text-muted-foreground",
isItemPlaygroundLocked &&
"aria-disabled:pointer-events-auto",
disabled && "pointer-events-none",
)}
>
<ItemIcon className="h-4 w-4" />
<span className="min-w-0 truncate">{item.title}</span>
{item.beta && showPlaygroundBeta ? (
<Tooltip>
<TooltipTrigger asChild>
<Badge
className="ml-auto rounded-full px-1.5 py-0 text-[10px] font-semibold leading-tight uppercase tracking-wider"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
Beta
</Badge>
</TooltipTrigger>
<TooltipContent
side="right"
sideOffset={6}
className="max-w-[200px]"
>
This tab is a work in progress, data may not be
persisted.
</TooltipContent>
</Tooltip>
) : null}
</SidebarMenuSubButton>
);

return (
<SidebarMenuSubItem key={item.title}>
<SidebarMenuSubButton
isActive={item.isActive(activeTab)}
href={item.href}
onClick={(e) => {
e.preventDefault();
if (disabled) return;
item.onClick();
}}
aria-disabled={disabled || undefined}
className={
disabled ? "pointer-events-none opacity-50" : undefined
}
>
<ItemIcon className="h-4 w-4" />
<span>{item.title}</span>
</SidebarMenuSubButton>
{isItemPlaygroundLocked ? (
<Tooltip>
<TooltipTrigger asChild>{subnavButton}</TooltipTrigger>
<TooltipContent side="right" sideOffset={6}>
Coming soon. Playground is in beta.
</TooltipContent>
</Tooltip>
) : (
subnavButton
)}
</SidebarMenuSubItem>
);
})}
Expand Down Expand Up @@ -516,8 +565,8 @@ export function MCPSidebar({
const learningFlagEnabled = useFeatureFlagEnabled("mcpjam-learning");
const sandboxesEnabled = useFeatureFlagEnabled("sandboxes-enabled");
const registryEnabled = useFeatureFlagEnabled("registry-enabled");
const evalsEnabled = useFeatureFlagEnabled("evals-enabled");
const evaluateRunsEnabled = useFeatureFlagEnabled("evaluate-runs");
const playgroundEnabled = useFeatureFlagEnabled("playground-enabled");
const xaaEnabled = useFeatureFlagEnabled("xaa");
const learnMoreEnabled = useFeatureFlagEnabled("learn-more-enabled");
const conformanceEnabled = useFeatureFlagEnabled("mcpjam-conformance");
Expand Down Expand Up @@ -561,15 +610,13 @@ export function MCPSidebar({
"mcpjam-learning": !!learningEnabled,
"sandboxes-enabled": !!sandboxesEnabled && isAuthenticated,
"registry-enabled": registryEnabled === true,
"evals-enabled": !!evalsEnabled,
"mcpjam-conformance": conformanceEnabled === true,
xaa: xaaEnabled === true,
}),
[
learningEnabled,
sandboxesEnabled,
registryEnabled,
evalsEnabled,
conformanceEnabled,
xaaEnabled,
isAuthenticated,
Expand Down Expand Up @@ -704,6 +751,8 @@ export function MCPSidebar({
disabledTooltip={evalsEntry.disabledTooltip}
activeTab={activeTab}
showRuns={evaluateRunsEnabled === true}
showPlaygroundBeta={playgroundEnabled === true}
playgroundEnabled={playgroundEnabled === true}
/>
) : null}
{/* Add subtle divider between sections (except after the last section) */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { MCPSidebar } from "@/components/mcp-sidebar";
const mockUseConvexAuth = vi.fn();
const mockUseAuth = vi.fn();
const mockShareWorkspaceDialog = vi.fn();
const mockFeatureFlags: Record<string, boolean | undefined> = {};

vi.mock("convex/react", () => ({
useConvexAuth: (...args: unknown[]) => mockUseConvexAuth(...args),
Expand All @@ -19,7 +20,7 @@ vi.mock("posthog-js/react", () => ({
usePostHog: () => ({
capture: vi.fn(),
}),
useFeatureFlagEnabled: () => false,
useFeatureFlagEnabled: (flag: string) => mockFeatureFlags[flag] ?? false,
}));

vi.mock("@/stores/preferences/preferences-provider", () => ({
Expand Down Expand Up @@ -85,8 +86,12 @@ vi.mock("@/components/ui/sidebar", () => ({
SidebarMenuButton: ({
children,
tooltip: _tooltip,
isActive: _isActive,
...props
}: ButtonHTMLAttributes<HTMLButtonElement> & { tooltip?: string }) => (
}: ButtonHTMLAttributes<HTMLButtonElement> & {
isActive?: boolean;
tooltip?: string;
}) => (
<button type="button" {...props}>
{children}
</button>
Expand All @@ -96,8 +101,9 @@ vi.mock("@/components/ui/sidebar", () => ({
),
SidebarMenuSubButton: ({
children,
isActive: _isActive,
...props
}: ButtonHTMLAttributes<HTMLButtonElement>) => (
}: ButtonHTMLAttributes<HTMLButtonElement> & { isActive?: boolean }) => (
<button type="button" {...props}>
{children}
</button>
Expand Down Expand Up @@ -153,6 +159,9 @@ function renderSidebar(overrides: Partial<React.ComponentProps<typeof MCPSidebar
describe("sidebar invite CTA", () => {
beforeEach(() => {
vi.clearAllMocks();
Object.keys(mockFeatureFlags).forEach((flag) => {
delete mockFeatureFlags[flag];
});
mockUseConvexAuth.mockReturnValue({
isAuthenticated: true,
isLoading: false,
Expand Down Expand Up @@ -232,4 +241,48 @@ describe("sidebar invite CTA", () => {
screen.getByRole("button", { name: "Invite team members" }),
).toBeInTheDocument();
});

it("shows disabled Playground with a beta tooltip when the flag is off", () => {
mockFeatureFlags["playground-enabled"] = false;
window.location.hash = "#servers";

renderSidebar();

expect(screen.getByRole("button", { name: "Evaluate" })).toHaveAttribute(
"aria-disabled",
"true",
);
const playground = screen.getByRole("button", { name: "Playground" });
expect(playground).toHaveAttribute("aria-disabled", "true");
expect(playground).toHaveClass("cursor-not-allowed");
expect(
screen.getByText("Coming soon. Playground is in beta."),
).toBeInTheDocument();
expect(screen.queryByText("Beta")).not.toBeInTheDocument();

fireEvent.click(playground);

expect(window.location.hash).toBe("#servers");
});

it("enables Playground and shows the beta badge when the flag is on", () => {
mockFeatureFlags["playground-enabled"] = true;

renderSidebar();

const playground = screen
.getByText("Playground")
.closest("button") as HTMLButtonElement;
expect(playground).toBeInTheDocument();
expect(playground).not.toHaveAttribute("aria-disabled");
expect(screen.getByText("Beta")).toBeInTheDocument();
expect(
screen.getByText(
"This tab is a work in progress, data may not be persisted.",
),
).toBeInTheDocument();
expect(
screen.queryByText("Coming soon. Playground is in beta."),
).not.toBeInTheDocument();
});
});
Loading