groupSidebar.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import isObject from 'lodash/isObject';
  4. import type {OnAssignCallback} from 'sentry/components/assigneeSelectorDropdown';
  5. import AvatarList from 'sentry/components/avatar/avatarList';
  6. import DateTime from 'sentry/components/dateTime';
  7. import ErrorBoundary from 'sentry/components/errorBoundary';
  8. import AssignedTo from 'sentry/components/group/assignedTo';
  9. import ExternalIssueList from 'sentry/components/group/externalIssuesList';
  10. import GroupReleaseStats from 'sentry/components/group/releaseStats';
  11. import TagFacets, {
  12. BACKEND_TAGS,
  13. DEFAULT_TAGS,
  14. FRONTEND_TAGS,
  15. MOBILE_TAGS,
  16. TAGS_FORMATTER,
  17. } from 'sentry/components/group/tagFacets';
  18. import QuestionTooltip from 'sentry/components/questionTooltip';
  19. import * as SidebarSection from 'sentry/components/sidebarSection';
  20. import {backend, frontend} from 'sentry/data/platformCategories';
  21. import {t} from 'sentry/locale';
  22. import ConfigStore from 'sentry/stores/configStore';
  23. import {space} from 'sentry/styles/space';
  24. import {AvatarUser, CurrentRelease, Group, Organization, Project} from 'sentry/types';
  25. import {Event} from 'sentry/types/event';
  26. import {trackAnalytics} from 'sentry/utils/analytics';
  27. import {getUtcDateString} from 'sentry/utils/dates';
  28. import {getAnalyticsDataForGroup} from 'sentry/utils/events';
  29. import {userDisplayName} from 'sentry/utils/formatters';
  30. import {isMobilePlatform} from 'sentry/utils/platform';
  31. import {useApiQuery} from 'sentry/utils/queryClient';
  32. import {useLocation} from 'sentry/utils/useLocation';
  33. import {getGroupDetailsQueryData} from 'sentry/views/issueDetails/utils';
  34. type Props = {
  35. environments: string[];
  36. group: Group;
  37. organization: Organization;
  38. project: Project;
  39. event?: Event;
  40. };
  41. function useFetchAllEnvsGroupData(group: Group) {
  42. return useApiQuery<Group>(
  43. [`/issues/${group.id}/`, {query: getGroupDetailsQueryData()}],
  44. {
  45. staleTime: 30000,
  46. cacheTime: 30000,
  47. }
  48. );
  49. }
  50. function useFetchCurrentRelease(group: Group) {
  51. return useApiQuery<CurrentRelease>([`/issues/${group.id}/current-release/`], {
  52. staleTime: 30000,
  53. cacheTime: 30000,
  54. });
  55. }
  56. export default function GroupSidebar({
  57. event,
  58. group,
  59. project,
  60. organization,
  61. environments,
  62. }: Props) {
  63. const {data: allEnvironmentsGroupData} = useFetchAllEnvsGroupData(group);
  64. const {data: currentRelease} = useFetchCurrentRelease(group);
  65. const location = useLocation();
  66. const trackAssign: OnAssignCallback = (type, _assignee, suggestedAssignee) => {
  67. const {alert_date, alert_rule_id, alert_type} = location.query;
  68. trackAnalytics('issue_details.action_clicked', {
  69. organization,
  70. project_id: parseInt(project.id, 10),
  71. action_type: 'assign',
  72. assigned_type: type,
  73. assigned_suggestion_reason: suggestedAssignee?.suggestedReason,
  74. alert_date:
  75. typeof alert_date === 'string' ? getUtcDateString(Number(alert_date)) : undefined,
  76. alert_rule_id: typeof alert_rule_id === 'string' ? alert_rule_id : undefined,
  77. alert_type: typeof alert_type === 'string' ? alert_type : undefined,
  78. ...getAnalyticsDataForGroup(group),
  79. });
  80. };
  81. const renderPluginIssue = () => {
  82. const issues: React.ReactNode[] = [];
  83. (group.pluginIssues || []).forEach(plugin => {
  84. const issue = plugin.issue;
  85. // # TODO(dcramer): remove plugin.title check in Sentry 8.22+
  86. if (issue) {
  87. issues.push(
  88. <Fragment key={plugin.slug}>
  89. <span>{`${plugin.shortName || plugin.name || plugin.title}: `}</span>
  90. <a href={issue.url}>{isObject(issue.label) ? issue.label.id : issue.label}</a>
  91. </Fragment>
  92. );
  93. }
  94. });
  95. if (!issues.length) {
  96. return null;
  97. }
  98. return (
  99. <SidebarSection.Wrap>
  100. <SidebarSection.Title>{t('External Issues')}</SidebarSection.Title>
  101. <SidebarSection.Content>
  102. <ExternalIssues>{issues}</ExternalIssues>
  103. </SidebarSection.Content>
  104. </SidebarSection.Wrap>
  105. );
  106. };
  107. const renderParticipantData = () => {
  108. const {participants} = group;
  109. if (!participants.length) {
  110. return null;
  111. }
  112. return (
  113. <SidebarSection.Wrap>
  114. <SidebarSection.Title>
  115. {t('Participants (%s)', participants.length)}
  116. <QuestionTooltip
  117. size="xs"
  118. position="top"
  119. title={t('People who have resolved, ignored, or added a comment')}
  120. />
  121. </SidebarSection.Title>
  122. <SidebarSection.Content>
  123. <StyledAvatarList users={participants} avatarSize={28} maxVisibleAvatars={13} />
  124. </SidebarSection.Content>
  125. </SidebarSection.Wrap>
  126. );
  127. };
  128. const renderSeenByList = () => {
  129. const {seenBy} = group;
  130. const activeUser = ConfigStore.get('user');
  131. const displayUsers = seenBy.filter(user => activeUser.id !== user.id);
  132. if (!displayUsers.length) {
  133. return null;
  134. }
  135. return (
  136. <SidebarSection.Wrap>
  137. <SidebarSection.Title>
  138. {t('Viewers (%s)', displayUsers.length)}{' '}
  139. <QuestionTooltip
  140. size="xs"
  141. position="top"
  142. title={t('People who have viewed this issue')}
  143. />
  144. </SidebarSection.Title>
  145. <SidebarSection.Content>
  146. <StyledAvatarList
  147. users={displayUsers}
  148. avatarSize={28}
  149. maxVisibleAvatars={13}
  150. renderTooltip={user => (
  151. <Fragment>
  152. {userDisplayName(user)}
  153. <br />
  154. <DateTime date={(user as AvatarUser).lastSeen} />
  155. </Fragment>
  156. )}
  157. />
  158. </SidebarSection.Content>
  159. </SidebarSection.Wrap>
  160. );
  161. };
  162. return (
  163. <Container>
  164. <AssignedTo group={group} event={event} project={project} onAssign={trackAssign} />
  165. <GroupReleaseStats
  166. organization={organization}
  167. project={project}
  168. environments={environments}
  169. allEnvironments={allEnvironmentsGroupData}
  170. group={group}
  171. currentRelease={currentRelease}
  172. />
  173. {event && (
  174. <ErrorBoundary mini>
  175. <ExternalIssueList project={project} group={group} event={event} />
  176. </ErrorBoundary>
  177. )}
  178. {renderPluginIssue()}
  179. <TagFacets
  180. environments={environments}
  181. groupId={group.id}
  182. tagKeys={
  183. isMobilePlatform(project?.platform)
  184. ? !organization.features.includes('device-classification')
  185. ? MOBILE_TAGS.filter(tag => tag !== 'device.class')
  186. : MOBILE_TAGS
  187. : frontend.some(val => val === project?.platform)
  188. ? FRONTEND_TAGS
  189. : backend.some(val => val === project?.platform)
  190. ? BACKEND_TAGS
  191. : DEFAULT_TAGS
  192. }
  193. event={event}
  194. tagFormatter={TAGS_FORMATTER}
  195. project={project}
  196. />
  197. {renderParticipantData()}
  198. {renderSeenByList()}
  199. </Container>
  200. );
  201. }
  202. const Container = styled('div')`
  203. font-size: ${p => p.theme.fontSizeMedium};
  204. `;
  205. const ExternalIssues = styled('div')`
  206. display: grid;
  207. grid-template-columns: auto max-content;
  208. gap: ${space(2)};
  209. `;
  210. const StyledAvatarList = styled(AvatarList)`
  211. justify-content: flex-end;
  212. padding-left: ${space(0.75)};
  213. `;