From 0e168907e4e8756fae28aa562460c374d29bb327 Mon Sep 17 00:00:00 2001 From: "andrii.dudar" Date: Tue, 3 Sep 2024 09:50:22 +0200 Subject: [PATCH] OPIK-21 [UX] Improve UX of the trace component (#149) * OPIK-21 [UX] Improve UX of the trace component * fix letter spacing handling for safari --- .../DatasetCompareExperimentsPage.tsx | 30 ++----- .../TraceTreeViewer.module.scss | 5 +- .../TraceTreeViewer/TraceTreeViewer.tsx | 86 ++++++++++++++++++- apps/opik-frontend/src/lib/utils.ts | 16 ++++ 4 files changed, 108 insertions(+), 29 deletions(-) diff --git a/apps/opik-frontend/src/components/pages/DatasetCompareExperimentsPage/DatasetCompareExperimentsPage.tsx b/apps/opik-frontend/src/components/pages/DatasetCompareExperimentsPage/DatasetCompareExperimentsPage.tsx index 250123ebec..a715d56f28 100644 --- a/apps/opik-frontend/src/components/pages/DatasetCompareExperimentsPage/DatasetCompareExperimentsPage.tsx +++ b/apps/opik-frontend/src/components/pages/DatasetCompareExperimentsPage/DatasetCompareExperimentsPage.tsx @@ -24,7 +24,6 @@ import useAppStore from "@/store/AppStore"; import { useDatasetIdFromURL } from "@/hooks/useDatasetIdFromURL"; import { DatasetCompareAddExperimentHeader } from "@/components/pages/DatasetCompareExperimentsPage/DatasetCompareAddExperimentHeader"; import { - CELL_VERTICAL_ALIGNMENT, COLUMN_TYPE, ColumnData, OnChangeFn, @@ -164,21 +163,11 @@ const DatasetCompareExperimentsPage: React.FunctionComponent = () => { const retVal = convertColumnDataToColumn< ExperimentsCompare, ExperimentsCompare - >( - DEFAULT_COLUMNS.map((c) => { - return height === ROW_HEIGHT.small - ? { - ...c, - verticalAlignment: CELL_VERTICAL_ALIGNMENT.start, - } - : c; - }), - { - columnsWidth, - selectedColumns, - columnsOrder, - }, - ); + >(DEFAULT_COLUMNS, { + columnsWidth, + selectedColumns, + columnsOrder, + }); experimentsIds.forEach((id: string) => { const size = columnsWidth[id] ?? 400; @@ -204,14 +193,7 @@ const DatasetCompareExperimentsPage: React.FunctionComponent = () => { }); return retVal; - }, [ - columnsWidth, - selectedColumns, - columnsOrder, - experimentsIds, - setTraceId, - height, - ]); + }, [columnsWidth, selectedColumns, columnsOrder, experimentsIds, setTraceId]); const { data, isPending } = useCompareExperimentsList( { diff --git a/apps/opik-frontend/src/components/shared/TraceDetailsPanel/TraceTreeViewer/TraceTreeViewer.module.scss b/apps/opik-frontend/src/components/shared/TraceDetailsPanel/TraceTreeViewer/TraceTreeViewer.module.scss index 0a2702e98a..1f9dc0b489 100644 --- a/apps/opik-frontend/src/components/shared/TraceDetailsPanel/TraceTreeViewer/TraceTreeViewer.module.scss +++ b/apps/opik-frontend/src/components/shared/TraceDetailsPanel/TraceTreeViewer/TraceTreeViewer.module.scss @@ -55,8 +55,8 @@ height: 100%; .detailsContainerWithArrow { - width: 65%; - min-width: 250px; + width: var(--details-container-width, 65%); + min-width: 100px; display: flex; position: relative; @@ -163,6 +163,7 @@ } .chainSpanOuterContainer { + min-width: 150px; display: flex; flex-direction: column; align-items: start; diff --git a/apps/opik-frontend/src/components/shared/TraceDetailsPanel/TraceTreeViewer/TraceTreeViewer.tsx b/apps/opik-frontend/src/components/shared/TraceDetailsPanel/TraceTreeViewer/TraceTreeViewer.tsx index b789d5e333..75d60abff0 100644 --- a/apps/opik-frontend/src/components/shared/TraceDetailsPanel/TraceTreeViewer/TraceTreeViewer.tsx +++ b/apps/opik-frontend/src/components/shared/TraceDetailsPanel/TraceTreeViewer/TraceTreeViewer.tsx @@ -7,7 +7,7 @@ import { } from "react-complex-tree"; import { BASE_TRACE_DATA_TYPE, Span, Trace } from "@/types/traces"; import { treeRenderers } from "./treeRenderers"; -import { calcDuration } from "@/lib/utils"; +import { calcDuration, getTextWidth } from "@/lib/utils"; import { SPANS_COLORS_MAP, TRACE_TYPE_FOR_TREE } from "@/constants/traces"; import { Button } from "@/components/ui/button"; import useDeepMemo from "@/hooks/useDeepMemo"; @@ -32,6 +32,30 @@ type TraceTreeViewerProps = { onSelectRow: (id: string) => void; }; +type ItemWidthObject = { + id: string; + name: string; + parentId?: string; + children: ItemWidthObject[]; + level?: number; +}; + +const getSpansWithLevel = ( + item: ItemWidthObject, + accumulator: ItemWidthObject[] = [], + level = 0, +) => { + accumulator.push({ + ...item, + level, + }); + + if (item.children) { + item.children.forEach((i) => getSpansWithLevel(i, accumulator, level + 1)); + } + return accumulator; +}; + const TraceTreeViewer: React.FunctionComponent = ({ trace, spans, @@ -133,7 +157,7 @@ const TraceTreeViewer: React.FunctionComponent = ({ retVal[directParentKey].children?.push(span.id); } return retVal; - }, acc); + }); return retVal; }, [trace, traceSpans]); @@ -148,8 +172,64 @@ const TraceTreeViewer: React.FunctionComponent = ({ [rowId, expandedTraceSpans], ); + const maxWidth = useMemo(() => { + const map: Record = {}; + const list: ItemWidthObject[] = traceSpans.map((s) => ({ + id: s.id, + name: s.name || "", + parentId: s.parent_span_id, + children: [], + })); + const rootElement: ItemWidthObject = { + id: trace.id, + name: trace.name, + children: [], + level: 1, + }; + + list.forEach((item, index) => { + map[item.id] = index; + }); + + list.forEach((item) => { + if (item.parentId) { + list[map[item.parentId]].children.push(item); + } else { + rootElement.children.push(item); + } + }); + + const items = getSpansWithLevel(rootElement, [], 2); + + const widthArray = getTextWidth( + items.map((i) => i.name), + { font: "14px Inter" }, + ); + + const OTHER_SPACE = 52; + const LEVEL_WIDTH = 16; + + return Math.ceil( + Math.max( + ...items.map( + (i, index) => + OTHER_SPACE + + (i.level || 1) * LEVEL_WIDTH + + widthArray[index] * 1.03, //where 1.03 Letter spacing compensation + ), + ), + ); + }, [traceSpans, trace]); + return ( -
+
Trace spans
diff --git a/apps/opik-frontend/src/lib/utils.ts b/apps/opik-frontend/src/lib/utils.ts index 2ed65bd3b5..c9eee4b685 100644 --- a/apps/opik-frontend/src/lib/utils.ts +++ b/apps/opik-frontend/src/lib/utils.ts @@ -35,3 +35,19 @@ export const millisecondsToSeconds = (milliseconds: number) => { // rounds with precision, one character after the point return round(milliseconds / 1000, 1); }; + +export const getTextWidth = ( + text: string[], + properties: { + font?: string; + } = {}, +) => { + const canvas = document.createElement("canvas"); + const context = canvas.getContext("2d") as CanvasRenderingContext2D; + + if (properties.font) { + context.font = properties.font; + } + + return text.map((v) => context.measureText(v).width); +};