From 6a62bd2a9050a31b9d0eeee49366fa8fe215ea4f Mon Sep 17 00:00:00 2001 From: Xander Song Date: Tue, 3 Dec 2024 13:21:52 -0800 Subject: [PATCH] fix: add copy to clipboard icon for experiment ids on experiment compare page (#5596) * fix: add copy to clipboard icon for experiment ids on experiment compare page * refactor to re-use experiment dropdown * more minimal style --------- Co-authored-by: Mikyo King --- .../experiment/ExperimentActionMenu.tsx | 126 ++++++++++++++++++ app/src/components/table/styles.ts | 6 + .../experiment/ExperimentCompareTable.tsx | 40 +++++- .../ExperimentCompareTableQuery.graphql.ts | 31 ++++- .../pages/experiments/ExperimentsTable.tsx | 124 +---------------- 5 files changed, 196 insertions(+), 131 deletions(-) create mode 100644 app/src/components/experiment/ExperimentActionMenu.tsx diff --git a/app/src/components/experiment/ExperimentActionMenu.tsx b/app/src/components/experiment/ExperimentActionMenu.tsx new file mode 100644 index 0000000000..0baae7b9c2 --- /dev/null +++ b/app/src/components/experiment/ExperimentActionMenu.tsx @@ -0,0 +1,126 @@ +import React, { ReactNode, useState } from "react"; +import { useNavigate } from "react-router"; +import copy from "copy-to-clipboard"; + +import { + ActionMenu, + ActionMenuProps, + Dialog, + DialogContainer, + Flex, + Icon, + Icons, + Item, + Text, +} from "@arizeai/components"; + +import { JSONBlock } from "@phoenix/components/code"; +import { useNotifySuccess } from "@phoenix/contexts"; +import { assertUnreachable } from "@phoenix/typeUtils"; + +export enum ExperimentAction { + GO_TO_EXPERIMENT_RUN_TRACES = "GO_TO_EXPERIMENT_RUN_TRACES", + COPY_EXPERIMENT_ID = "COPY_EXPERIMENT_ID", + VIEW_METADATA = "VIEW_METADATA", +} + +export function ExperimentActionMenu(props: { + projectId?: string | null; + experimentId: string; + metadata: unknown; + isQuiet?: ActionMenuProps["isQuiet"]; +}) { + const { projectId, isQuiet = false } = props; + const navigate = useNavigate(); + const [dialog, setDialog] = useState(null); + const notifySuccess = useNotifySuccess(); + return ( +
{ + // prevent parent anchor link from being followed + e.preventDefault(); + e.stopPropagation(); + }} + > + { + const action = firedAction as ExperimentAction; + switch (action) { + case ExperimentAction.GO_TO_EXPERIMENT_RUN_TRACES: { + return navigate(`/projects/${projectId}`); + } + case ExperimentAction.VIEW_METADATA: { + setDialog( + setDialog(null)}> + + + ); + break; + } + case ExperimentAction.COPY_EXPERIMENT_ID: { + copy(props.experimentId); + notifySuccess({ + title: "Copied", + message: "The experiment ID has been copied to your clipboard", + }); + break; + } + default: { + assertUnreachable(action); + } + } + }} + > + + + } /> + View run traces + + + + + } /> + View metadata + + + + + } /> + Copy experiment ID + + + + { + setDialog(null); + }} + > + {dialog} + +
+ ); +} diff --git a/app/src/components/table/styles.ts b/app/src/components/table/styles.ts index 111cdce27f..0785c20225 100644 --- a/app/src/components/table/styles.ts +++ b/app/src/components/table/styles.ts @@ -50,6 +50,12 @@ export const tableCSS = (theme: Theme) => css` background: var(--ac-global-color-primary); } } + // Style action menu buttons in the header + .ac-button[data-size="compact"][data-childless="true"] { + padding: 0; + border: none; + background-color: transparent; + } } } } diff --git a/app/src/pages/experiment/ExperimentCompareTable.tsx b/app/src/pages/experiment/ExperimentCompareTable.tsx index bd7c51436c..b1108fca4c 100644 --- a/app/src/pages/experiment/ExperimentCompareTable.tsx +++ b/app/src/pages/experiment/ExperimentCompareTable.tsx @@ -43,6 +43,7 @@ import { } from "@phoenix/components/annotation"; import { JSONBlock } from "@phoenix/components/code"; import { JSONText } from "@phoenix/components/code/JSONText"; +import { ExperimentActionMenu } from "@phoenix/components/experiment/ExperimentActionMenu"; import { SequenceNumberLabel } from "@phoenix/components/experiment/SequenceNumberLabel"; import { resizeHandleCSS } from "@phoenix/components/resize"; import { @@ -73,7 +74,13 @@ type ExampleCompareTableProps = { type ExperimentInfoMap = Record< string, - { name: string; sequenceNumber: number } | undefined + | { + name: string; + sequenceNumber: number; + metadata: object; + projectId: string | null; + } + | undefined >; type TableRow = ExperimentCompareTableQuery$data["comparisons"][number] & { @@ -174,6 +181,10 @@ export function ExperimentCompareTable(props: ExampleCompareTableProps) { id name sequenceNumber + metadata + project { + id + } } } } @@ -189,7 +200,10 @@ export function ExperimentCompareTable(props: ExampleCompareTableProps) { const experimentInfoById = useMemo(() => { return ( data.dataset?.experiments?.edges.reduce((acc, edge) => { - acc[edge.experiment.id] = { ...edge.experiment }; + acc[edge.experiment.id] = { + ...edge.experiment, + projectId: edge.experiment?.project?.id || null, + }; return acc; }, {} as ExperimentInfoMap) || {} ); @@ -278,12 +292,28 @@ export function ExperimentCompareTable(props: ExampleCompareTableProps) { return experimentIds.map((experimentId) => ({ header: () => { const name = experimentInfoById[experimentId]?.name; + const metadata = experimentInfoById[experimentId]?.metadata; + const projectId = experimentInfoById[experimentId]?.projectId; const sequenceNumber = experimentInfoById[experimentId]?.sequenceNumber || 0; return ( - - - {name} + + + + {name} + + ); }, diff --git a/app/src/pages/experiment/__generated__/ExperimentCompareTableQuery.graphql.ts b/app/src/pages/experiment/__generated__/ExperimentCompareTableQuery.graphql.ts index 0395a595db..512687c9cb 100644 --- a/app/src/pages/experiment/__generated__/ExperimentCompareTableQuery.graphql.ts +++ b/app/src/pages/experiment/__generated__/ExperimentCompareTableQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<5496aa01511684dacbbf9339c68f1593>> + * @generated SignedSource<<56f7ed5ad02bc5e59f842e1861f0aa83>> * @lightSyntaxTransform * @nogrep */ @@ -58,7 +58,11 @@ export type ExperimentCompareTableQuery$data = { readonly edges: ReadonlyArray<{ readonly experiment: { readonly id: string; + readonly metadata: any; readonly name: string; + readonly project: { + readonly id: string; + } | null; readonly sequenceNumber: number; }; }>; @@ -339,6 +343,25 @@ v7 = { "kind": "ScalarField", "name": "sequenceNumber", "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "metadata", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "Project", + "kind": "LinkedField", + "name": "project", + "plural": false, + "selections": [ + (v2/*: any*/) + ], + "storageKey": null } ], "storageKey": null @@ -414,16 +437,16 @@ return { ] }, "params": { - "cacheID": "97e80c617426f75b81affa68e276b3d8", + "cacheID": "e8b3f1f7011d0059ded7076b4d83afea", "id": null, "metadata": {}, "name": "ExperimentCompareTableQuery", "operationKind": "query", - "text": "query ExperimentCompareTableQuery(\n $experimentIds: [GlobalID!]!\n $datasetId: GlobalID!\n) {\n comparisons: compareExperiments(experimentIds: $experimentIds) {\n example {\n id\n revision {\n input\n referenceOutput: output\n }\n }\n runComparisonItems {\n experimentId\n runs {\n output\n error\n startTime\n endTime\n trace {\n traceId\n projectId\n }\n annotations {\n edges {\n annotation: node {\n id\n name\n score\n label\n annotatorKind\n explanation\n trace {\n traceId\n projectId\n }\n }\n }\n }\n }\n }\n }\n dataset: node(id: $datasetId) {\n __typename\n id\n ... on Dataset {\n experiments {\n edges {\n experiment: node {\n id\n name\n sequenceNumber\n }\n }\n }\n }\n }\n}\n" + "text": "query ExperimentCompareTableQuery(\n $experimentIds: [GlobalID!]!\n $datasetId: GlobalID!\n) {\n comparisons: compareExperiments(experimentIds: $experimentIds) {\n example {\n id\n revision {\n input\n referenceOutput: output\n }\n }\n runComparisonItems {\n experimentId\n runs {\n output\n error\n startTime\n endTime\n trace {\n traceId\n projectId\n }\n annotations {\n edges {\n annotation: node {\n id\n name\n score\n label\n annotatorKind\n explanation\n trace {\n traceId\n projectId\n }\n }\n }\n }\n }\n }\n }\n dataset: node(id: $datasetId) {\n __typename\n id\n ... on Dataset {\n experiments {\n edges {\n experiment: node {\n id\n name\n sequenceNumber\n metadata\n project {\n id\n }\n }\n }\n }\n }\n }\n}\n" } }; })(); -(node as any).hash = "e6132b301f6d80be7f5c5b0653e25178"; +(node as any).hash = "7e1c83a2fa6f2f46532902b526017663"; export default node; diff --git a/app/src/pages/experiments/ExperimentsTable.tsx b/app/src/pages/experiments/ExperimentsTable.tsx index c30b3a979a..33821b222f 100644 --- a/app/src/pages/experiments/ExperimentsTable.tsx +++ b/app/src/pages/experiments/ExperimentsTable.tsx @@ -1,10 +1,4 @@ -import React, { - ReactNode, - useCallback, - useMemo, - useRef, - useState, -} from "react"; +import React, { useCallback, useMemo, useRef, useState } from "react"; import { graphql, usePaginationFragment } from "react-relay"; import { useNavigate } from "react-router"; import { @@ -13,19 +7,12 @@ import { getCoreRowModel, useReactTable, } from "@tanstack/react-table"; -import copy from "copy-to-clipboard"; import { css } from "@emotion/react"; import { - ActionMenu, - Dialog, - DialogContainer, Flex, Heading, HelpTooltip, - Icon, - Icons, - Item, ProgressBar, Text, TooltipTrigger, @@ -34,8 +21,8 @@ import { } from "@arizeai/components"; import { AnnotationColorSwatch } from "@phoenix/components/annotation"; -import { JSONBlock } from "@phoenix/components/code"; import { SequenceNumberLabel } from "@phoenix/components/experiment"; +import { ExperimentActionMenu } from "@phoenix/components/experiment/ExperimentActionMenu"; import { Link } from "@phoenix/components/Link"; import { CompactJSONCell, IntCell } from "@phoenix/components/table"; import { IndeterminateCheckboxCell } from "@phoenix/components/table/IndeterminateCheckboxCell"; @@ -43,9 +30,7 @@ import { selectableTableCSS } from "@phoenix/components/table/styles"; import { TextCell } from "@phoenix/components/table/TextCell"; import { TimestampCell } from "@phoenix/components/table/TimestampCell"; import { LatencyText } from "@phoenix/components/trace/LatencyText"; -import { useNotifySuccess } from "@phoenix/contexts"; import { useWordColor } from "@phoenix/hooks/useWordColor"; -import { assertUnreachable } from "@phoenix/typeUtils"; import { floatFormatter, formatPercent, @@ -495,108 +480,3 @@ function AnnotationAggregationCell({ ); } - -export enum ExperimentAction { - GO_TO_EXPERIMENT_RUN_TRACES = "GO_TO_EXPERIMENT_RUN_TRACES", - COPY_EXPERIMENT_ID = "COPY_EXPERIMENT_ID", - VIEW_METADATA = "VIEW_METADATA", -} - -function ExperimentActionMenu(props: { - projectId: string | null; - experimentId: string; - metadata: unknown; -}) { - const { projectId } = props; - const navigate = useNavigate(); - const [dialog, setDialog] = useState(null); - const notifySuccess = useNotifySuccess(); - return ( -
{ - // prevent parent anchor link from being followed - e.preventDefault(); - e.stopPropagation(); - }} - > - { - const action = firedAction as ExperimentAction; - switch (action) { - case ExperimentAction.GO_TO_EXPERIMENT_RUN_TRACES: { - return navigate(`/projects/${projectId}`); - } - case ExperimentAction.VIEW_METADATA: { - setDialog( - setDialog(null)}> - - - ); - break; - } - case ExperimentAction.COPY_EXPERIMENT_ID: { - copy(props.experimentId); - notifySuccess({ - title: "Copied", - message: "The experiment ID has been copied to your clipboard", - }); - break; - } - default: { - assertUnreachable(action); - } - } - }} - > - - - } /> - View run traces - - - - - } /> - View metadata - - - - - } /> - Copy experiment ID - - - - { - setDialog(null); - }} - > - {dialog} - -
- ); -}