import {Fragment} from 'react'; import styled from '@emotion/styled'; import AvatarList from 'sentry/components/avatar/avatarList'; import {DateTime} from 'sentry/components/dateTime'; import type {OnAssignCallback} from 'sentry/components/deprecatedAssigneeSelectorDropdown'; import ErrorBoundary from 'sentry/components/errorBoundary'; import {EventThroughput} from 'sentry/components/events/eventStatisticalDetector/eventThroughput'; import AssignedTo from 'sentry/components/group/assignedTo'; import ExternalIssueList from 'sentry/components/group/externalIssuesList'; import GroupReleaseStats from 'sentry/components/group/releaseStats'; import TagFacets, { BACKEND_TAGS, DEFAULT_TAGS, FRONTEND_TAGS, MOBILE_TAGS, TAGS_FORMATTER, } from 'sentry/components/group/tagFacets'; import QuestionTooltip from 'sentry/components/questionTooltip'; import * as SidebarSection from 'sentry/components/sidebarSection'; import {backend, frontend} from 'sentry/data/platformCategories'; import {t, tn} from 'sentry/locale'; import ConfigStore from 'sentry/stores/configStore'; import IssueListCacheStore from 'sentry/stores/IssueListCacheStore'; import {space} from 'sentry/styles/space'; import type { AvatarUser, CurrentRelease, Group, Organization, OrganizationSummary, Project, TeamParticipant, UserParticipant, } from 'sentry/types'; import {IssueType} from 'sentry/types'; import type {Event} from 'sentry/types/event'; import {trackAnalytics} from 'sentry/utils/analytics'; import {getUtcDateString} from 'sentry/utils/dates'; import {getAnalyticsDataForGroup} from 'sentry/utils/events'; import {userDisplayName} from 'sentry/utils/formatters'; import {getConfigForIssueType} from 'sentry/utils/issueTypeConfig'; import {isMobilePlatform} from 'sentry/utils/platform'; import {getAnalyicsDataForProject} from 'sentry/utils/projects'; import {useApiQuery} from 'sentry/utils/queryClient'; import {useLocation} from 'sentry/utils/useLocation'; import { getGroupDetailsQueryData, useHasStreamlinedUI, } from 'sentry/views/issueDetails/utils'; import {ParticipantList} from './participantList'; type Props = { environments: string[]; group: Group; organization: Organization; project: Project; event?: Event; }; function useFetchAllEnvsGroupData(organization: OrganizationSummary, group: Group) { return useApiQuery( [ `/organizations/${organization.slug}/issues/${group.id}/`, {query: getGroupDetailsQueryData()}, ], { staleTime: 30000, cacheTime: 30000, } ); } function useFetchCurrentRelease(organization: OrganizationSummary, group: Group) { return useApiQuery( [`/organizations/${organization.slug}/issues/${group.id}/current-release/`], { staleTime: 30000, cacheTime: 30000, } ); } export default function GroupSidebar({ event, group, project, organization, environments, }: Props) { const {data: allEnvironmentsGroupData} = useFetchAllEnvsGroupData(organization, group); const {data: currentRelease} = useFetchCurrentRelease(organization, group); const hasStreamlinedUI = useHasStreamlinedUI(); const location = useLocation(); const onAssign: OnAssignCallback = (type, _assignee, suggestedAssignee) => { const {alert_date, alert_rule_id, alert_type} = location.query; trackAnalytics('issue_details.action_clicked', { organization, action_type: 'assign', assigned_type: type, assigned_suggestion_reason: suggestedAssignee?.suggestedReason, alert_date: typeof alert_date === 'string' ? getUtcDateString(Number(alert_date)) : undefined, alert_rule_id: typeof alert_rule_id === 'string' ? alert_rule_id : undefined, alert_type: typeof alert_type === 'string' ? alert_type : undefined, ...getAnalyticsDataForGroup(group), ...getAnalyicsDataForProject(project), }); IssueListCacheStore.reset(); }; const renderPluginIssue = () => { const issues: React.ReactNode[] = []; (group.pluginIssues || []).forEach(plugin => { const issue = plugin.issue; // # TODO(dcramer): remove plugin.title check in Sentry 8.22+ if (issue) { issues.push( {`${plugin.shortName || plugin.name || plugin.title}: `} {typeof issue.label === 'object' ? issue.label.id : issue.label} ); } }); if (!issues.length) { return null; } return ( {t('External Issues')} {issues} ); }; const renderParticipantData = () => { const {participants} = group; if (!participants.length) { return null; } const userParticipants = participants.filter( (p): p is UserParticipant => p.type === 'user' ); const teamParticipants = participants.filter( (p): p is TeamParticipant => p.type === 'team' ); const getParticipantTitle = (): React.ReactNode => { const individualText = tn( '%s Individual', '%s Individuals', userParticipants.length ); const teamText = tn('%s Team', '%s Teams', teamParticipants.length); if (teamParticipants.length === 0) { return individualText; } if (userParticipants.length === 0) { return teamText; } return ( {teamText}, {individualText} ); }; const avatars = ( ); return ( {t('Participants')} ({getParticipantTitle()}) {avatars} ); }; const renderSeenByList = () => { const {seenBy} = group; const activeUser = ConfigStore.get('user'); const displayUsers = seenBy.filter(user => activeUser.id !== user.id); if (!displayUsers.length) { return null; } const avatars = ( ( {userDisplayName(user)}
)} /> ); return ( {t('Viewers')} ({displayUsers.length}) {avatars} ); }; const issueTypeConfig = getConfigForIssueType(group, project); return ( {!hasStreamlinedUI && ( )} {issueTypeConfig.stats.enabled && ( )} {event && ( )} {renderPluginIssue()} {issueTypeConfig.tags.enabled && ( val === project?.platform) ? FRONTEND_TAGS : backend.some(val => val === project?.platform) ? BACKEND_TAGS : DEFAULT_TAGS } event={event} tagFormatter={TAGS_FORMATTER} project={project} isStatisticalDetector={ group.issueType === IssueType.PERFORMANCE_DURATION_REGRESSION || group.issueType === IssueType.PERFORMANCE_ENDPOINT_REGRESSION } /> )} {issueTypeConfig.regression.enabled && event && ( )} {!hasStreamlinedUI && renderParticipantData()} {!hasStreamlinedUI && renderSeenByList()} ); } const Container = styled('div')` font-size: ${p => p.theme.fontSizeMedium}; `; const ExternalIssues = styled('div')` display: grid; grid-template-columns: auto max-content; gap: ${space(2)}; `; const StyledAvatarList = styled(AvatarList)` justify-content: flex-end; padding-left: ${space(0.75)}; `; const TitleNumber = styled('span')` font-weight: ${p => p.theme.fontWeightNormal}; `; // Using 22px + space(1) = space(4) const SmallerSidebarWrap = styled(SidebarSection.Wrap)` margin-bottom: 22px; `;