From cbdf9b91b005c058afdb010c2f2449827cf29b7f Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Fri, 20 Sep 2024 15:00:23 -0700 Subject: [PATCH] add drawer open to more places, project slug --- .../events/eventTagsAndScreenshot/tags.tsx | 17 +- .../highlights/highlightsDataSection.spec.tsx | 5 +- .../highlights/highlightsDataSection.tsx | 6 +- .../groupTags/groupTagsDrawer.tsx | 337 ++---------------- .../groupTags/groupTagsDrawerTagDetails.tsx | 299 ++++++++++++++++ .../groupTags/useGroupTagsDrawer.tsx | 53 +++ static/app/views/issueDetails/header.tsx | 16 - 7 files changed, 413 insertions(+), 320 deletions(-) create mode 100644 static/app/views/issueDetails/groupTags/groupTagsDrawerTagDetails.tsx create mode 100644 static/app/views/issueDetails/groupTags/useGroupTagsDrawer.tsx diff --git a/static/app/components/events/eventTagsAndScreenshot/tags.tsx b/static/app/components/events/eventTagsAndScreenshot/tags.tsx index d26aebc70715d..6306cd34903ac 100644 --- a/static/app/components/events/eventTagsAndScreenshot/tags.tsx +++ b/static/app/components/events/eventTagsAndScreenshot/tags.tsx @@ -1,6 +1,7 @@ -import {forwardRef, useCallback, useMemo, useState} from 'react'; +import {forwardRef, useCallback, useMemo, useRef, useState} from 'react'; import styled from '@emotion/styled'; +import {Button} from 'sentry/components/button'; import ButtonBar from 'sentry/components/buttonBar'; import { getSentryDefaultTags, @@ -14,8 +15,10 @@ import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Event} from 'sentry/types/event'; import type {Project} from 'sentry/types/project'; +import {useGroupTagsDrawer} from 'sentry/views/issueDetails/groupTags/groupTagsDrawer'; import {SectionKey} from 'sentry/views/issueDetails/streamline/context'; import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection'; +import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils'; import {EventTags} from '../eventTags'; @@ -27,6 +30,13 @@ type Props = { export const EventTagsDataSection = forwardRef( function EventTagsDataSection({event, projectSlug}: Props, ref) { const sentryTags = getSentryDefaultTags(); + const hasStreamlinedUI = useHasStreamlinedUI(); + const openButtonRef = useRef(null); + const {openTagsDrawer} = useGroupTagsDrawer({ + projectSlug: projectSlug, + groupId: event.groupID!, + openButtonRef: openButtonRef, + }); const [tagFilter, setTagFilter] = useState(TagFilter.ALL); const handleTagFilterChange = useCallback((value: TagFilter) => { @@ -51,6 +61,11 @@ export const EventTagsDataSection = forwardRef( const actions = ( + {hasStreamlinedUI && ( + + )} , {organization} ); @@ -87,7 +90,7 @@ describe('HighlightsDataSection', function () { body: {}, }); - render(, { + render(, { organization, }); expect(screen.getByText('Event Highlights')).toBeInTheDocument(); diff --git a/static/app/components/events/highlights/highlightsDataSection.tsx b/static/app/components/events/highlights/highlightsDataSection.tsx index acb0e23dbb626..0e58a7f15189a 100644 --- a/static/app/components/events/highlights/highlightsDataSection.tsx +++ b/static/app/components/events/highlights/highlightsDataSection.tsx @@ -261,7 +261,11 @@ export default function HighlightsDataSection({ const organization = useOrganization(); const hasStreamlinedUI = useHasStreamlinedUI(); const openButtonRef = useRef(null); - const {openTagsDrawer} = useGroupTagsDrawer({groupId, openButtonRef, project}); + const {openTagsDrawer} = useGroupTagsDrawer({ + groupId, + openButtonRef, + projectSlug: project.slug, + }); const viewAllButton = hasStreamlinedUI ? ( // Streamline details ui has "Jump to" feature, instead we'll show the drawer button diff --git a/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx b/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx index 40c7f0320bf23..504161ff6f2f7 100644 --- a/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx +++ b/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx @@ -1,15 +1,13 @@ -import {Fragment, useCallback, useRef} from 'react'; +import {useCallback, useRef} from 'react'; import styled from '@emotion/styled'; import Alert from 'sentry/components/alert'; import ProjectAvatar from 'sentry/components/avatar/projectAvatar'; import {Button, LinkButton} from 'sentry/components/button'; import ButtonBar from 'sentry/components/buttonBar'; -import {CompactSelect} from 'sentry/components/compactSelect'; import Count from 'sentry/components/count'; import DataExport, {ExportQueryType} from 'sentry/components/dataExport'; import {DeviceName} from 'sentry/components/deviceName'; -import {DropdownMenu} from 'sentry/components/dropdownMenu'; import { CrumbContainer, EventDrawerBody, @@ -22,246 +20,29 @@ import { } from 'sentry/components/events/eventReplay/eventDrawer'; import {TAGS_DOCS_LINK} from 'sentry/components/events/eventTags/util'; import useDrawer from 'sentry/components/globalDrawer'; -import GlobalSelectionLink from 'sentry/components/globalSelectionLink'; -import UserBadge from 'sentry/components/idBadge/userBadge'; import ExternalLink from 'sentry/components/links/externalLink'; import Link from 'sentry/components/links/link'; import LoadingError from 'sentry/components/loadingError'; import LoadingIndicator from 'sentry/components/loadingIndicator'; -import {extractSelectionParameters} from 'sentry/components/organizations/pageFilters/utils'; -import Pagination from 'sentry/components/pagination'; import Panel from 'sentry/components/panels/panel'; import PanelBody from 'sentry/components/panels/panelBody'; -import {PanelTable} from 'sentry/components/panels/panelTable'; -import TimeSince from 'sentry/components/timeSince'; import Version from 'sentry/components/version'; -import {IconArrow, IconEllipsis, IconMail, IconOpen} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; -import type {SavedQueryVersions} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import {percent} from 'sentry/utils'; -import EventView from 'sentry/utils/discover/eventView'; -import {SavedQueryDatasets} from 'sentry/utils/discover/types'; -import {isUrl} from 'sentry/utils/string/isUrl'; import {useLocation} from 'sentry/utils/useLocation'; import {useNavigate} from 'sentry/utils/useNavigate'; import useOrganization from 'sentry/utils/useOrganization'; -import {hasDatasetSelector} from 'sentry/views/dashboards/utils'; +import useProjects from 'sentry/utils/useProjects'; +import {GroupTagsDrawerTagDetails} from 'sentry/views/issueDetails/groupTags/groupTagsDrawerTagDetails'; import {useGroupTags} from 'sentry/views/issueDetails/groupTags/useGroupTags'; -import {useTagQueries} from 'sentry/views/issueDetails/groupTagValues'; -import {useEnvironmentsFromUrl} from 'sentry/views/issueDetails/utils'; -import {StyledExternalLink} from 'sentry/views/settings/organizationMembers/inviteBanner'; type GroupTagsDrawerProps = { groupId: string; project: Project; }; -interface GroupTagsDrawerTagDetailsProps extends GroupTagsDrawerProps { - drawerRef: React.RefObject; -} - -type TagSort = 'date' | 'count'; -const DEFAULT_SORT: TagSort = 'count'; - -function GroupTagsDrawerTagDetails({groupId, project, drawerRef}: GroupTagsDrawerProps) { - const location = useLocation(); - const organization = useOrganization(); - const tagKey = location.query.tagDrawerKey as string; - const environments = useEnvironmentsFromUrl(); - const {cursor, page: _page, ...currentQuery} = location.query; - - const title = tagKey === 'user' ? t('Affected Users') : tagKey; - const sort: TagSort = - (location.query.tagDrawerSort as TagSort | undefined) ?? DEFAULT_SORT; - const sortArrow = ; - - const {tagValueList, tag, isLoading, isError, pageLinks} = useTagQueries({ - groupId: groupId, - sort, - tagKey, - environments, - cursor: typeof cursor === 'string' ? cursor : undefined, - }); - - const lastSeenColumnHeader = ( - - {t('Last Seen')} {sort === 'date' && sortArrow} - - ); - const countColumnHeader = ( - - {t('Count')} {sort === 'count' && sortArrow} - - ); - const renderResults = () => { - if (isError) { - return ; - } - - if (isLoading) { - return null; - } - - const discoverFields = [ - 'title', - 'release', - 'environment', - 'user.display', - 'timestamp', - ]; - - const globalSelectionParams = extractSelectionParameters(location.query); - return tagValueList?.map((tagValue, tagValueIdx) => { - const pct = tag?.totalValues - ? `${percent(tagValue.count, tag?.totalValues).toFixed(2)}%` - : '--'; - const key = tagValue.key ?? tagKey; - const issuesQuery = tagValue.query || `${key}:"${tagValue.value}"`; - const discoverView = EventView.fromSavedQuery({ - id: undefined, - name: key ?? '', - fields: [ - ...(key !== undefined ? [key] : []), - ...discoverFields.filter(field => field !== key), - ], - orderby: '-timestamp', - // query: `issue:${group.shortId} ${issuesQuery}`, - projects: [Number(project?.id)], - environment: environments, - version: 2 as SavedQueryVersions, - range: '90d', - }); - const issuesPath = `/organizations/${organization.slug}/issues/`; - - return ( - - - - - {key === 'user' ? ( - - ) : ( - - )} - - - - {tagValue.email && ( - - - - )} - {isUrl(tagValue.value) && ( - - - - )} - - {pct} - {tagValue.count.toLocaleString()} - - - - - , - 'aria-label': t('More'), - }} - usePortal - portalContainerRef={drawerRef} - items={[ - { - key: 'open-in-discover', - label: t('Open in Discover'), - to: discoverView.getResultsViewUrlTarget( - organization.slug, - false, - hasDatasetSelector(organization) - ? SavedQueryDatasets.ERRORS - : undefined - ), - hidden: !organization.features.includes('discover-basic'), - }, - { - key: 'search-issues', - label: t('Search All Issues with Tag Value'), - to: { - pathname: issuesPath, - query: { - ...globalSelectionParams, // preserve page filter selections - query: issuesQuery, - }, - }, - }, - ]} - /> - - - ); - }); - }; - - return ( - - {t('Percent')}, - countColumnHeader, - lastSeenColumnHeader, - '', - ]} - emptyMessage={t('Sorry, the tags for this issue could not be found.')} - emptyAction={ - environments?.length - ? t('No tags were found for the currently selected environments') - : null - } - > - {renderResults()} - - - - ); -} - export function GroupTagsDrawer({project, groupId}: GroupTagsDrawerProps) { const location = useLocation(); const organization = useOrganization(); @@ -326,27 +107,25 @@ export function GroupTagsDrawer({project, groupId}: GroupTagsDrawerProps) {
{tagDrawerKey ? t('Tag Details') : t('Tags')}
{tagDrawerKey && ( - - - - {t('Export Page to CSV')} - - - - + + + {t('Export Page to CSV')} + + + )}
@@ -436,23 +215,30 @@ export function GroupTagsDrawer({project, groupId}: GroupTagsDrawerProps) { } export function useGroupTagsDrawer({ - project, + projectSlug, groupId, openButtonRef, }: { groupId: string; openButtonRef: React.RefObject; - project: Project; + projectSlug: Project['slug']; }) { const location = useLocation(); const navigate = useNavigate(); const drawer = useDrawer(); + const {projects} = useProjects({slugs: [projectSlug]}); + const project = projects.find(p => p.slug === projectSlug); const openTagsDrawer = useCallback(() => { + if (!project) { + return; + } + drawer.openDrawer(() => , { ariaLabel: 'tags drawer', onClose: () => { - if (location.query.tagDrawerSort || location.query.tagDrawerKey) { + const params = new URL(window.location.href).searchParams; + if (params.has('tagDrawerSort') || params.has('tagDrawerKey')) { // Remove drawer state from URL navigate( { @@ -477,6 +263,10 @@ export function useGroupTagsDrawer({ }); }, [location, navigate, drawer, project, groupId, openButtonRef]); + if (!project) { + return {}; + } + return {openTagsDrawer}; } @@ -563,58 +353,3 @@ const TagBarCount = styled('div')` padding-right: ${space(1)}; font-variant-numeric: tabular-nums; `; - -const StyledPanelTable = styled(PanelTable)` - white-space: nowrap; - font-size: ${p => p.theme.fontSizeMedium}; - - overflow: auto; - - & > * { - padding: ${space(1)} ${space(2)}; - } -`; - -const StyledLoadingError = styled(LoadingError)` - grid-column: 1 / -1; - margin-bottom: ${space(4)}; - border-radius: 0; - border-width: 1px 0; -`; - -const PercentColumnHeader = styled('div')` - text-align: right; -`; - -const StyledSortLink = styled(Link)` - text-align: right; - color: inherit; - - :hover { - color: inherit; - } -`; - -const Column = styled('div')` - display: flex; - align-items: center; -`; - -const NameColumn = styled(Column)` - ${p => p.theme.overflowEllipsis}; - display: flex; - min-width: 320px; -`; - -const NameWrapper = styled('span')` - ${p => p.theme.overflowEllipsis}; - width: auto; -`; - -const RightAlignColumn = styled(Column)` - justify-content: flex-end; -`; - -const StyledPagination = styled(Pagination)` - margin: 0; -`; diff --git a/static/app/views/issueDetails/groupTags/groupTagsDrawerTagDetails.tsx b/static/app/views/issueDetails/groupTags/groupTagsDrawerTagDetails.tsx new file mode 100644 index 0000000000000..d752896684b1f --- /dev/null +++ b/static/app/views/issueDetails/groupTags/groupTagsDrawerTagDetails.tsx @@ -0,0 +1,299 @@ +import {Fragment} from 'react'; +import styled from '@emotion/styled'; + +import {DeviceName} from 'sentry/components/deviceName'; +import {DropdownMenu} from 'sentry/components/dropdownMenu'; +import GlobalSelectionLink from 'sentry/components/globalSelectionLink'; +import UserBadge from 'sentry/components/idBadge/userBadge'; +import Link from 'sentry/components/links/link'; +import LoadingError from 'sentry/components/loadingError'; +import {extractSelectionParameters} from 'sentry/components/organizations/pageFilters/utils'; +import Pagination from 'sentry/components/pagination'; +import {PanelTable} from 'sentry/components/panels/panelTable'; +import TimeSince from 'sentry/components/timeSince'; +import {IconArrow, IconEllipsis, IconMail, IconOpen} from 'sentry/icons'; +import {t} from 'sentry/locale'; +import {space} from 'sentry/styles/space'; +import type {SavedQueryVersions} from 'sentry/types/organization'; +import type {Project} from 'sentry/types/project'; +import {percent} from 'sentry/utils'; +import EventView from 'sentry/utils/discover/eventView'; +import {SavedQueryDatasets} from 'sentry/utils/discover/types'; +import {isUrl} from 'sentry/utils/string/isUrl'; +import {useLocation} from 'sentry/utils/useLocation'; +import useOrganization from 'sentry/utils/useOrganization'; +import {hasDatasetSelector} from 'sentry/views/dashboards/utils'; +import {useTagQueries} from 'sentry/views/issueDetails/groupTagValues'; +import {useEnvironmentsFromUrl} from 'sentry/views/issueDetails/utils'; +import {StyledExternalLink} from 'sentry/views/settings/organizationMembers/inviteBanner'; + +type GroupTagsDrawerProps = { + groupId: string; + project: Project; +}; + +interface GroupTagsDrawerTagDetailsProps extends GroupTagsDrawerProps { + /** + * Helps dropdowns append to the correct element + */ + drawerRef: React.RefObject; +} + +type TagSort = 'date' | 'count'; +const DEFAULT_SORT: TagSort = 'count'; + +export function GroupTagsDrawerTagDetails({ + groupId, + project, + drawerRef, +}: GroupTagsDrawerTagDetailsProps) { + const location = useLocation(); + const organization = useOrganization(); + const tagKey = location.query.tagDrawerKey as string; + const environments = useEnvironmentsFromUrl(); + const {cursor, page: _page, ...currentQuery} = location.query; + + const title = tagKey === 'user' ? t('Affected Users') : tagKey; + const sort: TagSort = + (location.query.tagDrawerSort as TagSort | undefined) ?? DEFAULT_SORT; + const sortArrow = ; + + const {tagValueList, tag, isLoading, isError, pageLinks} = useTagQueries({ + groupId: groupId, + sort, + tagKey, + environments, + cursor: typeof cursor === 'string' ? cursor : undefined, + }); + + const lastSeenColumnHeader = ( + + {t('Last Seen')} {sort === 'date' && sortArrow} + + ); + const countColumnHeader = ( + + {t('Count')} {sort === 'count' && sortArrow} + + ); + const renderResults = () => { + if (isError) { + return ; + } + + if (isLoading) { + return null; + } + + const discoverFields = [ + 'title', + 'release', + 'environment', + 'user.display', + 'timestamp', + ]; + + const globalSelectionParams = extractSelectionParameters(location.query); + return tagValueList?.map((tagValue, tagValueIdx) => { + const pct = tag?.totalValues + ? `${percent(tagValue.count, tag?.totalValues).toFixed(2)}%` + : '--'; + const key = tagValue.key ?? tagKey; + const issuesQuery = tagValue.query || `${key}:"${tagValue.value}"`; + const discoverView = EventView.fromSavedQuery({ + id: undefined, + name: key ?? '', + fields: [ + ...(key !== undefined ? [key] : []), + ...discoverFields.filter(field => field !== key), + ], + orderby: '-timestamp', + // query: `issue:${group.shortId} ${issuesQuery}`, + projects: [Number(project?.id)], + environment: environments, + version: 2 as SavedQueryVersions, + range: '90d', + }); + const issuesPath = `/organizations/${organization.slug}/issues/`; + + return ( + + + + + {key === 'user' ? ( + + ) : ( + + )} + + + + {tagValue.email && ( + + + + )} + {isUrl(tagValue.value) && ( + + + + )} + + {pct} + {tagValue.count.toLocaleString()} + + + + + , + 'aria-label': t('More'), + }} + usePortal + portalContainerRef={drawerRef} + items={[ + { + key: 'open-in-discover', + label: t('Open in Discover'), + to: discoverView.getResultsViewUrlTarget( + organization.slug, + false, + hasDatasetSelector(organization) + ? SavedQueryDatasets.ERRORS + : undefined + ), + hidden: !organization.features.includes('discover-basic'), + }, + { + key: 'search-issues', + label: t('Search All Issues with Tag Value'), + to: { + pathname: issuesPath, + query: { + ...globalSelectionParams, // preserve page filter selections + query: issuesQuery, + }, + }, + }, + ]} + /> + + + ); + }); + }; + + return ( + + {t('Percent')}, + countColumnHeader, + lastSeenColumnHeader, + '', + ]} + emptyMessage={t('Sorry, the tags for this issue could not be found.')} + emptyAction={ + environments?.length + ? t('No tags were found for the currently selected environments') + : null + } + > + {renderResults()} + + + + ); +} + +const StyledPanelTable = styled(PanelTable)` + white-space: nowrap; + font-size: ${p => p.theme.fontSizeMedium}; + + overflow: auto; + + & > * { + padding: ${space(1)} ${space(2)}; + } +`; + +const StyledLoadingError = styled(LoadingError)` + grid-column: 1 / -1; + margin-bottom: ${space(4)}; + border-radius: 0; + border-width: 1px 0; +`; + +const PercentColumnHeader = styled('div')` + text-align: right; +`; + +const StyledSortLink = styled(Link)` + text-align: right; + color: inherit; + + :hover { + color: inherit; + } +`; + +const Column = styled('div')` + display: flex; + align-items: center; +`; + +const NameColumn = styled(Column)` + ${p => p.theme.overflowEllipsis}; + display: flex; + min-width: 320px; +`; + +const NameWrapper = styled('span')` + ${p => p.theme.overflowEllipsis}; + width: auto; +`; + +const RightAlignColumn = styled(Column)` + justify-content: flex-end; +`; + +const StyledPagination = styled(Pagination)` + margin: 0; +`; diff --git a/static/app/views/issueDetails/groupTags/useGroupTagsDrawer.tsx b/static/app/views/issueDetails/groupTags/useGroupTagsDrawer.tsx new file mode 100644 index 0000000000000..ccc85e8229c7e --- /dev/null +++ b/static/app/views/issueDetails/groupTags/useGroupTagsDrawer.tsx @@ -0,0 +1,53 @@ +import {useCallback} from 'react'; + +import useDrawer from 'sentry/components/globalDrawer'; +import type {Project} from 'sentry/types/project'; +import {useLocation} from 'sentry/utils/useLocation'; +import {useNavigate} from 'sentry/utils/useNavigate'; +import {GroupTagsDrawer} from 'sentry/views/issueDetails/groupTags/groupTagsDrawer'; + +export function useGroupTagsDrawer({ + project, + groupId, + openButtonRef, +}: { + groupId: string; + openButtonRef: React.RefObject; + project: Project; +}) { + const location = useLocation(); + const navigate = useNavigate(); + const drawer = useDrawer(); + + const openTagsDrawer = useCallback(() => { + drawer.openDrawer(() => , { + ariaLabel: 'tags drawer', + onClose: () => { + const params = new URL(window.location.href).searchParams; + if (params.has('tagDrawerSort') || params.has('tagDrawerKey')) { + // Remove drawer state from URL + navigate( + { + pathname: location.pathname, + query: { + ...location.query, + tagDrawerSort: undefined, + tagDrawerKey: undefined, + }, + }, + {replace: true} + ); + } + }, + shouldCloseOnInteractOutside: element => { + const viewAllButton = openButtonRef.current; + if (viewAllButton?.contains(element)) { + return false; + } + return true; + }, + }); + }, [location, navigate, drawer, project, groupId, openButtonRef]); + + return {openTagsDrawer}; +} diff --git a/static/app/views/issueDetails/header.tsx b/static/app/views/issueDetails/header.tsx index 15303e328405b..f163af0d61a3f 100644 --- a/static/app/views/issueDetails/header.tsx +++ b/static/app/views/issueDetails/header.tsx @@ -125,22 +125,6 @@ export function GroupHeaderTabs({ > {t('User Feedback')} - -