diff --git a/static/app/views/navigation/index.mobile.spec.tsx b/static/app/views/navigation/index.mobile.spec.tsx index 03e81e8f3d26..b18cb57885ad 100644 --- a/static/app/views/navigation/index.mobile.spec.tsx +++ b/static/app/views/navigation/index.mobile.spec.tsx @@ -1,9 +1,11 @@ +import {BroadcastFixture} from 'sentry-fixture/broadcast'; import {GroupSearchViewFixture} from 'sentry-fixture/groupSearchView'; import {OrganizationFixture} from 'sentry-fixture/organization'; import {UserFixture} from 'sentry-fixture/user'; import { render, + renderGlobalModal, screen, userEvent, within, @@ -184,6 +186,34 @@ describe('mobile navigation', () => { ).toBeInTheDocument(); }); + it("moves the Command Palette into the mobile row and What's New into the Help menu", async () => { + const context = navigationContext(); + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/broadcasts/', + body: [BroadcastFixture({title: 'Mobile Broadcast', hasSeen: true})], + }); + + render( + + + , + context + ); + renderGlobalModal({organization: context.organization}); + + const mobileHeader = within(screen.getByRole('banner')); + expect( + mobileHeader.getByRole('button', {name: 'Command Palette'}) + ).toBeInTheDocument(); + expect(mobileHeader.getByRole('button', {name: 'Help'})).toBeInTheDocument(); + expect(screen.queryByRole('button', {name: "What's New"})).not.toBeInTheDocument(); + + await userEvent.click(screen.getByRole('button', {name: 'Help'})); + await userEvent.click(screen.getByRole('menuitemradio', {name: "What's New"})); + + expect(await screen.findByText('Mobile Broadcast')).toBeInTheDocument(); + }); + describe('secondary nav route inference', () => { it('opens secondary navigation by default when on a sub-view', async () => { render( diff --git a/static/app/views/navigation/mobileNavigation.tsx b/static/app/views/navigation/mobileNavigation.tsx index a72d630b95fd..03021d37663d 100644 --- a/static/app/views/navigation/mobileNavigation.tsx +++ b/static/app/views/navigation/mobileNavigation.tsx @@ -7,6 +7,7 @@ import {Flex, type FlexProps, Stack} from '@sentry/scraps/layout'; import {SizeProvider} from '@sentry/scraps/sizeContext'; import {useScrollLock} from '@sentry/scraps/useScrollLock'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {IconMenu} from 'sentry/icons'; import {t} from 'sentry/locale'; import {useOnClickOutside} from 'sentry/utils/useOnClickOutside'; @@ -20,7 +21,12 @@ import { PrimaryNavigationItems, } from 'sentry/views/navigation/navigation'; import {PrimaryNavigation} from 'sentry/views/navigation/primary/components'; +import { + PrimaryNavigationHelpMenu, + useWhatsNewHelpMenuOptions, +} from 'sentry/views/navigation/primary/helpMenu'; import {OrganizationDropdown} from 'sentry/views/navigation/primary/organizationDropdown'; +import {SearchButton} from 'sentry/views/navigation/searchButton'; import {SecondaryNavigation} from 'sentry/views/navigation/secondary/components'; import {SecondaryNavigationContent} from 'sentry/views/navigation/secondary/content'; import {useSecondaryNavigation} from 'sentry/views/navigation/secondaryNavigationContext'; @@ -48,6 +54,15 @@ function MobileNavigationHeader(props: FlexProps<'header'>) { ); } +function MobileWhatsNewHelpMenu() { + const whatsNewHelpMenuOptions = useWhatsNewHelpMenuOptions(); + return ; +} + +function MobileHelpMenuFallback() { + return ; +} + function MobilePrimaryNavigation() { const {view} = useSecondaryNavigation(); @@ -136,7 +151,14 @@ export function MobileNavigation() { /> - + + + {buttonProps => } + + + + + diff --git a/static/app/views/navigation/navigation.tsx b/static/app/views/navigation/navigation.tsx index cc05684feff0..409dc65bb05a 100644 --- a/static/app/views/navigation/navigation.tsx +++ b/static/app/views/navigation/navigation.tsx @@ -1,4 +1,4 @@ -import {Fragment, type RefObject, useMemo, useRef} from 'react'; +import {Fragment, type PropsWithChildren, type RefObject, useMemo, useRef} from 'react'; import {mergeProps} from '@react-aria/utils'; import {motion, type MotionProps} from 'framer-motion'; @@ -92,7 +92,12 @@ export function Navigation() { paddingBottom="md" > - + + + + + + @@ -282,7 +287,7 @@ export function PrimaryNavigationItems({listRef}: PrimaryNavigationItemsProps) { /** * Returns the list of items from the footer of the primary navigation */ -export function PrimaryNavigationFooterItems() { +export function PrimaryNavigationFooterItems({children}: PropsWithChildren) { const organization = useOrganization(); return ( @@ -302,10 +307,7 @@ export function PrimaryNavigationFooterItems() { - - - - + {children} ); } diff --git a/static/app/views/navigation/primary/components.tsx b/static/app/views/navigation/primary/components.tsx index 3f533a8aca57..6d2bea34af1d 100644 --- a/static/app/views/navigation/primary/components.tsx +++ b/static/app/views/navigation/primary/components.tsx @@ -324,7 +324,7 @@ function PrimaryNavigationMenu(props: PrimaryNavigationMenuProps) { > ) => { if (organization) { trackAnalytics('navigation.primary_item_clicked', { @@ -361,7 +361,7 @@ function NavigationButton(props: DistributedOmit) { const {layout} = usePrimaryNavigation(); return ( - + {p => ( ) { {...(layout === 'mobile' ? {variant: 'secondary'} : {variant: props.variant})} /> )} - + ); } +function PrimaryNavigationButtonContainer(props: React.ComponentProps) { + return ; +} + /** * @TODO(JonasBadalic) Scraps buttons have been setting overflow hidden onto the inner surface wrapper ever since * we inherited that component, and we need to override that to ensure that the indicator is visible as it will @@ -569,6 +573,7 @@ export const PrimaryNavigation = { ListItem: PrimaryNavigationListItem, Link: PrimaryNavigationLink, Button: PrimaryNavigationButton, + ButtonContainer: PrimaryNavigationButtonContainer, ButtonBar: PrimaryNavigationButtonBar, Menu: PrimaryNavigationMenu, ButtonOverlay: PrimaryNavigationButtonOverlay, diff --git a/static/app/views/navigation/primary/helpMenu.spec.tsx b/static/app/views/navigation/primary/helpMenu.spec.tsx index 2825044f5d30..9ea03ff5f5c8 100644 --- a/static/app/views/navigation/primary/helpMenu.spec.tsx +++ b/static/app/views/navigation/primary/helpMenu.spec.tsx @@ -1,10 +1,25 @@ +import {BroadcastFixture} from 'sentry-fixture/broadcast'; import {OrganizationFixture} from 'sentry-fixture/organization'; -import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; +import { + render, + renderGlobalModal, + screen, + userEvent, +} from 'sentry-test/reactTestingLibrary'; import {ConfigStore} from 'sentry/stores/configStore'; +import {ModalStore} from 'sentry/stores/modalStore'; import * as intercom from 'sentry/utils/intercom'; -import {PrimaryNavigationHelpMenu} from 'sentry/views/navigation/primary/helpMenu'; +import { + PrimaryNavigationHelpMenu, + useWhatsNewHelpMenuOptions, +} from 'sentry/views/navigation/primary/helpMenu'; + +function HelpMenuWithWhatsNew() { + const whatsNewOptions = useWhatsNewHelpMenuOptions(); + return ; +} jest.mock('sentry/utils/intercom', () => ({ showIntercom: jest.fn(), @@ -18,6 +33,7 @@ async function expandResourcesSubmenu() { describe('PrimaryNavigationHelpMenu', () => { beforeEach(() => { jest.clearAllMocks(); + ModalStore.reset(); ConfigStore.set('supportEmail', 'support@sentry.io'); }); @@ -31,4 +47,22 @@ describe('PrimaryNavigationHelpMenu', () => { expect(intercom.showIntercom).toHaveBeenCalledWith(organization.slug); }); + + it("updates What's New when broadcasts finish loading", async () => { + const organization = OrganizationFixture(); + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/broadcasts/`, + match: [MockApiClient.matchQuery({show: 'latest', limit: '3'})], + asyncDelay: 100, + body: [BroadcastFixture({id: '1', title: 'New Broadcast', hasSeen: true})], + }); + + render(, {organization}); + renderGlobalModal({organization}); + + await userEvent.click(screen.getByRole('button', {name: 'Help'})); + await userEvent.click(screen.getByRole('menuitemradio', {name: "What's New"})); + + expect(await screen.findByText('New Broadcast')).toBeInTheDocument(); + }); }); diff --git a/static/app/views/navigation/primary/helpMenu.tsx b/static/app/views/navigation/primary/helpMenu.tsx index 3a51cb4545f8..45897a987992 100644 --- a/static/app/views/navigation/primary/helpMenu.tsx +++ b/static/app/views/navigation/primary/helpMenu.tsx @@ -1,9 +1,12 @@ -import {useEffect} from 'react'; +import {Fragment, useEffect} from 'react'; import {Flex} from '@sentry/scraps/layout'; +import {openModal} from 'sentry/actionCreators/modal'; import type {MenuItemProps} from 'sentry/components/dropdownMenu'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import { + IconBroadcast, IconBuilding, IconDiscord, IconDocs, @@ -26,8 +29,20 @@ import {showIntercom} from 'sentry/utils/intercom'; import {useFeedbackForm} from 'sentry/utils/useFeedbackForm'; import {useOrganization} from 'sentry/utils/useOrganization'; import {PrimaryNavigation} from 'sentry/views/navigation/primary/components'; +import { + useWhatsNewBroadcasts, + WhatsNewContent, +} from 'sentry/views/navigation/primary/whatsNew'; + +interface PrimaryNavigationHelpMenuProps { + additionalItems?: MenuItemProps[]; + indicator?: 'accent' | 'danger' | 'warning'; +} -export function PrimaryNavigationHelpMenu() { +export function PrimaryNavigationHelpMenu({ + additionalItems = [], + indicator, +}: PrimaryNavigationHelpMenuProps = {}) { const organization = useOrganization(); const contactSupportItem = getContactSupportItem(organization); const openForm = useFeedbackForm(); @@ -38,6 +53,7 @@ export function PrimaryNavigationHelpMenu() { }, [organization]); const items: MenuItemProps[] = [ + ...additionalItems, { key: 'resources', label: t('Resources'), @@ -199,10 +215,42 @@ export function PrimaryNavigationHelpMenu() { analyticsKey="help" label={t('Help')} icon={} + indicator={indicator} /> ); } +export function useWhatsNewHelpMenuOptions(): PrimaryNavigationHelpMenuProps { + const {unseenPostIds} = useWhatsNewBroadcasts(); + + return { + additionalItems: [ + { + key: 'whats-new', + label: t("What's New"), + leadingItems: ( + + + + ), + onAction() { + openModal(({Header, Body}) => ( + +
{t("What's New")}
+ + + + + +
+ )); + }, + }, + ], + indicator: unseenPostIds.length > 0 ? 'accent' : undefined, + }; +} + function getContactSupportItem(organization: Organization): MenuItemProps | null { const supportEmail = ConfigStore.get('supportEmail'); diff --git a/static/app/views/navigation/primary/whatsNew.tsx b/static/app/views/navigation/primary/whatsNew.tsx index 6f22ee87cd99..6b3593713de4 100644 --- a/static/app/views/navigation/primary/whatsNew.tsx +++ b/static/app/views/navigation/primary/whatsNew.tsx @@ -49,7 +49,7 @@ function BroadcastImage({src, alt}: {alt: string; src: string}) { ); } -function WhatsNewContent({ +function WhatsNewBroadcastList({ unseenPostIds, isPending, broadcasts = [], @@ -186,7 +186,15 @@ function WhatsNewContent({ ); } -export function PrimaryNavigationWhatsNew() { +/** + * Recent broadcasts plus the derived unseen/deduped views of them. Shared by the + * standalone What's New button and the help menu entry that replaces it when the + * navigation row has no room for a dedicated trigger. + * + * Pass `enabled: false` from a caller that only conditionally surfaces + * broadcasts, so it does not fetch them when it will not show them. + */ +export function useWhatsNewBroadcasts({enabled = true}: {enabled?: boolean} = {}) { const organization = useOrganization(); const {isPending, data: broadcasts} = useApiQuery( [ @@ -196,6 +204,7 @@ export function PrimaryNavigationWhatsNew() { {query: {show: 'latest', limit: '3'}}, ], { + enabled, // Five minute stale time prevents window focus frequent refetches staleTime: 1000 * 60 * 5, // 10 minutes poll @@ -224,6 +233,24 @@ export function PrimaryNavigationWhatsNew() { }); }, [allBroadcasts]); + return {isPending, unseenPostIds, uniqueBroadcasts}; +} + +export function WhatsNewContent() { + const {isPending, unseenPostIds, uniqueBroadcasts} = useWhatsNewBroadcasts(); + + return ( + + ); +} + +export function PrimaryNavigationWhatsNew() { + const {unseenPostIds} = useWhatsNewBroadcasts(); + const { isOpen, triggerProps: overlayTriggerProps, @@ -243,11 +270,7 @@ export function PrimaryNavigationWhatsNew() { /> {isOpen && ( - + )} diff --git a/static/app/views/navigation/searchButton.tsx b/static/app/views/navigation/searchButton.tsx index a68ebb23b21b..77055c88dc70 100644 --- a/static/app/views/navigation/searchButton.tsx +++ b/static/app/views/navigation/searchButton.tsx @@ -1,7 +1,4 @@ -import {useTheme} from '@emotion/react'; -import styled from '@emotion/styled'; - -import {Button} from '@sentry/scraps/button'; +import {Button, type ButtonProps} from '@sentry/scraps/button'; import {Hotkey} from '@sentry/scraps/hotkey'; import {Flex} from '@sentry/scraps/layout'; @@ -12,24 +9,29 @@ import { } from 'sentry/components/commandPalette/ui/commandPaletteStateContext'; import {IconSearch} from 'sentry/icons'; import {t} from 'sentry/locale'; -import {useMedia} from 'sentry/utils/useMedia'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useSeerExplorerContext} from 'sentry/views/seerExplorer/useSeerExplorerContext'; import {isSeerExplorerEnabled} from 'sentry/views/seerExplorer/utils'; -export function SearchButton() { - const theme = useTheme(); +export function SearchButton(props: Pick) { const organization = useOrganization({allowNull: true}); const state = useCommandPaletteState(); const dispatch = useCommandPaletteDispatch(); const {openSeerExplorer} = useSeerExplorerContext(); - const isDesktop = useMedia(`(min-width: ${theme.breakpoints.md})`); - return ( - } - aria-label={t('Search')} + aria-label={t('Command Palette')} + tooltipProps={{ + title: ( + + {t('Command Palette')} + + + ), + }} onClick={() => { if (!organization) { return; @@ -43,19 +45,6 @@ export function SearchButton() { isSeerExplorerEnabled(organization) ? openSeerExplorer : undefined ); }} - > - {isDesktop ? ( - - {t('Search')} - - - ) : null} - + /> ); } - -const StyledButton = styled(Button)` - > span:last-child { - overflow: visible; - } -`; diff --git a/static/app/views/navigation/topBar.spec.tsx b/static/app/views/navigation/topBar.spec.tsx index 09a1cdf8a74d..19baf6c41658 100644 --- a/static/app/views/navigation/topBar.spec.tsx +++ b/static/app/views/navigation/topBar.spec.tsx @@ -1,23 +1,50 @@ import {OrganizationFixture} from 'sentry-fixture/organization'; +import {ThemeFixture} from 'sentry-fixture/theme'; -import {render, screen, within} from 'sentry-test/reactTestingLibrary'; -import {getEmotionRules} from 'sentry-test/utils'; +import {render, screen} from 'sentry-test/reactTestingLibrary'; import {BreadcrumbList} from '@sentry/scraps/breadcrumbList'; +import {Flex} from '@sentry/scraps/layout'; import {TopBar} from './topBar'; -function renderTopBar() { - render( +const theme = ThemeFixture(); + +jest.mock('sentry/components/feedbackButton/feedbackButton', () => ({ + FeedbackButton: ({variant}: {variant?: string}) => ( + + ), +})); + +jest.mock('sentry/views/seerExplorer/utils', () => ({ + ...jest.requireActual('sentry/views/seerExplorer/utils'), + isSeerExplorerEnabled: () => true, +})); + +function renderTopBar(width?: number) { + if (width !== undefined) { + jest.spyOn(Element.prototype, 'clientWidth', 'get').mockReturnValue(width); + } + + const topBar = ( Page title - , - {organization: OrganizationFixture()} + ); + + render({topBar}, { + organization: OrganizationFixture({ + features: ['gen-ai-features', 'seer-explorer'], + }), + }); } -describe('TopBar title slot', () => { +describe('TopBar', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + it('renders the title as an h1 by default', () => { renderTopBar(); @@ -26,20 +53,6 @@ describe('TopBar title slot', () => { ).toBeInTheDocument(); }); - it('hides the empty breadcrumbs outlet when only the title slot is used', () => { - renderTopBar(); - - const emptyBreadcrumbsOutlet = Array.from( - screen.getByRole('banner').querySelectorAll('*') - ).find(element => - getEmotionRules(element).some( - rule => /display:\s*none/.test(rule) && /flex:\s*0 1 auto/.test(rule) - ) - ); - - expect(emptyBreadcrumbsOutlet).toBeDefined(); - }); - it('keeps BreadcrumbList titles inside the single TopBar heading', () => { render( @@ -54,9 +67,35 @@ describe('TopBar title slot', () => { {organization: OrganizationFixture()} ); - expect(within(screen.getByRole('banner')).getAllByRole('heading')).toHaveLength(1); + expect(screen.getAllByRole('heading', {name: /.+/})).toHaveLength(1); expect( screen.getByRole('heading', {name: 'Current Issue', level: 1}) ).toBeInTheDocument(); }); + + it('uses icon-only actions below sm', () => { + renderTopBar(Number.parseFloat(theme.container.sm) - 1); + + expect(screen.queryByText('Command Palette')).not.toBeInTheDocument(); + expect(screen.queryByText('Ask Seer')).not.toBeInTheDocument(); + }); + + it('shows the Ask Seer label while keeping other actions compact at sm', () => { + renderTopBar(Number.parseFloat(theme.container.sm)); + const askSeerButton = screen.getByRole('button', {name: 'Ask Seer'}); + + expect(screen.queryByText('Command Palette')).not.toBeInTheDocument(); + expect(screen.getByText('Ask Seer')).toBeInTheDocument(); + expect(screen.queryByText('/')).not.toBeInTheDocument(); + expect(screen.getByRole('button', {name: 'Give Feedback'})).toHaveAttribute( + 'data-variant', + 'transparent' + ); + + expect(screen.getAllByRole('button')).toEqual([ + askSeerButton, + screen.getByRole('button', {name: 'Command Palette'}), + screen.getByRole('button', {name: 'Give Feedback'}), + ]); + }); }); diff --git a/static/app/views/navigation/topBar.tsx b/static/app/views/navigation/topBar.tsx index ae485690692a..1be82d1a0f02 100644 --- a/static/app/views/navigation/topBar.tsx +++ b/static/app/views/navigation/topBar.tsx @@ -11,6 +11,7 @@ import {FeedbackButton} from 'sentry/components/feedbackButton/feedbackButton'; import {t} from 'sentry/locale'; import {useOrganization} from 'sentry/utils/useOrganization'; import {SearchButton} from 'sentry/views/navigation/searchButton'; +import {useTopBarActionDisplay} from 'sentry/views/navigation/useTopBarActionDisplay'; import {useTopOffset} from 'sentry/views/navigation/useTopOffset'; import {AskSeerButton} from 'sentry/views/seerExplorer/components/askSeerButton'; import {useSeerExplorerChatState} from 'sentry/views/seerExplorer/seerExplorerChatStateContext'; @@ -33,6 +34,7 @@ function TopBarContent() { const {pageContentTop} = useTopOffset(); const organization = useOrganization({allowNull: true}); + const {isSearchInMobileRow} = useTopBarActionDisplay(); useEffect(() => { document.documentElement.style.setProperty(TOP_BAR_HEIGHT_CSS_VAR, pageContentTop); @@ -88,16 +90,11 @@ function TopBarContent() { containerType="inline-size" > - {(props, hasConsumers) => ( - - )} + {(props, hasConsumers) => + hasConsumers ? ( + + ) : null + } @@ -113,15 +110,19 @@ function TopBarContent() { - {props => } + {(props, hasConsumers) => + hasConsumers ? : null + } - {props => } + {(props, hasConsumers) => + hasConsumers ? : null + } - {isSeerExplorerEnabled(organization) ? : null} + {isSearchInMobileRow ? null : } {props => ( diff --git a/static/app/views/navigation/topBarActions.spec.tsx b/static/app/views/navigation/topBarActions.spec.tsx new file mode 100644 index 000000000000..f708413d6585 --- /dev/null +++ b/static/app/views/navigation/topBarActions.spec.tsx @@ -0,0 +1,58 @@ +import {OrganizationFixture} from 'sentry-fixture/organization'; +import {ThemeFixture} from 'sentry-fixture/theme'; + +import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; + +import {Flex} from '@sentry/scraps/layout'; + +import {SearchButton} from 'sentry/views/navigation/searchButton'; +import {AskSeerButton} from 'sentry/views/seerExplorer/components/askSeerButton'; + +const theme = ThemeFixture(); + +function renderActions(width = 0) { + jest.spyOn(Element.prototype, 'clientWidth', 'get').mockReturnValue(width); + + return render( + + + + , + {organization: OrganizationFixture()} + ); +} + +describe('top bar actions', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('shows tooltips for icon-only actions', async () => { + renderActions(); + + const searchButton = screen.getByRole('button', {name: 'Command Palette'}); + const askSeerButton = screen.getByRole('button', {name: 'Ask Seer'}); + expect(screen.queryByText('Command Palette')).not.toBeInTheDocument(); + expect(screen.queryByText('Ask Seer')).not.toBeInTheDocument(); + + await userEvent.hover(searchButton); + expect(screen.getByText('Command Palette')).toBeInTheDocument(); + + await userEvent.hover(askSeerButton); + expect(screen.getByText('Ask Seer')).toBeInTheDocument(); + }); + + it('keeps Command Palette compact and exposes the Ask Seer shortcut in its tooltip at sm', async () => { + renderActions(Number.parseFloat(theme.container.sm)); + + const askSeerButton = screen.getByRole('button', {name: 'Ask Seer'}); + expect(screen.getByRole('button', {name: 'Command Palette'})).toBeInTheDocument(); + expect(screen.queryByText('Command Palette')).not.toBeInTheDocument(); + expect(screen.getByText('Ask Seer')).toBeInTheDocument(); + expect(screen.queryByText('K')).not.toBeInTheDocument(); + + await userEvent.hover(askSeerButton); + expect(screen.getByText('Ctrl', {selector: 'kbd'})).toBeInTheDocument(); + expect(screen.getByText('/', {selector: 'kbd'})).toBeInTheDocument(); + }); +}); diff --git a/static/app/views/navigation/useTopBarActionDisplay.tsx b/static/app/views/navigation/useTopBarActionDisplay.tsx new file mode 100644 index 000000000000..c889b0be69f2 --- /dev/null +++ b/static/app/views/navigation/useTopBarActionDisplay.tsx @@ -0,0 +1,21 @@ +import {useResponsivePropValue} from '@sentry/scraps/layout'; + +import {usePrimaryNavigation} from 'sentry/views/navigation/primaryNavigationContext'; + +export type TopBarActionDisplay = 'icon' | 'label'; + +export function useTopBarActionDisplay(): { + display: TopBarActionDisplay; + isSearchInMobileRow: boolean; +} { + const isSearchInMobileRow = usePrimaryNavigation().layout === 'mobile'; + const responsiveDisplay = useResponsivePropValue({ + zero: 'icon', + sm: 'label', + }); + + return { + display: isSearchInMobileRow ? 'icon' : responsiveDisplay, + isSearchInMobileRow, + }; +} diff --git a/static/app/views/seerExplorer/components/askSeerButton.tsx b/static/app/views/seerExplorer/components/askSeerButton.tsx index 9fcbb7673425..ac5182388938 100644 --- a/static/app/views/seerExplorer/components/askSeerButton.tsx +++ b/static/app/views/seerExplorer/components/askSeerButton.tsx @@ -10,12 +10,15 @@ import {Text} from '@sentry/scraps/text'; import {IconSeer} from 'sentry/icons'; import {t} from 'sentry/locale'; +import {useTopBarActionDisplay} from 'sentry/views/navigation/useTopBarActionDisplay'; import {useSeerExplorerContext} from 'sentry/views/seerExplorer/useSeerExplorerContext'; export function AskSeerButton() { const {isOpen, toggleSeerExplorer, sessionState: state} = useSeerExplorerContext(); + const {display: actionDisplay} = useTopBarActionDisplay(); const showMessageIndicator = !isOpen && state === 'done-thinking'; const prefersReducedMotion = useReducedMotion(); + const isIconOnly = actionDisplay === 'icon'; return ( + {t('Ask Seer')} + + + ), + }} icon={ - + + + {showMessageIndicator && isIconOnly ? : null} + } > - - - {t('Ask Seer')} - - - - - {state === 'thinking' ? ( - - {prefersReducedMotion ? ( - {t('Thinking...')} - ) : ( - - )} - - ) : null} - {showMessageIndicator ? ( + {isIconOnly ? null : ( + - + {t('Ask Seer')} - ) : null} - + {state === 'thinking' ? ( + + {prefersReducedMotion ? ( + {t('Thinking...')} + ) : ( + + )} + + ) : null} + {showMessageIndicator ? : null} + + )} ); } +function MessageIndicator() { + return ( + + + + ); +} + const SeerLoader = styled(Flex)` color: ${p => p.theme.tokens.graphics.accent.vibrant}; `;