diff --git a/static/app/components/droppedData/drawer/droppedDataDrawer.tsx b/static/app/components/droppedData/drawer/droppedDataDrawer.tsx index abe1fb068559..82e93a1f953b 100644 --- a/static/app/components/droppedData/drawer/droppedDataDrawer.tsx +++ b/static/app/components/droppedData/drawer/droppedDataDrawer.tsx @@ -26,13 +26,13 @@ import {useChartInterval} from 'sentry/utils/useChartInterval'; interface DroppedDataDrawerProps { dataset: DiscoverDatasets; interval?: string; - onInvestigate?: (row: ReasonRow) => void; + onTroubleshoot?: (row: ReasonRow) => void; } export function DroppedDataDrawer({ dataset, interval, - onInvestigate, + onTroubleshoot, }: DroppedDataDrawerProps) { const theme = useTheme(); const [chartInterval] = useChartInterval(); @@ -87,7 +87,7 @@ export function DroppedDataDrawer({ )} diff --git a/static/app/components/droppedData/drawer/droppedDataOutcomeList.spec.tsx b/static/app/components/droppedData/drawer/droppedDataOutcomeList.spec.tsx index bf7265f2ee60..04560558ad63 100644 --- a/static/app/components/droppedData/drawer/droppedDataOutcomeList.spec.tsx +++ b/static/app/components/droppedData/drawer/droppedDataOutcomeList.spec.tsx @@ -10,17 +10,17 @@ import type {DroppedEventsBucket} from 'sentry/components/droppedData/types'; function OutcomeList({ droppedEvents, acceptedEvents, - onInvestigate, + onTroubleshoot, }: { acceptedEvents: DroppedEventsBucket[]; droppedEvents: DroppedEventsBucket[]; - onInvestigate?: (row: ReasonRow) => void; + onTroubleshoot?: (row: ReasonRow) => void; }) { return ( ); } @@ -94,7 +94,7 @@ describe('DroppedDataOutcomeList', () => { expect(screen.queryByText(/ of /)).not.toBeInTheDocument(); }); - it('renders the short description under the reason title', () => { + it('shows the short description when hovering the reason title', async () => { render( { /> ); - expect( - screen.getByText("User agent matched Sentry's known crawler list.") - ).toBeInTheDocument(); + const description = "User agent matched Sentry's known crawler list."; + expect(screen.queryByText(description)).not.toBeInTheDocument(); + + await userEvent.hover(screen.getByText('Web crawler filter')); + + expect(await screen.findByText(description)).toBeInTheDocument(); }); - it('fills the data type into the description from the event category', () => { + it('fills the data type into the description from the event category', async () => { render( { /> ); + await userEvent.hover(screen.getByText('Project abuse limit')); + expect( - screen.getByText('Your log events exceeded the project abuse limit.') + await screen.findByText('Your log events exceeded the project abuse limit.') ).toBeInTheDocument(); }); @@ -192,7 +197,8 @@ describe('DroppedDataOutcomeList', () => { expect(screen.queryByRole('table')).not.toBeInTheDocument(); }); - it('reveals the fix options under a reason row via its chevron', async () => { + it('opens the reason actions menu with docs and troubleshoot entries', async () => { + const onTroubleshoot = jest.fn(); render( { }), ]} acceptedEvents={[]} + onTroubleshoot={onTroubleshoot} /> ); - // The fix-this affordance is hidden until the reason chevron is clicked. - expect(screen.queryByRole('button', {name: 'Learn more'})).not.toBeInTheDocument(); - - await userEvent.click(screen.getByRole('button', {name: 'Toggle fix options'})); - - expect(screen.getByRole('button', {name: 'Learn more'})).toBeInTheDocument(); - - // Collapsing hides it again. - await userEvent.click(screen.getByRole('button', {name: 'Toggle fix options'})); - expect(screen.queryByRole('button', {name: 'Learn more'})).not.toBeInTheDocument(); - - // The toggle is reachable from the keyboard. - act(() => screen.getByRole('button', {name: 'Toggle fix options'}).focus()); - await userEvent.keyboard('{Enter}'); - expect(screen.getByRole('button', {name: 'Learn more'})).toBeInTheDocument(); - }); - - it('shows the long description when a reason row is expanded', async () => { - render( - - ); - - const longDescription = - 'The event reached Sentry but was rejected with an error code.'; - expect(screen.queryByText(longDescription)).not.toBeInTheDocument(); - - await userEvent.click(screen.getByRole('button', {name: 'Toggle fix options'})); - - expect(screen.getByText(longDescription)).toBeInTheDocument(); - }); - - it('opens the fix-this menu with investigate, settings, and docs entries', async () => { - const onInvestigate = jest.fn(); - render( - - ); - - await userEvent.click(screen.getByRole('button', {name: 'Toggle fix options'})); - await userEvent.click(screen.getByRole('button', {name: 'Learn more'})); - - // Project settings deep-links to the usage stats page scoped to the - // reason's data category. - expect(screen.getByRole('menuitemradio', {name: 'Project Settings'})).toHaveAttribute( - 'href', - expect.stringContaining('/stats/?dataCategory=spans') - ); + await userEvent.click(screen.getByRole('button', {name: 'Reason actions'})); + expect(screen.getAllByRole('menuitemradio').map(item => item.textContent)).toEqual([ + 'Troubleshoot', + 'Go to Docs', + ]); expect(screen.getByRole('menuitemradio', {name: 'Go to Docs'})).toHaveAttribute( 'href', expect.stringContaining('docs.sentry.io') ); - await userEvent.click(screen.getByRole('menuitemradio', {name: 'Investigate'})); - expect(onInvestigate).toHaveBeenCalledWith( + await userEvent.click(screen.getByRole('menuitemradio', {name: 'Troubleshoot'})); + expect(onTroubleshoot).toHaveBeenCalledWith( expect.objectContaining({outcome: 'rate_limited', reason: 'smart_rate_limit'}) ); }); - it('hides investigate when there is no Seer hand-off', async () => { + it('hides troubleshoot when there is no Seer hand-off', async () => { render( { /> ); - await userEvent.click(screen.getByRole('button', {name: 'Toggle fix options'})); - await userEvent.click(screen.getByRole('button', {name: 'Learn more'})); + await userEvent.click(screen.getByRole('button', {name: 'Reason actions'})); + expect(screen.getByRole('menuitemradio', {name: 'Go to Docs'})).toBeInTheDocument(); expect( - screen.getByRole('menuitemradio', {name: 'Project Settings'}) - ).toBeInTheDocument(); - expect( - screen.queryByRole('menuitemradio', {name: 'Investigate'}) + screen.queryByRole('menuitemradio', {name: 'Troubleshoot'}) ).not.toBeInTheDocument(); }); }); diff --git a/static/app/components/droppedData/drawer/droppedDataOutcomeList.tsx b/static/app/components/droppedData/drawer/droppedDataOutcomeList.tsx index dac291468223..476c63b71a65 100644 --- a/static/app/components/droppedData/drawer/droppedDataOutcomeList.tsx +++ b/static/app/components/droppedData/drawer/droppedDataOutcomeList.tsx @@ -1,9 +1,11 @@ -import {Fragment, useId, useState} from 'react'; +import {useId, useState} from 'react'; import {IconChevron} from '@sentry/icons/chevron'; +import {IconEllipsis} from '@sentry/icons/ellipsis'; import {Button} from '@sentry/scraps/button'; import {useDrawer, useDrawerContentContext} from '@sentry/scraps/drawer'; import {DropdownMenu, type MenuItemProps} from '@sentry/scraps/dropdownMenu'; +import {InfoText} from '@sentry/scraps/info'; import {Flex, Grid, Stack} from '@sentry/scraps/layout'; import {OverlayTrigger} from '@sentry/scraps/overlayTrigger'; import type {TableColumnConfig} from '@sentry/scraps/table'; @@ -15,63 +17,47 @@ import type { ReasonRow, } from 'sentry/components/droppedData/drawer/outcomeSections'; import { - dataCategoryInfo, formatDroppedShare, reasonDescription, - reasonLongDescription, reasonTitle, type OutcomeColors, } from 'sentry/components/droppedData/outcomes'; import {SimpleTable} from 'sentry/components/tables/simpleTable'; import {t} from 'sentry/locale'; -import {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; -import {useOrganization} from 'sentry/utils/useOrganization'; -const COLUMN_WIDTHS = {reason: '1fr', dropped: '160px', share: '96px', toggle: '48px'}; +const COLUMN_WIDTHS = {reason: '1fr', dropped: '160px', share: '96px', actions: '48px'}; const TABLE_COLUMNS: TableColumnConfig[] = Object.entries(COLUMN_WIDTHS).map( ([key, width]) => ({key, width}) ); const HEADER_COLUMNS = Object.values(COLUMN_WIDTHS).join(' '); -function FixThisMenu({ +function ReasonActionsMenu({ row, - onInvestigate, + onTroubleshoot, }: { row: ReasonRow; - onInvestigate?: (row: ReasonRow) => void; + onTroubleshoot?: (row: ReasonRow) => void; }) { - const organization = useOrganization(); const {onClose: closeDroppedDataDrawer} = useDrawerContentContext(); const {panelRef} = useDrawer(); - const info = dataCategoryInfo(row.category); - - const settingsTo = normalizeUrl( - `/settings/${organization.slug}/stats/${info ? `?dataCategory=${info.plural}` : ''}` - ); - // TODO: this will be a dynamic URL once the per-reason docs page // fully up-to-date. const docsHref = 'https://docs.sentry.io/'; const items: MenuItemProps[] = [ - ...(onInvestigate + ...(onTroubleshoot ? [ { - key: 'investigate', - label: t('Investigate'), + key: 'troubleshoot', + label: t('Troubleshoot'), onAction: () => { closeDroppedDataDrawer?.(); - onInvestigate(row); + onTroubleshoot(row); }, }, ] : []), - { - key: 'project-settings', - label: t('Project Settings'), - to: settingsTo, - }, { key: 'docs', label: t('Go to Docs'), @@ -86,89 +72,47 @@ function FixThisMenu({ usePortal portalContainerRef={panelRef} trigger={(triggerProps, isOpen) => ( - } + aria-label={t('Reason actions')} aria-expanded={isOpen} - > - {t('Learn more')} - + /> )} /> ); } -function ReasonExpansion({ - row, - onInvestigate, -}: { - row: ReasonRow; - onInvestigate?: (row: ReasonRow) => void; -}) { - const longDescription = reasonLongDescription(row.reason); - - return ( - - - {longDescription && {longDescription}} - - - - ); -} - function ReasonTableRow({ row, - onInvestigate, + onTroubleshoot, }: { row: ReasonRow; - onInvestigate?: (row: ReasonRow) => void; + onTroubleshoot?: (row: ReasonRow) => void; }) { - const [expanded, setExpanded] = useState(false); - const description = reasonDescription(row.reason, row.category); - return ( - - - - {reasonTitle(row.reason)} - {description && ( - - {description} - - )} - - - - {row.events.toLocaleString()} - - - - - {formatDroppedShare(row.shareRatio)} - - - -