Skip to main content
Glama

@arizeai/phoenix-mcp

Official
by Arize-ai
Menu.stories.tsx10 kB
import { useMemo, useState } from "react"; import { Autocomplete, Collection, Header, Input, MenuSection, SubmenuTrigger, useFilter, } from "react-aria-components"; import { Meta } from "@storybook/react"; import { Button, Flex, Icon, IconButton, Icons, Menu, MenuContainer, MenuFooter, MenuHeader, MenuHeaderTitle, MenuItem, MenuTrigger, Popover, SearchField, Separator, Text, Token, View, } from "@phoenix/components"; import { Truncate } from "@phoenix/components/utility/Truncate"; const meta: Meta<typeof Menu> = { title: "Menu", component: Menu, parameters: { layout: "centered", }, }; export default meta; const NESTED_MENU_ITEMS = [ { id: "1", name: "Item 1 Group", children: [ { id: "1.1", name: "Item 1.1", }, ], }, { id: "2", name: "Item 2 Group", }, { id: "3", name: "Item 3 Group", children: [ { id: "3.1", name: "Item 3.1", }, { id: "3.2", name: "Item 3.2", }, { id: "3.3", name: "Item 3.3 Group", children: [ { id: "3.3.1", name: "Item 3.3.1", }, ], }, ], }, ]; export const Template = () => { return ( <MenuTrigger> <Button>Open Menu</Button> <Popover> <Menu> <MenuItem>Item 1</MenuItem> <MenuItem>Item 2</MenuItem> <MenuItem>Item 3</MenuItem> </Menu> </Popover> </MenuTrigger> ); }; Template.args = {}; export const DynamicSearchableMenu = () => { const { contains } = useFilter({ sensitivity: "base" }); return ( <Flex direction="column" gap="size-200"> <Text>View the storybook for comments on implementation</Text> <MenuTrigger> <div> <Button leadingVisual={<Icon svg={<Icons.Search />} />}> Searchable Menu </Button> </div> <Popover> {/* Wrap the menu in an Autocomplete component */} <Autocomplete filter={contains}> {/* Nest a SearchField as child, it will automatically filter the sibling menu items */} <View paddingX="size-100" marginTop="size-100"> <SearchField aria-label="Search" autoFocus> <Input placeholder="Search..." /> </SearchField> </View> {/* Provide the items as a prop to menu, instantiating a react-aria collection */} <Menu items={NESTED_MENU_ITEMS}> {function renderMenuItem({ id, name, children }) { // handle items with children to generate submenus if (children) { return ( <SubmenuTrigger> <MenuItem key={id}>{name}</MenuItem> <Popover> {/* You can nest another Autocomplete here */} <Autocomplete filter={contains}> <View paddingX="size-100" marginTop="size-100"> {/* This will find the nearest autocomplete and menu items */} <SearchField aria-label="Search"> <Input placeholder="Search..." /> </SearchField> </View> <Menu items={children}> {/* recursively call the render fn to render submenu item children */} {(item) => renderMenuItem(item)} </Menu> </Autocomplete> </Popover> </SubmenuTrigger> ); } // handle items with no children return <MenuItem key={id}>{name}</MenuItem>; }} </Menu> </Autocomplete> </Popover> </MenuTrigger> </Flex> ); }; export const SectionedMenu = () => { const lastSection = useMemo(() => { return NESTED_MENU_ITEMS[NESTED_MENU_ITEMS.length - 1]; }, []); return ( <Flex direction="column" gap="size-200"> <Text>View the storybook for comments on implementation</Text> <MenuTrigger> <div> <Button>Menu with Sections</Button> </div> <Popover> <Menu items={NESTED_MENU_ITEMS} renderEmptyState={() => "No Items in Section"} > {/* You could support nested section rendering by naming this render function and calling it recursively. For now, this example just renders sections one level deep */} {(section) => { // do not render empty sections, it is difficult to add an empty state to them // you could possibly add a menu item that links to the place to add data that would populate this section if (!section.children) return <></>; return ( <> <MenuSection> <Header> <Flex justifyContent="space-between" alignItems="center"> <Text weight="heavy">{section.name}</Text> <Text size="S">({section.children.length})</Text> </Flex> </Header> <Collection items={section.children}> {(item) => <MenuItem key={item.id}>{item.name}</MenuItem>} </Collection> </MenuSection> {/* Only render a separator if this is not the last section, otherwise it looks ugly */} {section.name !== lastSection.name && <Separator />} </> ); }} </Menu> </Popover> </MenuTrigger> </Flex> ); }; // Generic sample data for filter menu const FILTER_OPTIONS = [ { id: "1", name: "Category A", color: "#3B82F6" }, { id: "2", name: "Category B", color: "#10B981" }, { id: "3", name: "Category C", color: "#F59E0B" }, { id: "4", name: "Category D", color: "#EF4444" }, { id: "5", name: "Category E", color: "#8B5CF6" }, { id: "6", name: "Category F", color: "#06B6D4" }, { id: "7", name: "Category G", color: "#84CC16" }, { id: "8", name: "Category H", color: "#F97316" }, { id: "9", name: "Category I", color: "#EC4899" }, { id: "10", name: "Category JJJJJJJJJJJJJJJJJJJJJJ", color: "#6B7280" }, { id: "11", name: "Category K", color: "#6B7280" }, { id: "12", name: "Category L", color: "#6B7280" }, { id: "13", name: "Category M", color: "#6B7280" }, { id: "14", name: "Category N", color: "#6B7280" }, { id: "15", name: "Category O", color: "#6B7280" }, { id: "16", name: "Category P", color: "#6B7280" }, { id: "17", name: "Category Q", color: "#6B7280" }, { id: "18", name: "Category R", color: "#6B7280" }, { id: "19", name: "Category S", color: "#6B7280" }, { id: "20", name: "Category T", color: "#6B7280" }, { id: "21", name: "Category U", color: "#6B7280" }, { id: "22", name: "Category V", color: "#6B7280" }, { id: "23", name: "Category W", color: "#6B7280" }, ]; export const FilterMenu = () => { const { contains } = useFilter({ sensitivity: "base" }); const [selectedIds, setSelectedIds] = useState<string[]>([]); return ( <Flex direction="column" gap="size-200"> <Text> Generic filter menu with search and multi-select functionality </Text> <MenuTrigger> <Button leadingVisual={<Icon svg={<Icons.PriceTagsOutline />} />}> Filter Categories {selectedIds.length > 0 ? ` (${selectedIds.length})` : ""} </Button> <MenuContainer> <Autocomplete filter={contains}> <MenuHeader> <MenuHeaderTitle leadingContent={ <IconButton size="S"> <Icon svg={<Icons.ChevronLeft />} /> </IconButton> } trailingContent={ <IconButton size="S"> <Icon svg={<Icons.PlusCircleOutline />} /> </IconButton> } > Filter by Categories </MenuHeaderTitle> <SearchField aria-label="Search categories" autoFocus> <Input placeholder="Search categories..." /> </SearchField> </MenuHeader> <Menu items={FILTER_OPTIONS} selectionMode="multiple" renderEmptyState={() => "No categories found"} selectedKeys={selectedIds} onSelectionChange={(keys) => { if (keys === "all") { setSelectedIds(FILTER_OPTIONS.map((item) => item.id)); } else { setSelectedIds(Array.from(keys as Set<string>)); } }} > {({ id, name, color }) => ( <MenuItem id={id} textValue={name}> <Token color={color}> <Truncate maxWidth={80}>{name}</Truncate> </Token> </MenuItem> )} </Menu> </Autocomplete> <MenuFooter> <Button>Apply</Button> </MenuFooter> </MenuContainer> </MenuTrigger> {selectedIds.length > 0 && ( <Flex direction="column" gap="size-100"> <Text size="S" weight="heavy"> Selected Categories: </Text> <Flex wrap gap="size-100"> {selectedIds.map((id) => { const option = FILTER_OPTIONS.find((opt) => opt.id === id); return option ? ( <Token key={id} color={option.color}> {option.name} </Token> ) : null; })} </Flex> </Flex> )} </Flex> ); };

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Arize-ai/phoenix'

If you have feedback or need assistance with the MCP directory API, please join our Discord server