diff --git a/mcpjam-inspector/client/src/components/mcp-sidebar.tsx b/mcpjam-inspector/client/src/components/mcp-sidebar.tsx index f5cac3f40..b7f71fcb8 100644 --- a/mcpjam-inspector/client/src/components/mcp-sidebar.tsx +++ b/mcpjam-inspector/client/src/components/mcp-sidebar.tsx @@ -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, @@ -201,7 +202,6 @@ const navigationSections: NavSection[] = [ icon: FlaskConical, billingFeature: "evals", evalsSubnav: true, - featureFlag: "evals-enabled", }, ], }, @@ -364,6 +364,7 @@ type EvalsSubnavItem = { icon: typeof Puzzle | typeof GitBranch; isActive: (activeTab?: string) => boolean; onClick: () => void; + beta?: boolean; }; export function getEvalsSubnavItems(options: { @@ -376,6 +377,7 @@ export function getEvalsSubnavItems(options: { icon: Puzzle, isActive: (activeTab) => activeTab === "evals", onClick: navigateToEvalsExploreList, + beta: true, }, ]; @@ -399,6 +401,8 @@ export function SidebarEvalsNavGroup({ disabledTooltip, activeTab, showRuns = true, + showPlaygroundBeta = false, + playgroundEnabled = false, }: { title: string; Icon: React.ComponentType<{ className?: string }>; @@ -406,8 +410,11 @@ export function SidebarEvalsNavGroup({ 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, }); @@ -415,15 +422,15 @@ export function SidebarEvalsNavGroup({ const parentButton = ( { - 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" @@ -462,25 +469,67 @@ export function SidebarEvalsNavGroup({ {subnavItems.map((item) => { const ItemIcon = item.icon; + const isItemPlaygroundLocked = item.beta && isPlaygroundLocked; + const isItemDisabled = disabled || isItemPlaygroundLocked; + + const subnavButton = ( + { + 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", + )} + > + + {item.title} + {item.beta && showPlaygroundBeta ? ( + + + { + e.preventDefault(); + e.stopPropagation(); + }} + > + Beta + + + + This tab is a work in progress, data may not be + persisted. + + + ) : null} + + ); return ( - { - e.preventDefault(); - if (disabled) return; - item.onClick(); - }} - aria-disabled={disabled || undefined} - className={ - disabled ? "pointer-events-none opacity-50" : undefined - } - > - - {item.title} - + {isItemPlaygroundLocked ? ( + + {subnavButton} + + Coming soon. Playground is in beta. + + + ) : ( + subnavButton + )} ); })} @@ -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"); @@ -561,7 +610,6 @@ export function MCPSidebar({ "mcpjam-learning": !!learningEnabled, "sandboxes-enabled": !!sandboxesEnabled && isAuthenticated, "registry-enabled": registryEnabled === true, - "evals-enabled": !!evalsEnabled, "mcpjam-conformance": conformanceEnabled === true, xaa: xaaEnabled === true, }), @@ -569,7 +617,6 @@ export function MCPSidebar({ learningEnabled, sandboxesEnabled, registryEnabled, - evalsEnabled, conformanceEnabled, xaaEnabled, isAuthenticated, @@ -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) */} diff --git a/mcpjam-inspector/client/src/components/sidebar/__tests__/sidebar-invite-cta.test.tsx b/mcpjam-inspector/client/src/components/sidebar/__tests__/sidebar-invite-cta.test.tsx index ace2d51d2..1c6c9a15b 100644 --- a/mcpjam-inspector/client/src/components/sidebar/__tests__/sidebar-invite-cta.test.tsx +++ b/mcpjam-inspector/client/src/components/sidebar/__tests__/sidebar-invite-cta.test.tsx @@ -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 = {}; vi.mock("convex/react", () => ({ useConvexAuth: (...args: unknown[]) => mockUseConvexAuth(...args), @@ -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", () => ({ @@ -85,8 +86,12 @@ vi.mock("@/components/ui/sidebar", () => ({ SidebarMenuButton: ({ children, tooltip: _tooltip, + isActive: _isActive, ...props - }: ButtonHTMLAttributes & { tooltip?: string }) => ( + }: ButtonHTMLAttributes & { + isActive?: boolean; + tooltip?: string; + }) => ( @@ -96,8 +101,9 @@ vi.mock("@/components/ui/sidebar", () => ({ ), SidebarMenuSubButton: ({ children, + isActive: _isActive, ...props - }: ButtonHTMLAttributes) => ( + }: ButtonHTMLAttributes & { isActive?: boolean }) => ( @@ -153,6 +159,9 @@ function renderSidebar(overrides: Partial { beforeEach(() => { vi.clearAllMocks(); + Object.keys(mockFeatureFlags).forEach((flag) => { + delete mockFeatureFlags[flag]; + }); mockUseConvexAuth.mockReturnValue({ isAuthenticated: true, isLoading: false, @@ -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(); + }); });