sidebar.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. import {Component, Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import isEqual from 'lodash/isEqual';
  4. import isObject from 'lodash/isObject';
  5. import keyBy from 'lodash/keyBy';
  6. import pickBy from 'lodash/pickBy';
  7. import {Client} from 'sentry/api';
  8. import EnvironmentPageFilter from 'sentry/components/environmentPageFilter';
  9. import ErrorBoundary from 'sentry/components/errorBoundary';
  10. import ExternalIssueList from 'sentry/components/group/externalIssuesList';
  11. import GroupParticipants from 'sentry/components/group/participants';
  12. import GroupReleaseStats from 'sentry/components/group/releaseStats';
  13. import SuggestedOwners from 'sentry/components/group/suggestedOwners/suggestedOwners';
  14. import GroupTagDistributionMeter from 'sentry/components/group/tagDistributionMeter';
  15. import LoadingError from 'sentry/components/loadingError';
  16. import Placeholder from 'sentry/components/placeholder';
  17. import {t} from 'sentry/locale';
  18. import space from 'sentry/styles/space';
  19. import {
  20. CurrentRelease,
  21. Environment,
  22. Group,
  23. Organization,
  24. Project,
  25. TagWithTopValues,
  26. } from 'sentry/types';
  27. import {Event} from 'sentry/types/event';
  28. import withApi from 'sentry/utils/withApi';
  29. import SidebarSection from './sidebarSection';
  30. type Props = {
  31. api: Client;
  32. environments: Environment[];
  33. group: Group;
  34. organization: Organization;
  35. project: Project;
  36. event?: Event;
  37. };
  38. type State = {
  39. environments: Environment[];
  40. participants: Group['participants'];
  41. allEnvironmentsGroupData?: Group;
  42. currentRelease?: CurrentRelease;
  43. error?: boolean;
  44. tagsWithTopValues?: Record<string, TagWithTopValues>;
  45. };
  46. class BaseGroupSidebar extends Component<Props, State> {
  47. state: State = {
  48. participants: [],
  49. environments: this.props.environments,
  50. };
  51. componentDidMount() {
  52. this.fetchAllEnvironmentsGroupData();
  53. this.fetchCurrentRelease();
  54. this.fetchParticipants();
  55. this.fetchTagData();
  56. }
  57. UNSAFE_componentWillReceiveProps(nextProps: Props) {
  58. if (!isEqual(nextProps.environments, this.props.environments)) {
  59. this.setState({environments: nextProps.environments}, this.fetchTagData);
  60. }
  61. }
  62. async fetchAllEnvironmentsGroupData() {
  63. const {group, api} = this.props;
  64. // Fetch group data for all environments since the one passed in props is filtered for the selected environment
  65. // The charts rely on having all environment data as well as the data for the selected env
  66. try {
  67. const query = {collapse: 'release'};
  68. const allEnvironmentsGroupData = await api.requestPromise(`/issues/${group.id}/`, {
  69. query,
  70. });
  71. // eslint-disable-next-line react/no-did-mount-set-state
  72. this.setState({allEnvironmentsGroupData});
  73. } catch {
  74. // eslint-disable-next-line react/no-did-mount-set-state
  75. this.setState({error: true});
  76. }
  77. }
  78. async fetchCurrentRelease() {
  79. const {group, api} = this.props;
  80. try {
  81. const {currentRelease} = await api.requestPromise(
  82. `/issues/${group.id}/current-release/`
  83. );
  84. this.setState({currentRelease});
  85. } catch {
  86. this.setState({error: true});
  87. }
  88. }
  89. async fetchParticipants() {
  90. const {group, api} = this.props;
  91. try {
  92. const participants = await api.requestPromise(`/issues/${group.id}/participants/`);
  93. this.setState({
  94. participants,
  95. error: false,
  96. });
  97. return participants;
  98. } catch {
  99. this.setState({
  100. error: true,
  101. });
  102. return [];
  103. }
  104. }
  105. async fetchTagData() {
  106. const {api, group} = this.props;
  107. try {
  108. // Fetch the top values for the current group's top tags.
  109. const data = await api.requestPromise(`/issues/${group.id}/tags/`, {
  110. query: pickBy({
  111. key: group.tags.map(tag => tag.key),
  112. environment: this.state.environments.map(env => env.name),
  113. }),
  114. });
  115. this.setState({tagsWithTopValues: keyBy(data, 'key')});
  116. } catch {
  117. this.setState({
  118. tagsWithTopValues: {},
  119. error: true,
  120. });
  121. }
  122. }
  123. renderPluginIssue() {
  124. const issues: React.ReactNode[] = [];
  125. (this.props.group.pluginIssues || []).forEach(plugin => {
  126. const issue = plugin.issue;
  127. // # TODO(dcramer): remove plugin.title check in Sentry 8.22+
  128. if (issue) {
  129. issues.push(
  130. <Fragment key={plugin.slug}>
  131. <span>{`${plugin.shortName || plugin.name || plugin.title}: `}</span>
  132. <a href={issue.url}>{isObject(issue.label) ? issue.label.id : issue.label}</a>
  133. </Fragment>
  134. );
  135. }
  136. });
  137. if (!issues.length) {
  138. return null;
  139. }
  140. return (
  141. <SidebarSection title={t('External Issues')}>
  142. <ExternalIssues>{issues}</ExternalIssues>
  143. </SidebarSection>
  144. );
  145. }
  146. renderParticipantData() {
  147. const {error, participants = []} = this.state;
  148. if (error) {
  149. return (
  150. <LoadingError
  151. message={t('There was an error while trying to load participants.')}
  152. />
  153. );
  154. }
  155. return participants.length !== 0 && <GroupParticipants participants={participants} />;
  156. }
  157. render() {
  158. const {event, group, organization, project, environments} = this.props;
  159. const {allEnvironmentsGroupData, currentRelease, tagsWithTopValues} = this.state;
  160. const projectId = project.slug;
  161. return (
  162. <Container>
  163. <PageFiltersContainer>
  164. <EnvironmentPageFilter alignDropdown="right" />
  165. </PageFiltersContainer>
  166. {event && <SuggestedOwners project={project} group={group} event={event} />}
  167. <GroupReleaseStats
  168. organization={organization}
  169. project={project}
  170. environments={environments}
  171. allEnvironments={allEnvironmentsGroupData}
  172. group={group}
  173. currentRelease={currentRelease}
  174. />
  175. {event && (
  176. <ErrorBoundary mini>
  177. <ExternalIssueList project={project} group={group} event={event} />
  178. </ErrorBoundary>
  179. )}
  180. {this.renderPluginIssue()}
  181. <SidebarSection title={t('Tags')}>
  182. {!tagsWithTopValues ? (
  183. <TagPlaceholders>
  184. <Placeholder height="40px" />
  185. <Placeholder height="40px" />
  186. <Placeholder height="40px" />
  187. <Placeholder height="40px" />
  188. </TagPlaceholders>
  189. ) : (
  190. group.tags.map(tag => {
  191. const tagWithTopValues = tagsWithTopValues[tag.key];
  192. const topValues = tagWithTopValues ? tagWithTopValues.topValues : [];
  193. const topValuesTotal = tagWithTopValues ? tagWithTopValues.totalValues : 0;
  194. return (
  195. <GroupTagDistributionMeter
  196. key={tag.key}
  197. tag={tag.key}
  198. totalValues={topValuesTotal}
  199. topValues={topValues}
  200. name={tag.name}
  201. organization={organization}
  202. projectId={projectId}
  203. group={group}
  204. />
  205. );
  206. })
  207. )}
  208. {group.tags.length === 0 && (
  209. <p data-test-id="no-tags">
  210. {environments.length
  211. ? t('No tags found in the selected environments')
  212. : t('No tags found')}
  213. </p>
  214. )}
  215. </SidebarSection>
  216. {this.renderParticipantData()}
  217. </Container>
  218. );
  219. }
  220. }
  221. const PageFiltersContainer = styled('div')`
  222. margin-bottom: ${space(2)};
  223. `;
  224. const Container = styled('div')`
  225. font-size: ${p => p.theme.fontSizeMedium};
  226. `;
  227. const TagPlaceholders = styled('div')`
  228. display: grid;
  229. gap: ${space(1)};
  230. grid-auto-flow: row;
  231. `;
  232. const ExternalIssues = styled('div')`
  233. display: grid;
  234. grid-template-columns: auto max-content;
  235. gap: ${space(2)};
  236. `;
  237. const GroupSidebar = withApi(BaseGroupSidebar);
  238. export default GroupSidebar;