From e7a24c77bed79f4dcaaebe209cdfceb260bb5f06 Mon Sep 17 00:00:00 2001 From: Rohan Agarwal Date: Wed, 12 Nov 2025 09:41:35 -0800 Subject: [PATCH] feat(explorer): replay tool UI and menu fixes --- .../app/views/seerExplorer/explorerMenu.tsx | 31 +++++++++++++++---- .../app/views/seerExplorer/inputSection.tsx | 7 ++--- static/app/views/seerExplorer/utils.tsx | 18 +++++++++++ 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/static/app/views/seerExplorer/explorerMenu.tsx b/static/app/views/seerExplorer/explorerMenu.tsx index 581137bf436621..25fb42d8532c60 100644 --- a/static/app/views/seerExplorer/explorerMenu.tsx +++ b/static/app/views/seerExplorer/explorerMenu.tsx @@ -1,7 +1,8 @@ -import {Activity, useCallback, useEffect, useMemo, useState} from 'react'; +import {Activity, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import styled from '@emotion/styled'; +import moment from 'moment-timezone'; -import {DateTime} from 'sentry/components/dateTime'; +import TimeSince from 'sentry/components/timeSince'; import {space} from 'sentry/styles/space'; import {useFeedbackForm} from 'sentry/utils/useFeedbackForm'; import {useExplorerSessions} from 'sentry/views/seerExplorer/hooks/useExplorerSessions'; @@ -129,12 +130,23 @@ export function useExplorerMenu({ const isVisible = menuMode !== 'hidden'; const [selectedIndex, setSelectedIndex] = useState(0); + const menuItemRefs = useRef>([]); // Reset selected index when items change useEffect(() => { setSelectedIndex(0); }, [menuItems]); + // Scroll selected item into view when selection changes + useEffect(() => { + if (isVisible && menuItemRefs.current[selectedIndex]) { + menuItemRefs.current[selectedIndex]?.scrollIntoView({ + block: 'nearest', + behavior: 'smooth', + }); + } + }, [selectedIndex, isVisible]); + // Handle keyboard navigation with higher priority const handleKeyDown = useCallback( (e: KeyboardEvent) => { @@ -193,6 +205,9 @@ export function useExplorerMenu({ {menuItems.map((item, index) => ( { + menuItemRefs.current[index] = el; + }} isSelected={index === selectedIndex} onClick={() => onSelect(item)} > @@ -310,9 +325,11 @@ function useSessions({ title: session.title, key: session.run_id.toString(), description: ( - - Last updated at - + ), handler: () => { onChangeSession(session.run_id); @@ -338,7 +355,6 @@ const MenuPanel = styled('div')<{ width: 300px; background: ${p => p.theme.background}; border: 1px solid ${p => p.theme.border}; - border-bottom: none; border-radius: ${p => p.theme.borderRadius}; box-shadow: ${p => p.theme.dropShadowHeavy}; max-height: ${p => @@ -366,6 +382,9 @@ const ItemName = styled('div')` font-weight: 600; color: ${p => p.theme.purple400}; font-size: ${p => p.theme.fontSize.sm}; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; `; const ItemDescription = styled('div')` diff --git a/static/app/views/seerExplorer/inputSection.tsx b/static/app/views/seerExplorer/inputSection.tsx index 92b15ff0330fdf..9a34ae5124cf65 100644 --- a/static/app/views/seerExplorer/inputSection.tsx +++ b/static/app/views/seerExplorer/inputSection.tsx @@ -93,13 +93,12 @@ const InputRow = styled('div')` const ButtonContainer = styled('div')` display: flex; - align-items: stretch; + align-items: center; padding: ${p => p.theme.space.sm}; button { - flex: 1; - height: 100%; - min-height: 100%; + width: auto; + padding: ${p => p.theme.space.md}; } `; diff --git a/static/app/views/seerExplorer/utils.tsx b/static/app/views/seerExplorer/utils.tsx index 22eb1c47240a5b..34abc0df41eb89 100644 --- a/static/app/views/seerExplorer/utils.tsx +++ b/static/app/views/seerExplorer/utils.tsx @@ -153,6 +153,14 @@ const TOOL_FORMATTERS: Record = { ? `Excavating commit history${dateRangeStr} in ${repoName}...` : `Excavated commit history${dateRangeStr} in ${repoName}`; }, + + get_replay_details: (args, isLoading) => { + const replayId = args.replay_id || ''; + const shortReplayId = replayId.slice(0, 8); + return isLoading + ? `Watching replay ${shortReplayId}...` + : `Watched replay ${shortReplayId}`; + }, }; /** @@ -310,6 +318,16 @@ export function buildToolLinkUrl( return {pathname: `/issues/${issue_id}/events/${event_id}/`}; } + case 'get_replay_details': { + const {replay_id} = toolLink.params; + if (!replay_id) { + return null; + } + + return { + pathname: `/organizations/${orgSlug}/replays/${replay_id}/`, + }; + } default: return null; }