Skip to main content
Glama

@arizeai/phoenix-mcp

Official
by Arize-ai
TraceTree.tsx16.4 kB
import { PropsWithChildren, startTransition, useEffect, useRef, useState, } from "react"; import { css } from "@emotion/react"; import { classNames } from "@arizeai/components"; import { Button, Flex, Heading, Icon, Icons, Tooltip, TooltipArrow, TooltipTrigger, } from "@phoenix/components"; import { TimelineBar, TimelineBarProps, } from "@phoenix/components/timeline/TimelineBar"; import { SpanTokenCount } from "@phoenix/components/trace/SpanTokenCount"; import { useSpanKindColor } from "@phoenix/components/trace/useSpanKindColor"; import { usePreferencesContext } from "@phoenix/contexts/PreferencesContext"; import { LatencyText } from "./LatencyText"; import { SpanKindIcon } from "./SpanKindIcon"; import { SpanStatusCodeIcon } from "./SpanStatusCodeIcon"; import { TraceTreeProvider, useTraceTree } from "./TraceTreeContext"; import { ISpanItem, SpanStatusCodeType } from "./types"; import { createSpanTree, SpanTreeNode } from "./utils"; type TraceTreeProps = { spans: ISpanItem[]; onSpanClick?: (span: ISpanItem) => void; selectedSpanNodeId: string; }; /** * The amount of padding to add to the left of the span item for each level of nesting. */ const NESTING_INDENT = 25; /** * The breakpoint at which the trace tree switches to compact mode. */ const COMPACT_BREAKPOINT = "300px"; /** * The breakpoint at which to show the timing bars */ const LARGE_BREAKPOINT = "500px"; /** * The breakpoint at which the tree gets considered large */ const EXTRA_LARGE_BREAKPOINT = "800px"; export function TraceTree(props: TraceTreeProps) { const { spans, onSpanClick, selectedSpanNodeId } = props; const spanTree = createSpanTree(spans); const rootSpan = spanTree[0].span; const overallTimeRange = { start: new Date(rootSpan.startTime), end: rootSpan.endTime ? new Date(rootSpan.endTime) : new Date(), }; return ( <TraceTreeProvider> <div css={css` display: flex; flex-direction: column; overflow: hidden; height: 100%; align-items: stretch; container-type: inline-size; `} > <TraceTreeToolbar /> <ul css={css` flex: 1 1 auto; display: flex; flex-direction: column; width: 100%; overflow: auto; --trace-tree-nesting-indent: ${NESTING_INDENT}px; @container (width < ${COMPACT_BREAKPOINT}) { --trace-tree-nesting-indent: 0; // Hide the collapse button .span-controls, .latency-text, .token-count-item, .span-tree-edge-connector, .span-tree-edge, .span-tree-timing { display: none; visibility: hidden; width: 0; } .span-node-wrap { padding-left: var(--ac-global-dimension-static-size-200); } } @container (width < ${LARGE_BREAKPOINT}) { .span-tree-timing { display: none; visibility: hidden; width: 0; } } @container (width > ${EXTRA_LARGE_BREAKPOINT}) { .span-tree-timing { width: 33%; } } `} data-testid="trace-tree" > {spanTree.map((spanNode) => ( <SpanTreeItem key={spanNode.span.id} node={spanNode} overallTimeRange={overallTimeRange} onSpanClick={onSpanClick} selectedSpanNodeId={selectedSpanNodeId} /> ))} </ul> </div> </TraceTreeProvider> ); } function TraceTreeToolbar() { const showMetricsInTraceTree = usePreferencesContext( (state) => state.showMetricsInTraceTree ); const setShowMetricsInTraceTree = usePreferencesContext( (state) => state.setShowMetricsInTraceTree ); const { isCollapsed, setIsCollapsed } = useTraceTree(); return ( <div className="trace-tree-toolbar" css={css` display: flex; flex-direction: row; justify-content: space-between; box-sizing: border-box; width: 100%; align-items: center; padding: var(--ac-global-dimension-size-100); border-bottom: 1px solid var(--ac-global-color-grey-300); height: var(--ac-global-dimension-size-600); @container (width < ${COMPACT_BREAKPOINT}) { button { display: none; } } `} > <Flex direction="row" justifyContent="space-between" alignItems="center" flex="none" gap="size-100" width="100%" > <Heading level={3}>Trace</Heading> <Flex direction="row" gap="size-100" className="trace-tree-controls"> <TooltipTrigger> <Button variant="default" size="S" aria-label={isCollapsed ? "Expand all" : "Collapse all"} onPress={() => { setIsCollapsed(!isCollapsed); }} leadingVisual={ <Icon svg={ isCollapsed ? ( <Icons.RowCollapseOutline /> ) : ( <Icons.RowExpandOutline /> ) } /> } /> <Tooltip offset={-5}> <TooltipArrow /> {isCollapsed ? "Expand all nested spans" : "Collapse all nested spans"} </Tooltip> </TooltipTrigger> <TooltipTrigger> <Button size="S" aria-label={ showMetricsInTraceTree ? "Hide metrics in trace tree" : "Show metrics in trace tree" } onPress={() => { setShowMetricsInTraceTree(!showMetricsInTraceTree); }} leadingVisual={ <Icon svg={ showMetricsInTraceTree ? ( <Icons.TimerOutline /> ) : ( <Icons.TimerOffOutline /> ) } /> } /> <Tooltip offset={-5}> <TooltipArrow /> {showMetricsInTraceTree ? "Hide metrics in trace tree" : "Show metrics in trace tree"} </Tooltip> </TooltipTrigger> </Flex> </Flex> </div> ); } const spanNameCSS = css` font-weight: 500; color: var(--ac-global-text-color-900); display: inline-block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; `; interface SpanTreeItemProps<TSpan extends ISpanItem> { node: SpanTreeNode<TSpan>; selectedSpanNodeId: string; overallTimeRange: TimeRange; onSpanClick?: (span: ISpanItem) => void; /** * How deep the item is nested in the tree. Starts at 0. * @default 0 */ nestingLevel?: number; } function SpanTreeItem<TSpan extends ISpanItem>( props: SpanTreeItemProps<TSpan> ) { const { node, selectedSpanNodeId, onSpanClick, nestingLevel = 0, overallTimeRange, } = props; const childNodes = node.children; const [isCollapsed, setIsCollapsed] = useState(false); const { isCollapsed: treeIsCollapsed } = useTraceTree(); const hasChildren = childNodes.length > 0; const showMetricsInTraceTree = usePreferencesContext( (state) => state.showMetricsInTraceTree ); const isSelected = selectedSpanNodeId === node.span.id; const itemRef = useRef<HTMLDivElement>(null); // Scroll into view when selected useEffect(() => { if (isSelected && itemRef.current) { itemRef.current.scrollIntoView({ behavior: "smooth", block: "nearest", }); } }, [isSelected]); // React to global changes to the trace tree state and change local state useEffect(() => { setIsCollapsed(treeIsCollapsed); }, [treeIsCollapsed]); const { name, latencyMs, statusCode, tokenCountTotal } = node.span; return ( <div ref={itemRef}> <button className="button--reset" css={css` width: 100%; overflow: hidden; cursor: pointer; `} onClick={() => { startTransition(() => { onSpanClick && onSpanClick(node.span); }); }} > <SpanNodeWrap isSelected={selectedSpanNodeId === node.span.id} nestingLevel={nestingLevel} > <Flex direction="row" gap="size-100" justifyContent="start" alignItems="center" flex="1 1 auto" minWidth={0} css={css` overflow: hidden; `} > <SpanKindIcon spanKind={node.span.spanKind} /> <span css={spanNameCSS} title={name}> {name} </span> {statusCode === "ERROR" ? ( <SpanStatusCodeIcon statusCode="ERROR" css={css` font-size: var(--ac-global-font-size-m); `} /> ) : null} {typeof tokenCountTotal === "number" && tokenCountTotal > 0 && showMetricsInTraceTree ? ( <SpanTokenCount tokenCountTotal={tokenCountTotal} nodeId={node.span.id} /> ) : null} </Flex> {showMetricsInTraceTree ? ( <div css={spanTimingCSS} className="span-tree-timing"> {latencyMs != null ? ( <LatencyText latencyMs={latencyMs} showIcon={false} size="XS" /> ) : null} <SpanTimelineBar spanKind={node.span.spanKind} overallTimeRange={overallTimeRange} spanTimeRange={{ start: new Date(node.span.startTime), end: node.span.endTime ? new Date(node.span.endTime) : new Date(), // Assume un-closed }} /> </div> ) : null} <div css={spanControlsCSS} data-testid="span-controls" className="span-controls" > {hasChildren ? ( <CollapseToggleButton isCollapsed={isCollapsed} onClick={() => { setIsCollapsed(!isCollapsed); }} /> ) : null} </div> </SpanNodeWrap> </button> {childNodes.length ? ( <ul css={css` display: ${isCollapsed ? "none" : "flex"}; flex-direction: column; `} > {childNodes.map((leafNode, index) => { // The last child does not need an edge connector, a line to connect the nodes // after to the parent node const nexSibling = childNodes[index + 1]; return ( <li key={leafNode.span.spanId} css={css` position: relative; `} > {nexSibling ? ( <SpanTreeEdgeConnector {...nexSibling.span} nestingLevel={nestingLevel} /> ) : null} <SpanTreeEdge {...leafNode.span} nestingLevel={nestingLevel} /> <SpanTreeItem node={leafNode} overallTimeRange={overallTimeRange} onSpanClick={onSpanClick} selectedSpanNodeId={selectedSpanNodeId} nestingLevel={nestingLevel + 1} /> </li> ); })} </ul> ) : null} </div> ); } function SpanNodeWrap( props: PropsWithChildren<{ isSelected: boolean; nestingLevel: number }> ) { return ( <div className={classNames("span-node-wrap", { "is-selected": props.isSelected, })} css={css` width: 100%; display: flex; flex-direction: row; justify-content: space-between; gap: var(--ac-global-dimension-static-size-100); padding-right: var(--ac-global-dimension-static-size-100); padding-top: var(--ac-global-dimension-static-size-100); padding-bottom: var(--ac-global-dimension-static-size-100); border-left: 4px solid transparent; box-sizing: border-box; &:hover { background-color: var(--ac-global-color-grey-200); } &.is-selected { background-color: var(--ac-global-color-primary-100); border-color: var(--ac-global-color-primary-200); } & > *:first-of-type { margin-left: calc( (${props.nestingLevel} * var(--trace-tree-nesting-indent)) + 16px ); } `} > {props.children} </div> ); } /** * The line that connects the parent node to the child node edge */ function SpanTreeEdgeConnector({ statusCode, nestingLevel, }: { statusCode: SpanStatusCodeType; nestingLevel: number; }) { const isError = statusCode === "ERROR"; return ( <div aria-hidden="true" data-testid="span-tree-edge-connector" className="span-tree-edge-connector" data-status-code={statusCode} css={css` position: absolute; border-left: 1px solid ${isError ? "var(--ac-global-color-danger)" : "var(--ac-global-color-grey-500)"}; z-index: ${isError ? 1 : 0}; top: 0; left: ${nestingLevel * NESTING_INDENT + 29}px; width: 42px; bottom: 0; z-index: 1; `} ></div> ); } function SpanTreeEdge({ nestingLevel, statusCode, }: { statusCode: SpanStatusCodeType; nestingLevel: number; }) { const isError = statusCode === "ERROR"; const color = isError ? "var(--ac-global-color-danger)" : "var(--ac-global-color-grey-500)"; const zIndex = isError ? 1 : 0; return ( <div aria-hidden="true" className="span-tree-edge" css={css` position: absolute; border-left: 1px solid ${color}; border-bottom: 1px solid ${color}; z-index: ${zIndex}; border-radius: 0 0 0 11px; top: -5px; left: ${nestingLevel * NESTING_INDENT + 29}px; width: 11px; height: 22px; `} ></div> ); } const spanControlsCSS = css` width: 20px; flex: none; `; const spanTimingCSS = css` gap: var(--ac-global-dimension-static-size-100); width: 150px; transition: all 0.2s ease-in-out; flex: none; display: flex; flex-direction: row; align-items: center; .latency-text { justify-content: end !important; min-width: 2.5rem; float: right; } `; const collapseButtonCSS = css` display: flex; align-items: center; justify-content: center; width: 20px; height: 20px; border: none; background: none; cursor: pointer; color: var(--ac-global-text-color-900); border-radius: 4px; transition: transform 0.2s; transition: background-color 0.5s; flex: none; background-color: rgba(0, 0, 0, 0.1); &:hover { background-color: rgba(0, 0, 0, 0.3); } &.is-collapsed { transform: rotate(-90deg); } `; function CollapseToggleButton({ isCollapsed, onClick, }: { isCollapsed: boolean; onClick: () => void; }) { return ( <button onClick={(e) => { e.stopPropagation(); e.preventDefault(); onClick(); }} className={classNames("button--reset collapse-toggle-button", { "is-collapsed": isCollapsed, })} css={collapseButtonCSS} > <Icon svg={<Icons.ArrowIosDownwardOutline />} /> </button> ); } function SpanTimelineBar({ spanKind, ...props }: Omit<TimelineBarProps, "color"> & { spanKind: string }) { const color = useSpanKindColor({ spanKind }); return <TimelineBar color={color} {...props} />; }

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