From 3ab5de88afa55de11b9b41ab3d38b49e96f88ed7 Mon Sep 17 00:00:00 2001 From: elijahbenizzy Date: Tue, 12 Nov 2024 18:01:11 +0000 Subject: [PATCH] WIP for parallelism UI --- .../components/routes/app/AnnotationsView.tsx | 36 +- .../ui/src/components/routes/app/AppView.tsx | 269 +++++-- .../components/routes/app/InsightsView.tsx | 30 +- .../components/routes/app/StateMachine.tsx | 31 +- .../ui/src/components/routes/app/StepList.tsx | 741 ++++++++++++++---- telemetry/ui/src/utils.tsx | 10 + 6 files changed, 871 insertions(+), 246 deletions(-) diff --git a/telemetry/ui/src/components/routes/app/AnnotationsView.tsx b/telemetry/ui/src/components/routes/app/AnnotationsView.tsx index 7ed0ed8c..8aabedd1 100644 --- a/telemetry/ui/src/components/routes/app/AnnotationsView.tsx +++ b/telemetry/ui/src/components/routes/app/AnnotationsView.tsx @@ -15,7 +15,7 @@ import CreatableSelect from 'react-select/creatable'; import { FaClipboardList, FaExternalLinkAlt, FaThumbsDown, FaThumbsUp } from 'react-icons/fa'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../common/table'; import { Chip } from '../../common/chip'; -import { Link, useParams } from 'react-router-dom'; +import { Link } from 'react-router-dom'; import { useMutation, useQuery } from 'react-query'; import { Loading } from '../../common/loading'; import { @@ -28,6 +28,7 @@ import { import { classNames } from '../../../utils/tailwind'; import { DateTimeDisplay } from '../../common/dates'; import { Drawer } from '../../common/drawer'; +import { useLocationParams } from '../../../utils'; export const InlineAppView = (props: { projectId: string; @@ -47,7 +48,11 @@ export const InlineAppView = (props: { allowAnnotations={false} restrictTabs={['data', 'code', 'reproduce', 'insights', 'graph']} disableNavigateSteps={false} - forceCurrentActionIndex={props.sequenceID} + forceCurrentActionIndex={{ + sequenceId: props.sequenceID, + appId: props.appId, + partitionKey: props.partitionKey + }} partitionKey={props.partitionKey} forceFullScreen={true} /> @@ -95,7 +100,6 @@ export const AnnotationsView = (props: { setCurrentEditingAnnotationContext, setCurrentHoverIndex, setCurrentSelectedIndex, - currentSelectedIndex, createAnnotation, updateAnnotation, refreshAnnotationData @@ -170,14 +174,24 @@ export const AnnotationsView = (props: { annotations={props.allAnnotations} onClick={(annotation) => { // TODO -- ensure that the indices are aligned/set correctly - setCurrentSelectedIndex(annotation.step_sequence_id); + setCurrentSelectedIndex({ + sequenceId: annotation.step_sequence_id, + appId: props.appId, + partitionKey: props.partitionKey + }); }} onHover={(annotation) => { - setCurrentHoverIndex(annotation.step_sequence_id); + setCurrentHoverIndex({ + sequenceId: annotation.step_sequence_id, + appId: props.appId, + partitionKey: props.partitionKey + }); }} displayProjectLevelAnnotationsLink={true} // we want to link back to the project level view projectId={props.projectId} - highlightedSequence={currentSelectedIndex} + highlightedSequence={ + currentEditingAnnotationContext ? currentEditingAnnotationContext.sequenceId : undefined + } /> ); @@ -717,6 +731,8 @@ export const AnnotationsTable = (props: { {props.allowInlineEdit && ( { props.setCurrentEditingAnnotationContext({ + appId: props.appID, + partitionKey: props.partitionKey, sequenceId: props.sequenceID, attributeName: props.attribute, spanId: props.spanID || null, @@ -1187,7 +1209,7 @@ const AnnotationEditCreateForm = (props: { * @returns */ export const AnnotationsViewContainer = () => { - const { projectId } = useParams(); + const { projectId } = useLocationParams(); const { data: backendSpec } = useQuery(['backendSpec'], () => DefaultService.getAppSpecApiV0MetadataAppSpecGet().then((response) => { return response; diff --git a/telemetry/ui/src/components/routes/app/AppView.tsx b/telemetry/ui/src/components/routes/app/AppView.tsx index bcff56ee..63eb06a1 100644 --- a/telemetry/ui/src/components/routes/app/AppView.tsx +++ b/telemetry/ui/src/components/routes/app/AppView.tsx @@ -1,19 +1,18 @@ -import { Navigate, useParams } from 'react-router'; +import { Navigate } from 'react-router'; import { AnnotationCreate, AnnotationOut, AnnotationUpdate, AttributeModel, - DefaultService, - Step + DefaultService } from '../../../api'; import { useMutation, useQuery } from 'react-query'; import { Loading } from '../../common/loading'; -import { StepList } from './StepList'; +import { ApplicationTable } from './StepList'; import { TwoColumnLayout, TwoRowLayout } from '../../common/layout'; import { AppStateView } from './StateMachine'; import { createContext, useEffect, useState } from 'react'; -import { Status } from '../../../utils'; +import { Status, useLocationParams } from '../../../utils'; import { GraphView } from './GraphView'; import { useSearchParams } from 'react-router-dom'; @@ -21,17 +20,17 @@ import { useSearchParams } from 'react-router-dom'; * Gives a list of prior actions. Note they're currently sorted in order from * most recent to least recent. We should probably switch that. */ -const getPriorActions = (currentActionIndex: number, steps: Step[], numPrevious: number) => { - return steps - .filter( - (step) => - step.step_start_log.sequence_id < currentActionIndex && - step.step_start_log.sequence_id > currentActionIndex - numPrevious - ) - .slice(0, numPrevious); -}; +// const getPriorActions = (currentActionIndex: number, steps: Step[], numPrevious: number) => { +// return steps +// .filter( +// (step) => +// step.step_start_log.sequence_id < currentActionIndex && +// step.step_start_log.sequence_id > currentActionIndex - numPrevious +// ) +// .slice(0, numPrevious); +// }; -const REFRESH_INTERVAL = 500; +export const REFRESH_INTERVAL = 500; export const backgroundColorsForStatus = (status: Status) => { const colorsByStatus = { @@ -90,8 +89,13 @@ export const backgroundColorsForIndex = (index: number, status: Status) => { // Default number of previous actions to show const NUM_PREVIOUS_ACTIONS = 0; -export type AnnotationEditingContext = { +export type SequenceLocation = { + appId: string; + partitionKey: string | null; sequenceId: number; +}; + +export type AnnotationEditingContext = SequenceLocation & { spanId: string | null; attributeName?: string; existingAnnotation: AnnotationOut | undefined; @@ -104,10 +108,10 @@ export type HighlightState = { setAttributesHighlighted: (attributes: AttributeModel[]) => void; setTab: (tab: string) => void; tab: string; - setCurrentSelectedIndex: (index: number | undefined) => void; - currentSelectedIndex?: number; - setCurrentHoverIndex: (index: number | undefined) => void; - currentHoverIndex?: number; + setCurrentSelectedIndex: (index: SequenceLocation | undefined) => void; + currentSelectedIndex?: SequenceLocation; + setCurrentHoverIndex: (index: SequenceLocation | undefined) => void; + currentHoverIndex?: SequenceLocation; currentEditingAnnotationContext?: AnnotationEditingContext; setCurrentEditingAnnotationContext: ( annotationContext: AnnotationEditingContext | undefined @@ -163,41 +167,82 @@ export const AppView = (props: { allowAnnotations: boolean; restrictTabs?: string[]; disableNavigateSteps?: boolean; - forceCurrentActionIndex?: number; + forceCurrentActionIndex?: SequenceLocation; forceFullScreen?: boolean; }) => { const [searchParams, setSearchParams] = useSearchParams(); const [topToBottomChronological, setTopToBottomChronological] = useState(true); const [inspectViewOpen, setInspectViewOpen] = useState(false); - const currentActionIndex = searchParams.get('sequence_id') - ? parseInt(searchParams.get('sequence_id')!) - : undefined; - const _setCurrentActionIndex = (index: number | undefined) => { + // const currentActionIndex = searchParams.get('sequence_id') + // ? parseInt(searchParams.get('sequence_id')!) + // : undefined; + // const _setCurrentActionIndex = (index: number | undefined) => { + // const newSearchParams = new URLSearchParams(searchParams); // Clone the searchParams + // if (index !== undefined) { + // newSearchParams.set('sequence_id', index.toString()); + // } else { + // newSearchParams.delete('sequence_id'); + // } + // setSearchParams(newSearchParams); // Update the searchParams with the new object + // }; + // if ( + // props.forceCurrentActionIndex !== undefined && + // currentActionIndex !== props.forceCurrentActionIndex + // ) { + // _setCurrentActionIndex(props.forceCurrentActionIndex); + // } + + // const setCurrentActionIndex = (index: number | undefined) => { + // if (!props.disableNavigateSteps) { + // _setCurrentActionIndex(index); + // } + // }; + + const currentSequenceLocation = ( + searchParams.get('sequence_location') + ? JSON.parse(searchParams.get('sequence_location')!) + : undefined + ) as SequenceLocation | undefined; + + // we want to use the current App ID passed in + const appID = props.appId; + // But if we want to focus on sub-applications we need to load those steps as well + // These will get passed to the data views + const currentFocusAppID = + currentSequenceLocation?.appId !== undefined ? currentSequenceLocation?.appId : appID; + // Ditto with partition key + const partitionKey = props.partitionKey; + const currentFocusPartitionKey = + currentSequenceLocation?.partitionKey !== undefined + ? currentSequenceLocation?.partitionKey + : props.partitionKey; + const _setCurrentSequenceLocation = (location: SequenceLocation | undefined) => { const newSearchParams = new URLSearchParams(searchParams); // Clone the searchParams - if (index !== undefined) { - newSearchParams.set('sequence_id', index.toString()); + if (location !== undefined) { + newSearchParams.set('sequence_location', JSON.stringify(location)); } else { - newSearchParams.delete('sequence_id'); + newSearchParams.delete('sequence_location'); } setSearchParams(newSearchParams); // Update the searchParams with the new object }; + if ( props.forceCurrentActionIndex !== undefined && - currentActionIndex !== props.forceCurrentActionIndex + JSON.stringify(currentSequenceLocation) !== JSON.stringify(props.forceCurrentActionIndex) ) { - _setCurrentActionIndex(props.forceCurrentActionIndex); + _setCurrentSequenceLocation(props.forceCurrentActionIndex); } - const setCurrentActionIndex = (index: number | undefined) => { + const setCurrentSequenceLocation = (location: SequenceLocation | undefined) => { if (!props.disableNavigateSteps) { - _setCurrentActionIndex(index); + _setCurrentSequenceLocation(location); } }; - const { projectId, appId, partitionKey } = props; + const { projectId } = props; // const [currentActionIndex, setCurrentActionIndex] = useState(undefined); - const [hoverSequenceID, setHoverSequenceID] = useState(undefined); + const [hoverIndex, setHoverIndex] = useState(undefined); const [autoRefresh, setAutoRefresh] = useState(props.defaultAutoRefresh || false); - const shouldQuery = projectId !== undefined && appId !== undefined; + const shouldQuery = projectId !== undefined && appID !== undefined; const [minimizedTable, setMinimizedTable] = useState(false); const [highlightedAttributes, setHighlightedAttributes] = useState([]); const fullScreen = @@ -230,11 +275,11 @@ export const AppView = (props: { }) ); const { data, error } = useQuery( - ['steps', appId], + ['steps', appID, partitionKey], () => DefaultService.getApplicationLogsApiV0ProjectIdAppIdPartitionKeyAppsGet( projectId as string, - appId as string, + appID as string, props.partitionKey !== null ? props.partitionKey : '__none__' ), { @@ -242,15 +287,51 @@ export const AppView = (props: { enabled: shouldQuery } ); + + const { data: currentFocusStepsData } = useQuery( + ['steps', currentFocusAppID, currentFocusPartitionKey], + () => + DefaultService.getApplicationLogsApiV0ProjectIdAppIdPartitionKeyAppsGet( + projectId as string, + currentFocusAppID as string, + currentFocusPartitionKey !== null ? currentFocusPartitionKey : '__none__' + ), + { + refetchInterval: autoRefresh ? REFRESH_INTERVAL : false, + enabled: currentFocusAppID !== appID && currentFocusAppID !== undefined + } + ); // TODO -- use a skiptoken to bypass annotation loading if we don't need them const { data: annotationsData, refetch: refetchAnnotationsData } = useQuery( - ['annotations', appId], + ['annotations', appID, partitionKey], () => DefaultService.getAnnotationsApiV0ProjectIdAnnotationsGet( projectId as string, - appId as string, + appID as string, partitionKey !== null ? partitionKey : '__none__' - ) + ), + { + refetchInterval: autoRefresh ? REFRESH_INTERVAL : false, + enabled: shouldQuery && props.allowAnnotations && backendSpec?.supports_annotations + } + ); + + const { data: currentFocusAnnotationsData } = useQuery( + ['annotations', currentFocusAppID, currentFocusPartitionKey], + () => + DefaultService.getAnnotationsApiV0ProjectIdAnnotationsGet( + projectId as string, + currentFocusAppID as string, + currentFocusPartitionKey !== null ? partitionKey : '__none__' + ), + { + enabled: + shouldQuery && + props.allowAnnotations && + backendSpec?.supports_annotations && + currentFocusAppID !== appID && + currentFocusAppID !== undefined + } ); const createAnnotationMutation = useMutation( @@ -263,7 +344,7 @@ export const AppView = (props: { }) => DefaultService.createAnnotationApiV0ProjectIdAppIdPartitionKeySequenceIdAnnotationsPost( projectId, - appId, + appID, data.partitionKey !== null ? data.partitionKey : '__none__', data.sequenceID, data.annotationData @@ -286,20 +367,39 @@ export const AppView = (props: { const handleKeyDown = (event: KeyboardEvent) => { switch (event.key) { case topToBottomChronological ? 'ArrowUp' : 'ArrowDown': - if (currentActionIndex === undefined || currentActionIndex <= minSequenceID) { - setCurrentActionIndex(minSequenceID); + if ( + currentSequenceLocation === undefined || + currentSequenceLocation.sequenceId <= minSequenceID + ) { + setCurrentSequenceLocation({ + appId: appID, + partitionKey: partitionKey, + sequenceId: minSequenceID + }); } else { - setCurrentActionIndex(currentActionIndex - 1); + setCurrentSequenceLocation({ + ...currentSequenceLocation, + sequenceId: currentSequenceLocation.sequenceId - 1 + }); + // semtCurrentActionIndex(currentActionIndex - 1); } break; case topToBottomChronological ? 'ArrowDown' : 'ArrowUp': // case 'ArrowUp': - if (currentActionIndex === undefined) { - setCurrentActionIndex(maxSequenceID); - } else if (currentActionIndex >= maxSequenceID) { - setCurrentActionIndex(currentActionIndex); + if (currentSequenceLocation === undefined) { + setCurrentSequenceLocation({ + appId: appID, + partitionKey: partitionKey, + sequenceId: maxSequenceID + }); + } else if (currentSequenceLocation.sequenceId >= maxSequenceID) { + // setCurrentActionIndex(currentActionIndex); } else { - setCurrentActionIndex(currentActionIndex + 1); + setCurrentSequenceLocation({ + ...currentSequenceLocation, + sequenceId: currentSequenceLocation.sequenceId + 1 + }); + // setCurrentActionIndex(currentActionIndex + 1); } break; default: @@ -312,13 +412,17 @@ export const AppView = (props: { return () => { document.removeEventListener('keydown', handleKeyDown); }; - }, [data?.steps, currentActionIndex, setCurrentActionIndex]); + }, [data?.steps, currentSequenceLocation, setCurrentSequenceLocation]); useEffect(() => { if (autoRefresh) { const maxSequenceID = Math.max( ...(data?.steps.map((step) => step.step_start_log.sequence_id) || []) ); - setCurrentActionIndex(maxSequenceID); + setCurrentSequenceLocation({ + appId: appID, + partitionKey: partitionKey, + sequenceId: maxSequenceID + }); } }, [data, autoRefresh]); if (!shouldQuery) { @@ -329,11 +433,11 @@ export const AppView = (props: { if (data === undefined || backendSpec === undefined || annotationsData === undefined) return ; const displayAnnotations = props.allowAnnotations && backendSpec.supports_annotations; - - const previousActions = - currentActionIndex !== undefined - ? getPriorActions(currentActionIndex, data.steps, NUM_PREVIOUS_ACTIONS) - : currentActionIndex; + // TODO -- re-enable this if I want... + // const previousActions = + // currentActionIndex !== undefined + // ? getPriorActions(currentActionIndex, data.steps, NUM_PREVIOUS_ACTIONS) + // : currentActionIndex; const stepsCopied = [...data.steps]; const stepsSorted = stepsCopied.sort((a, b) => { // Parse dates to get time in milliseconds @@ -359,10 +463,18 @@ export const AppView = (props: { }); const Layout = props.orientation === 'stacked_horizontal' ? TwoColumnLayout : TwoRowLayout; const currentStep = stepsSorted.find( - (step) => step.step_start_log.sequence_id === currentActionIndex + (step) => + step.step_start_log.sequence_id === currentSequenceLocation?.sequenceId && + appID === currentSequenceLocation?.appId && + partitionKey === currentSequenceLocation?.partitionKey ); - const hoverAction = hoverSequenceID - ? stepsSorted.find((step) => step.step_start_log.sequence_id === hoverSequenceID) + const hoverAction = hoverIndex + ? stepsSorted.find( + (step) => + step.step_start_log.sequence_id === hoverIndex.sequenceId && + appID === hoverIndex.appId && + partitionKey === hoverIndex.partitionKey + ) : undefined; return ( @@ -372,13 +484,13 @@ export const AppView = (props: { setAttributesHighlighted: setHighlightedAttributes, setTab: setCurrentTab, tab: currentTab, - setCurrentSelectedIndex: (n) => { - setCurrentActionIndex(n); - setInspectViewOpen(n !== undefined); + setCurrentSelectedIndex: (loc) => { + setCurrentSequenceLocation(loc); + setInspectViewOpen(loc !== undefined); }, - currentSelectedIndex: currentActionIndex, - setCurrentHoverIndex: setHoverSequenceID, - currentHoverIndex: hoverSequenceID, + currentSelectedIndex: currentSequenceLocation, + setCurrentHoverIndex: setHoverIndex, + currentHoverIndex: hoverIndex, currentEditingAnnotationContext: currentEditingAnnotationContext, setCurrentEditingAnnotationContext: setCurrentEditingAnnotationContext, // TODO -- handle span ID @@ -418,8 +530,10 @@ export const AppView = (props: {
-
@@ -454,16 +569,19 @@ export const AppView = (props: { } secondItem={ setInspectViewOpen(!min)} isMinimized={!inspectViewOpen} allowMinimized={inspectViewOpen && fullScreen} - annotations={annotationsData} + // TODO -- render better if this is undefined -- this is an odd way to fall back + annotations={currentFocusAnnotationsData || annotationsData} restrictTabs={props.restrictTabs} allowAnnotations={displayAnnotations} /> @@ -475,15 +593,12 @@ export const AppView = (props: { }; export const AppViewContainer = () => { - const { projectId, appId, partitionKey } = useParams(); - if (projectId === undefined || appId === undefined) { - return
Invalid URL
; - } + const { projectId, appId, partitionKey } = useLocationParams(); return ( { const individualValues = props.insight.captureIndividualValues?.(props.attributes); const [isExpanded, setIsExpanded] = React.useState(false); @@ -394,13 +396,29 @@ const InsightSubTable = (props: { key={attribute.key + i} className={` ${isCurrentSelected ? 'bg-gray-200' : isHovered ? 'bg-gray-50' : 'hover:bg-gray-50'} cursor-pointer`} onMouseEnter={() => { - setCurrentHoverIndex(span?.begin_entry.action_sequence_id); + setCurrentHoverIndex( + span + ? { + sequenceId: span.begin_entry.action_sequence_id, + appId: props.appID, + partitionKey: props.partitionKey + } + : undefined + ); }} onMouseLeave={() => { setCurrentHoverIndex(undefined); }} onClick={() => { - setCurrentSelectedIndex(span?.begin_entry.action_sequence_id); + setCurrentSelectedIndex( + span + ? { + sequenceId: span.begin_entry.action_sequence_id, + appId: props.appID, + partitionKey: props.partitionKey + } + : undefined + ); }} > @@ -429,7 +447,11 @@ const InsightSubTable = (props: { ); }; -export const InsightsView = (props: { steps: Step[] }) => { +export const InsightsView = (props: { + steps: Step[]; + appId: string; + partitionKey: string | null; +}) => { const allAttributes: AttributeModel[] = props.steps.flatMap((step) => step.attributes); const allSpans = props.steps.flatMap((step) => step.spans); @@ -456,6 +478,8 @@ export const InsightsView = (props: { steps: Step[] }) => { insight={insight} allSpans={allSpans} allSteps={props.steps} + appID={props.appId} + partitionKey={props.partitionKey} /> ); } diff --git a/telemetry/ui/src/components/routes/app/StateMachine.tsx b/telemetry/ui/src/components/routes/app/StateMachine.tsx index 5f13f74c..197f5157 100644 --- a/telemetry/ui/src/components/routes/app/StateMachine.tsx +++ b/telemetry/ui/src/components/routes/app/StateMachine.tsx @@ -5,11 +5,11 @@ import { ActionView } from './ActionView'; import { GraphView } from './GraphView'; import { InsightsView } from './InsightsView'; import { ReproduceView } from './ReproduceView'; -import { useParams } from 'react-router-dom'; import { useContext } from 'react'; -import { AppContext } from './AppView'; +import { AppContext, SequenceLocation } from './AppView'; import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline'; import { AnnotationsView } from './AnnotationsView'; +import { useLocationParams } from '../../../utils'; const NoStepSelected = () => { return ( @@ -24,7 +24,7 @@ export const AppStateView = (props: { stateMachine: ApplicationModel; highlightedActions: Step[] | undefined; hoverAction: Step | undefined; - currentSequenceID: number | undefined; + currentActionLocation: SequenceLocation | undefined; displayGraphAsTab: boolean; setMinimized: (minimized: boolean) => void; isMinimized: boolean; @@ -34,17 +34,24 @@ export const AppStateView = (props: { allowAnnotations?: boolean; }) => { const { tab, setTab } = useContext(AppContext); + const { projectId, appId, partitionKey } = useLocationParams(); const currentStep = props.steps.find( - (step) => step.step_start_log.sequence_id === props.currentSequenceID - ); - const priorStep = props.steps.find( - (step) => step.step_start_log.sequence_id === (props.currentSequenceID || 0) - 1 + (step) => + // We don't get the global App ID -- we assume the steps are from the right one + step.step_start_log.sequence_id === props.currentActionLocation?.sequenceId + // appId === props.currentActionLocation?.appId && + // partitionKey === props.currentActionLocation?.partitionKey ); + const priorStep = + currentStep && + props.steps.find( + (step) => + step.step_start_log.sequence_id === (props.currentActionLocation?.sequenceId || 0) - 1 + ); const actionModel = props.stateMachine.actions.find( (action) => action.name === currentStep?.step_start_log.action ); - const { projectId, appId, partitionKey } = useParams(); const tabs = [ { id: 'data', displayName: 'Data' }, { id: 'code', displayName: 'Code' }, @@ -86,7 +93,13 @@ export const AppStateView = (props: { hoverAction={props.hoverAction} /> )} - {tab === 'insights' && } + {tab === 'insights' && ( + + )} {tab === 'reproduce' && (currentStep ? ( { return ( @@ -82,6 +90,17 @@ const AutoRefreshSwitch = (props: { /> ); }; + +const RecursionDepthPadding = (props: { depth: number; children: React.ReactNode }) => { + return ( +
+ {new Array(props.depth).fill(0).map((i) => ( + + ))} + {props.children} +
+ ); +}; /** * Quick component to make the table row common between * the action and span rows @@ -93,35 +112,50 @@ const CommonTableRow = (props: { sequenceID: number; isHovered: boolean; shouldBeHighlighted: boolean; - currentSelectedIndex: number | undefined; + currentSelectedIndex: SequenceLocation | undefined; step: Step; - setCurrentHoverIndex: (index?: number) => void; - setCurrentSelectedIndex: (index?: number) => void; + setCurrentHoverIndex: (index?: SequenceLocation) => void; + setCurrentSelectedIndex: (index?: SequenceLocation) => void; + appID: string; + partitionKey: string | null; }) => { return ( { - props.setCurrentHoverIndex(props.sequenceID); + props.setCurrentHoverIndex({ + sequenceId: props.sequenceID, + appId: props.appID, + partitionKey: props.partitionKey + }); }} onMouseLeave={() => { props.setCurrentHoverIndex(undefined); }} - onClick={() => { - if (props.currentSelectedIndex === props.sequenceID) { + onClick={(e) => { + if ( + props.currentSelectedIndex?.sequenceId === props.sequenceID && + props.currentSelectedIndex?.appId === props.appID && + props.currentSelectedIndex?.partitionKey === props.partitionKey + ) { props.setCurrentSelectedIndex(undefined); } else { - props.setCurrentSelectedIndex(props.sequenceID); + props.setCurrentSelectedIndex({ + sequenceId: props.sequenceID, + appId: props.appID, + partitionKey: props.partitionKey + }); } + e.stopPropagation(); }} > {props.children} @@ -150,6 +184,9 @@ const ActionTableRow = (props: { streamingEvents: Array; displayAnnotations: boolean; existingAnnotation: AnnotationOut | undefined; + appID: string; + partitionKey: string | null; + depth: number; }) => { const sequenceID = props.step.step_start_log.sequence_id; const { @@ -161,13 +198,22 @@ const ActionTableRow = (props: { setCurrentSelectedIndex } = useContext(AppContext); - const isHovered = currentHoverIndex === sequenceID; + const isHovered = + currentHoverIndex?.sequenceId === sequenceID && + currentHoverIndex?.appId === props.appID && + currentHoverIndex?.partitionKey === props.partitionKey; // const spanCount = props.step.spans.length; const childCount = props.links.length; const shouldBeHighlighted = currentSelectedIndex !== undefined && - sequenceID <= currentSelectedIndex && - sequenceID >= currentSelectedIndex - props.numPriorIndices; + currentSelectedIndex.appId === props.appID && + currentSelectedIndex.partitionKey === props.partitionKey && + sequenceID <= currentSelectedIndex.sequenceId && + sequenceID >= currentSelectedIndex.sequenceId - props.numPriorIndices; + // const shouldBeHighlighted = + // currentSelectedIndex !== undefined && + // sequenceID <= currentSelectedIndex && + // sequenceID >= currentSelectedIndex - props.numPriorIndices; // const TraceExpandIcon = props.isTracesExpanded ? MinusIcon : PlusIcon; const LinkExpandIcon = props.isLinksExpanded ? MinusIcon : PlusIcon; const attributes = props.step.attributes || []; @@ -185,8 +231,16 @@ const ActionTableRow = (props: { step={props.step} setCurrentHoverIndex={setCurrentHoverIndex} setCurrentSelectedIndex={setCurrentSelectedIndex} + appID={props.appID} + partitionKey={props.partitionKey} > - {sequenceID} + +
+ + {sequenceID}{' '} + +
+
@@ -236,7 +290,7 @@ const ActionTableRow = (props: { startTime={new Date(props.step.step_start_log.start_time)} bgColor={backgroundColorsForStatus(getActionStatus(props.step))} isHighlighted={shouldBeHighlighted} - isEvent={false} + kind="span" globalEllapsedTimeMillis={props.step.latestGlobalEllapsedTime} /> @@ -266,6 +320,8 @@ const ActionTableRow = (props: { {props.displayAnnotations && ( { @@ -280,8 +336,109 @@ const ActionTableRow = (props: { ); }; +/** + * Loads the data for the StepList + anything else + * @param props + * @returns + */ +const SelfLoadingSubApplicationContainer = (props: { + appID: string; + partitionKey: string | null; + projectID: string; + minimized: boolean; + projectId: string; + topToBottomChronological: boolean; + displayAnnotations: boolean; + // traceExpandedActions: number[]; + displaySpansCol: boolean; + displayLinksCol: boolean; + earliestTimeSeen: Date; + latestTimeSeen: Date; + // links: ChildApplicationModel[]; + autoRefresh: boolean; + depth: number; +}) => { + const { data } = useQuery( + ['steps', props.projectID, props.appID, props.partitionKey], + () => + DefaultService.getApplicationLogsApiV0ProjectIdAppIdPartitionKeyAppsGet( + props.projectId as string, + props.appID as string, + props.partitionKey !== null ? props.partitionKey : '__none__' + ), + { + // TODO -- decide how we want to auto-refresh with lots of nested stuff? + // Really, we'll want a bulk API but this is OK for now... + refetchInterval: props.autoRefresh ? REFRESH_INTERVAL : false, + enabled: true + } + ); + // TODO -- use a skiptoken to bypass annotation loading if we don't need them + const { data: annotationsData } = useQuery( + ['annotations', props.projectID, props.appID, props.partitionKey], + () => + DefaultService.getAnnotationsApiV0ProjectIdAnnotationsGet( + props.projectId as string, + props.appID as string, + props.partitionKey !== null ? props.partitionKey : '__none__' + ) + ); + const [traceExpandedActions, setTraceExpandedActions] = useState([]); + const [linksExpandedActions, setLinksExpandedActions] = useState([]); + + const toggleTraceExpandedActions = (index: number) => { + if (traceExpandedActions.includes(index)) { + setTraceExpandedActions(traceExpandedActions.filter((i) => i !== index)); + } else { + setTraceExpandedActions([...traceExpandedActions, index]); + } + }; + const isLinksExpanded = (index: number) => { + return linksExpandedActions.includes(index); + }; + const toggleLinksExpanded = (index: number) => { + if (isLinksExpanded(index)) { + setLinksExpandedActions(linksExpandedActions.filter((i) => i !== index)); + } else { + setLinksExpandedActions([...linksExpandedActions, index]); + } + }; + + if (data === undefined || annotationsData === undefined) { + return <>; + } + const steps = data.steps; + const stepsWithEllapsedTime = collapseTimestampsToEllapsedTime(steps); + const annotations = annotationsData; + return ( + + ); +}; const LinkSubTable = (props: { + appID: string; + partitionKey: string | null; step: StepWithEllapsedTime; numPriorIndices: number; minimized: boolean; @@ -289,64 +446,156 @@ const LinkSubTable = (props: { projectId: string; displaySpansCol: boolean; displayLinksCol: boolean; + // TODO -- consider pushing down into this component, we probably don't need it in the container + expandedSubApplicationIDs: string[]; + setExpandedSubApplicationIDs: (appIDs: string[]) => void; + topToBottomChronological: boolean; + earliestTimeSeen: Date; + latestTimeSeen: Date; + depth: number; }) => { const { currentHoverIndex, setCurrentHoverIndex, currentSelectedIndex, setCurrentSelectedIndex } = useContext(AppContext); const sequenceID = props.step.step_start_log.sequence_id; - const isHovered = currentHoverIndex === sequenceID; + const isHovered = + currentHoverIndex?.appId === props.appID && + currentHoverIndex?.partitionKey === props.partitionKey && + currentHoverIndex?.sequenceId === sequenceID; const shouldBeHighlighted = currentSelectedIndex !== undefined && - sequenceID <= currentSelectedIndex && - sequenceID >= currentSelectedIndex - props.numPriorIndices; + currentSelectedIndex.appId === props.appID && + currentSelectedIndex.partitionKey === props.partitionKey && + sequenceID <= currentSelectedIndex.sequenceId && + sequenceID >= currentSelectedIndex.sequenceId - props.numPriorIndices; const normalText = shouldBeHighlighted ? 'text-gray-100' : 'text-gray-600'; const iconColor = shouldBeHighlighted ? 'text-gray-100' : 'text-gray-400'; const navigate = useNavigate(); return ( <> {props.links.map((subApp) => { + const subApplicationIsExpanded = props.expandedSubApplicationIDs.includes( + subApp.child.app_id + ); + // const ExpandIcon = subApplicationIsExpanded ? MinusIcon : PlusIcon; + // TODO - handle partition key? + const toggleExpanded = (appID: string) => { + if (props.expandedSubApplicationIDs.includes(appID)) { + props.setExpandedSubApplicationIDs( + props.expandedSubApplicationIDs.filter((id) => id !== appID) + ); + } else { + props.setExpandedSubApplicationIDs([...props.expandedSubApplicationIDs, appID]); + } + }; + // TODO: Implement this function properly + // const handleMagnifyClick = () => { + // // e.stopPropagation(); + // toggleExpanded(subApp.child.app_id); + // }; const childType = subApp.event_type; const Icon = childType === 'fork' ? TbGrillFork : TiFlowChildren; return ( - - - - - + -
{ - navigate( - `/project/${props.projectId}/${subApp.child.partition_key || 'null'}/${subApp.child.app_id}` - ); - e.stopPropagation(); - }} + + + + + + - {subApp.child.app_id} -
-
- {!props.minimized && ( - - { + navigate( + `/project/${props.projectId}/${subApp.child.partition_key || 'null'}/${subApp.child.app_id}` + ); + e.stopPropagation(); + }} + > + {subApp.child.app_id} +
+ + + { + toggleExpanded(subApp.child.app_id); + }} + isSubActionExpanded={subApplicationIsExpanded} /> + {/*
+
+ +
+ +
+
+
*/} + + {!props.minimized && ( + + + + )} + + + {subApplicationIsExpanded ? ( + + ) : ( + <> )} - - + ); })} @@ -354,6 +603,8 @@ const LinkSubTable = (props: { }; const StepSubTableRow = (props: { + appID: string; + partitionKey: string | null; spanID: string | null; // undefined if it is not associated with a span, E.G a free-standing attribute name: string; minimized: boolean; @@ -372,6 +623,7 @@ const StepSubTableRow = (props: { setDisplayAttributes?: (b: boolean) => void; displayAttributes?: boolean; displayAnnotations: boolean; + depth: number; // app-depth with recursion }) => { const { setCurrentHoverIndex, @@ -398,8 +650,6 @@ const StepSubTableRow = (props: { if (props.modelType === 'first_item_stream' || props.modelType === 'end_stream') { depth += 1; } - const isEvent = props.modelType !== 'span'; - // const idToDisplay = props.displaySpanID ? spanIDUniqueToAction : ''; const onClick = () => { if (props.modelType !== 'attribute') { return; @@ -410,6 +660,8 @@ const StepSubTableRow = (props: { }; return ( - {spanIDUniqueToAction} + + {spanIDUniqueToAction} + {!props.minimized ? ( <> @@ -482,7 +736,7 @@ const StepSubTableRow = (props: { startTime={props.startTime} bgColor={backgroundColorsForStatus(getActionStatus(props.step))} isHighlighted={props.shouldBeHighlighted} - isEvent={isEvent} + kind={'span'} globalEllapsedTimeMillis={props.step.latestGlobalEllapsedTime} /> ) : ( @@ -510,6 +764,8 @@ const StepSubTableRow = (props: { }; const StepSubTable = (props: { + appID: string; + partitionKey: string | null; spans: Span[]; attributes: AttributeModel[]; streamingEvents: Array; @@ -523,6 +779,7 @@ const StepSubTable = (props: { expandNonSpanAttributes: boolean; topToBottomChronological: boolean; displayAnnotations: boolean; + depth: number; }) => { const { currentHoverIndex, currentSelectedIndex } = useContext(AppContext); const attributesBySpanID = props.attributes.reduce((acc, attr) => { @@ -539,11 +796,17 @@ const StepSubTable = (props: { // return acc; // }, new Map>()); const sequenceID = props.step.step_start_log.sequence_id; - const isHovered = currentHoverIndex === sequenceID; + const isHovered = + currentHoverIndex?.sequenceId === sequenceID && + currentHoverIndex?.appId === props.appID && + currentHoverIndex?.partitionKey === props.partitionKey; + // const spanCount = props.step.spans.length; const shouldBeHighlighted = currentSelectedIndex !== undefined && - sequenceID <= currentSelectedIndex && - sequenceID >= currentSelectedIndex - props.numPriorIndices; + currentSelectedIndex.appId === props.appID && + currentSelectedIndex.partitionKey === props.partitionKey && + sequenceID <= currentSelectedIndex.sequenceId && + sequenceID >= currentSelectedIndex.sequenceId - props.numPriorIndices; const displayFullAppWaterfall = true; // TODO -- configure if we zoom on a step const allSpanIds = props.spans.map((span) => span.begin_entry.span_id); const [spanIdsWithAttributesDisplayed, setSpanIdsWithAttributesDisplayed] = useState( @@ -601,6 +864,8 @@ const StepSubTable = (props: { return ( ); } else { @@ -632,6 +898,8 @@ const StepSubTable = (props: { return ( ); } @@ -659,6 +928,8 @@ const StepSubTable = (props: { return ( ); }) @@ -697,6 +969,8 @@ const StepSubTable = (props: { , ...attributesToDisplay.map((attr, i) => { return ( ); }) @@ -761,11 +1039,14 @@ const WaterfallPiece: React.FC<{ globalEllapsedTimeMillis: number; // Total time of active trace bgColor: string; isHighlighted: boolean; - isEvent: boolean; + kind?: 'span' | 'event' | 'subaction'; // Whether to display just ellapsed (clock) time displayAbsoluteOrEllapsedTime?: 'absolute' | 'ellapsed'; + // In the case that we have the subaction type -- otherwise these will be undefined + setSubActionExpanded?: (b: boolean) => void; + isSubActionExpanded?: boolean; }> = (props) => { - const useAbsoluteTime = (props.displayAbsoluteOrEllapsedTime || 'ellapsed') === 'absolute'; + const useAbsoluteTime = (props.displayAbsoluteOrEllapsedTime || 'absolute') === 'absolute'; const containerRef = useRef(null); const stepStartTimeAbsolute = new Date(props.step.step_start_log.start_time).getTime(); @@ -806,25 +1087,27 @@ const WaterfallPiece: React.FC<{ const isCloseToEnd = leftPositionPercentage + widthPercentage > 80; // Threshold for "close to the end" // TODO -- unhack these, we're converting back and forth cause we already have interfaces for strings and // don't want to change - const hoverItem = props.isEvent ? ( - <> - ) : ( -
- - -
- ); + const SubActionIcon = props.isSubActionExpanded ? MinusIcon : PlusIcon; + const hoverItem = + props.kind === 'event' || props.kind === 'subaction' ? ( + <> + ) : ( +
+ + +
+ ); return ( <> - {!props.isEvent ? ( + {props.kind === 'span' ? (
setIsHovered(true)} @@ -836,8 +1119,8 @@ const WaterfallPiece: React.FC<{ height: '90%', left: `${leftPositionPercentage}%` }} - >
- ) : ( + /> + ) : props.kind === 'event' ? (
+ ) : ( + // Sub-action +
+
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + style={{ + width: `${widthPercentage}%`, + position: 'absolute', + bottom: '50%', + // height: '90%', + left: `${leftPositionPercentage}%` + }} + > + { + props.setSubActionExpanded?.(!props.isSubActionExpanded); + e.stopPropagation(); + }} + /> +
+
)} {/* Hoverable zone with buffer to avoid flicker */} @@ -921,40 +1228,92 @@ type StepWithEllapsedTime = Step & { * Note this may break if steps are not in a perfectly linear order. * This is not the case now, but as we add parallelism it very well may be. * @param steps + * @param globalStartTimeMillis -- optional, if not provided we assume the first step is the start + * @param globalEndTimeMillis - optional, if not provided we assume the last step is the end * @returns */ -const collapseTimestampsToEllapsedTime = (steps: Step[]): StepWithEllapsedTime[] => { +const collapseTimestampsToEllapsedTime = ( + steps: Step[], + globalStartTimeMillis?: number, + globalEndTimeMillis?: number +): StepWithEllapsedTime[] => { + if (steps.length === 0) { + return []; + } + const sortedSteps = steps.sort((a, b) => { return new Date(a.step_start_log.start_time) > new Date(b.step_start_log.start_time) ? 1 : -1; }); - if (sortedSteps.length === 0) { - return []; - } - let lastTime = new Date(sortedSteps[0].step_start_log.start_time); + + // If global start time is provided, calculate the offset to shift all times + const firstStepStartTime = new Date(sortedSteps[0].step_start_log.start_time).getTime(); + const timeOffset = globalStartTimeMillis ? globalStartTimeMillis - firstStepStartTime : 0; + + let lastTime = globalStartTimeMillis || firstStepStartTime; let cumulativeTimeMillis = 0; + return sortedSteps .map((step) => { - const stepStartTime = new Date(step.step_start_log.start_time); - const stepEndTime = new Date(step.step_end_log?.end_time || new Date()); - const pauseAfterLastStepMillis = stepStartTime.getTime() - lastTime.getTime(); - cumulativeTimeMillis += stepEndTime.getTime() - stepStartTime.getTime(); + const stepStartTime = new Date(step.step_start_log.start_time).getTime() + timeOffset; + const stepEndTime = + new Date(step.step_end_log?.end_time || new Date()).getTime() + timeOffset; + + const pauseAfterLastStepMillis = stepStartTime - lastTime; + const stepDurationMillis = stepEndTime - stepStartTime; + + cumulativeTimeMillis += stepDurationMillis; lastTime = stepEndTime; + return { ...step, cumulativeTimeMillis, - pauseAfterLastStepMillis: pauseAfterLastStepMillis, - stepDurationMillis: stepEndTime.getTime() - stepStartTime.getTime(), + pauseAfterLastStepMillis, + stepDurationMillis, latestGlobalEllapsedTime: 0 }; }) - .map((step) => { - return { - ...step, - latestGlobalEllapsedTime: cumulativeTimeMillis - }; - }); + .map((step) => ({ + ...step, + latestGlobalEllapsedTime: globalEndTimeMillis ? globalEndTimeMillis : cumulativeTimeMillis + })); }; +// const collapseTimestampsToEllapsedTime = ( +// steps: Step[], +// globalStartTimeMillis?: number, +// globalEndTimeMillis?: number +// ): StepWithEllapsedTime[] => { +// const sortedSteps = steps.sort((a, b) => { +// return new Date(a.step_start_log.start_time) > new Date(b.step_start_log.start_time) ? 1 : -1; +// }); +// if (sortedSteps.length === 0) { +// return []; +// } +// let lastTime = new Date(sortedSteps[0].step_start_log.start_time); +// let cumulativeTimeMillis = 0; +// return sortedSteps +// .map((step) => { +// const stepStartTime = new Date(step.step_start_log.start_time); +// const stepEndTime = new Date(step.step_end_log?.end_time || new Date()); +// const pauseAfterLastStepMillis = stepStartTime.getTime() - lastTime.getTime(); +// cumulativeTimeMillis += stepEndTime.getTime() - stepStartTime.getTime(); +// lastTime = stepEndTime; +// return { +// ...step, +// cumulativeTimeMillis, +// pauseAfterLastStepMillis: pauseAfterLastStepMillis, +// stepDurationMillis: stepEndTime.getTime() - stepStartTime.getTime(), +// latestGlobalEllapsedTime: 0 +// }; +// }) +// .map((step) => { +// return { +// ...step, +// latestGlobalEllapsedTime: cumulativeTimeMillis +// }; +// }); +// }; + const PauseRow = (props: { pauseMillis: number }) => { return ( @@ -972,6 +1331,8 @@ const PauseRow = (props: { pauseMillis: number }) => { const ActionSubTable = (props: { step: StepWithEllapsedTime; + appID: string; + partitionKey: string | null; numPriorIndices: number; isTraceExpanded: boolean; toggleTraceExpandedActions: (index: number) => void; @@ -990,6 +1351,7 @@ const ActionSubTable = (props: { topToBottomChronological: boolean; displayAnnotations: boolean; annotationsForStep: AnnotationOut[]; + depth: number; }) => { const { step, @@ -1004,9 +1366,11 @@ const ActionSubTable = (props: { earliestTimeSeen, setTraceExpanded, pauseTime, - topToBottomChronological + topToBottomChronological, + depth } = props; const [expandNonSpanAttributes, setExpandNonSpanAttributes] = useState(false); // attributes that are associated with the action, not the span... + const [highlightedSubApplicationIDs, setHighlightedSubApplicationIDs] = useState([]); // TODO -- lower a lot of this state into this component // This was pulled out quickly and we need to put anyting that's not read above into this return ( @@ -1016,6 +1380,8 @@ const ActionSubTable = (props: { )} {isTraceExpanded && ( )} {isLinksExpanded && ( )} {props.pauseLocation === 'bottom' && pauseTime !== undefined && ( @@ -1079,6 +1457,93 @@ const ActionSubTable = (props: { ); }; +export const StepList = (props: { + stepsWithEllapsedTime: StepWithEllapsedTime[]; + annotations: AnnotationOut[]; + numPriorIndices: number; + minimized: boolean; + projectId: string; + topToBottomChronological: boolean; + displayAnnotations: boolean; + traceExpandedActions: number[]; + setTraceExpandedActions: (indices: number[]) => void; + linksExpandedActions: number[]; + toggleTraceExpandedActions: (index: number) => void; + toggleLinksExpanded: (index: number) => void; + displaySpansCol: boolean; + displayLinksCol: boolean; + earliestTimeSeen: Date; + latestTimeSeen: Date; + links: ChildApplicationModel[]; + appID: string; + partitionKey: string | null; + depth: number; +}) => { + const linksBySequenceID = props.links.reduce((acc, child) => { + const existing = acc.get(child.sequence_id || -1) || []; + existing.push(child); + acc.set(child.sequence_id || -1, existing); + return acc; + }, new Map()); + return ( + <> + {props.stepsWithEllapsedTime.map((step) => { + // TODO -- make more efficient with a map + const annotationsForStep = props.annotations.filter( + (annotation) => annotation.step_sequence_id === step.step_start_log.sequence_id + ); + const isTraceExpanded = props.traceExpandedActions.includes( + step.step_start_log.sequence_id + ); + const setTraceExpanded = (b: boolean) => { + if (b) { + props.setTraceExpandedActions([ + ...props.traceExpandedActions, + step.step_start_log.sequence_id + ]); + } else { + props.setTraceExpandedActions( + props.traceExpandedActions.filter((i) => i !== step.step_start_log.sequence_id) + ); + } + }; + const isLinksExpanded = props.linksExpandedActions.includes( + step.step_start_log.sequence_id + ); + const links = linksBySequenceID.get(step.step_start_log.sequence_id) || []; + const beforePause = step.pauseAfterLastStepMillis > PAUSE_TIME_THRESHOLD_MILLIS; + return ( + + ); + })} + + ); +}; + // const TableWithRef = forwardRef>( // (props, ref) => { // return } />; @@ -1095,8 +1560,10 @@ const ActionSubTable = (props: { * TODO -- add pagination. * TODO -- fix up indexing */ -export const StepList = (props: { +export const ApplicationTable = (props: { steps: Step[]; + appID: string; + partitionKey: string | null; annotations: AnnotationOut[]; numPriorIndices: number; autoRefresh: boolean; @@ -1201,12 +1668,6 @@ export const StepList = (props: { (step) => step.spans.length > 0 || step.streaming_events.length > 0 ); const displayLinksCol = props.links.length > 0; - const linksBySequenceID = props.links.reduce((acc, child) => { - const existing = acc.get(child.sequence_id || -1) || []; - existing.push(child); - acc.set(child.sequence_id || -1, existing); - return acc; - }, new Map()); const [tableHeight, setTableHeight] = useState('auto'); @@ -1361,51 +1822,31 @@ export const StepList = (props: { {props.topToBottomChronological ? parentRows : <>} - {stepsWithEllapsedTime.map((step) => { - // TODO -- make more efficient with a map - const annotationsForStep = props.annotations.filter( - (annotation) => annotation.step_sequence_id === step.step_start_log.sequence_id - ); - const isTraceExpanded = traceExpandedActions.includes(step.step_start_log.sequence_id); - const setTraceExpanded = (b: boolean) => { - if (b) { - setTraceExpandedActions([...traceExpandedActions, step.step_start_log.sequence_id]); - } else { - setTraceExpandedActions( - traceExpandedActions.filter((i) => i !== step.step_start_log.sequence_id) - ); - } - }; - const isLinksExpanded = linksExpandedActions.includes(step.step_start_log.sequence_id); - const links = linksBySequenceID.get(step.step_start_log.sequence_id) || []; - const beforePause = step.pauseAfterLastStepMillis > PAUSE_TIME_THRESHOLD_MILLIS; - return ( - - ); - })} - {!props.topToBottomChronological ? parentRows : <>} + + + {!props.topToBottomChronological ? parentRows : <>}
diff --git a/telemetry/ui/src/utils.tsx b/telemetry/ui/src/utils.tsx index 0530c132..7ca1bcdf 100644 --- a/telemetry/ui/src/utils.tsx +++ b/telemetry/ui/src/utils.tsx @@ -1,3 +1,4 @@ +import { useParams } from 'react-router-dom'; import { AttributeModel, Step } from './api'; export type Status = 'success' | 'failure' | 'running'; @@ -18,3 +19,12 @@ export const getActionStatus = (action: Step) => { export const getUniqueAttributeID = (attribute: AttributeModel) => { return `${attribute.action_sequence_id}-${attribute.span_id}`; }; + +export const useLocationParams = () => { + const { projectId, appId, partitionKey } = useParams(); + return { + projectId: projectId as string, + appId: appId as string, + partitionKey: (partitionKey as string) === 'null' ? null : (partitionKey as string) + }; +};