groupList.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. import React from 'react';
  2. import {browserHistory, withRouter, WithRouterProps} from 'react-router';
  3. import * as Sentry from '@sentry/react';
  4. import isEqual from 'lodash/isEqual';
  5. import * as qs from 'query-string';
  6. import {fetchOrgMembers, indexMembersByProject} from 'app/actionCreators/members';
  7. import {Client} from 'app/api';
  8. import EmptyStateWarning from 'app/components/emptyStateWarning';
  9. import LoadingError from 'app/components/loadingError';
  10. import LoadingIndicator from 'app/components/loadingIndicator';
  11. import Pagination from 'app/components/pagination';
  12. import {Panel, PanelBody} from 'app/components/panels';
  13. import StreamGroup, {
  14. DEFAULT_STREAM_GROUP_STATS_PERIOD,
  15. } from 'app/components/stream/group';
  16. import {t} from 'app/locale';
  17. import GroupStore from 'app/stores/groupStore';
  18. import {Group} from 'app/types';
  19. import {callIfFunction} from 'app/utils/callIfFunction';
  20. import StreamManager from 'app/utils/streamManager';
  21. import withApi from 'app/utils/withApi';
  22. import {TimePeriodType} from 'app/views/alerts/rules/details/constants';
  23. import GroupListHeader from './groupListHeader';
  24. const defaultProps = {
  25. canSelectGroups: true,
  26. withChart: true,
  27. withPagination: true,
  28. useFilteredStats: true,
  29. };
  30. type Props = WithRouterProps & {
  31. api: Client;
  32. query: string;
  33. orgId: string;
  34. endpointPath: string;
  35. renderEmptyMessage?: () => React.ReactNode;
  36. queryParams?: Record<string, number | string | string[] | undefined | null>;
  37. customStatsPeriod?: TimePeriodType;
  38. onFetchSuccess?: (
  39. groupListState: State,
  40. onCursor: (
  41. cursor: string,
  42. path: string,
  43. query: Record<string, any>,
  44. pageDiff: number
  45. ) => void
  46. ) => void;
  47. } & Partial<typeof defaultProps>;
  48. type State = {
  49. loading: boolean;
  50. error: boolean;
  51. groups: Group[];
  52. pageLinks: string | null;
  53. memberList?: ReturnType<typeof indexMembersByProject>;
  54. };
  55. class GroupList extends React.Component<Props, State> {
  56. static defaultProps = defaultProps;
  57. state: State = {
  58. loading: true,
  59. error: false,
  60. groups: [],
  61. pageLinks: null,
  62. };
  63. componentDidMount() {
  64. this.fetchData();
  65. }
  66. shouldComponentUpdate(nextProps: Props, nextState: State) {
  67. return (
  68. !isEqual(this.state, nextState) ||
  69. nextProps.endpointPath !== this.props.endpointPath ||
  70. nextProps.query !== this.props.query ||
  71. !isEqual(nextProps.queryParams, this.props.queryParams)
  72. );
  73. }
  74. componentDidUpdate(prevProps: Props) {
  75. if (
  76. prevProps.orgId !== this.props.orgId ||
  77. prevProps.endpointPath !== this.props.endpointPath ||
  78. prevProps.query !== this.props.query ||
  79. !isEqual(prevProps.queryParams, this.props.queryParams)
  80. ) {
  81. this.fetchData();
  82. }
  83. }
  84. componentWillUnmount() {
  85. GroupStore.reset();
  86. callIfFunction(this.listener);
  87. }
  88. listener = GroupStore.listen(() => this.onGroupChange(), undefined);
  89. private _streamManager = new StreamManager(GroupStore);
  90. fetchData = () => {
  91. GroupStore.loadInitialData([]);
  92. const {api, orgId} = this.props;
  93. this.setState({loading: true, error: false});
  94. fetchOrgMembers(api, orgId).then(members => {
  95. this.setState({memberList: indexMembersByProject(members)});
  96. });
  97. const endpoint = this.getGroupListEndpoint();
  98. api.request(endpoint, {
  99. success: (data, _, jqXHR) => {
  100. this._streamManager.push(data);
  101. this.setState(
  102. {
  103. error: false,
  104. loading: false,
  105. pageLinks: jqXHR?.getResponseHeader('Link') ?? null,
  106. },
  107. () => {
  108. this.props.onFetchSuccess?.(this.state, this.handleCursorChange);
  109. }
  110. );
  111. },
  112. error: err => {
  113. Sentry.captureException(err);
  114. this.setState({error: true, loading: false});
  115. },
  116. });
  117. };
  118. getGroupListEndpoint() {
  119. const {orgId, endpointPath, queryParams} = this.props;
  120. const path = endpointPath ?? `/organizations/${orgId}/issues/`;
  121. const queryParameters = queryParams ?? this.getQueryParams();
  122. return `${path}?${qs.stringify(queryParameters)}`;
  123. }
  124. getQueryParams() {
  125. const {location, query} = this.props;
  126. const queryParams = location.query;
  127. queryParams.limit = 50;
  128. queryParams.sort = 'new';
  129. queryParams.query = query;
  130. return queryParams;
  131. }
  132. handleCursorChange(
  133. cursor: string,
  134. path: string,
  135. query: Record<string, any>,
  136. pageDiff: number
  137. ) {
  138. const queryPageInt = parseInt(query.page, 10);
  139. let nextPage: number | undefined = isNaN(queryPageInt)
  140. ? pageDiff
  141. : queryPageInt + pageDiff;
  142. let nextCursor: string | undefined = cursor;
  143. // unset cursor and page when we navigate back to the first page
  144. // also reset cursor if somehow the previous button is enabled on
  145. // first page and user attempts to go backwards
  146. if (nextPage <= 0) {
  147. nextCursor = undefined;
  148. nextPage = undefined;
  149. }
  150. browserHistory.push({
  151. pathname: path,
  152. query: {...query, cursor: nextCursor},
  153. });
  154. }
  155. onGroupChange() {
  156. const groups = this._streamManager.getAllItems();
  157. if (!isEqual(groups, this.state.groups)) {
  158. this.setState({groups});
  159. }
  160. }
  161. render() {
  162. const {
  163. canSelectGroups,
  164. withChart,
  165. renderEmptyMessage,
  166. withPagination,
  167. useFilteredStats,
  168. customStatsPeriod,
  169. queryParams,
  170. } = this.props;
  171. const {loading, error, groups, memberList, pageLinks} = this.state;
  172. if (loading) {
  173. return <LoadingIndicator />;
  174. }
  175. if (error) {
  176. return <LoadingError onRetry={this.fetchData} />;
  177. }
  178. if (groups.length === 0) {
  179. if (typeof renderEmptyMessage === 'function') {
  180. return renderEmptyMessage();
  181. }
  182. return (
  183. <Panel>
  184. <PanelBody>
  185. <EmptyStateWarning>
  186. <p>{t("There don't seem to be any events fitting the query.")}</p>
  187. </EmptyStateWarning>
  188. </PanelBody>
  189. </Panel>
  190. );
  191. }
  192. const statsPeriod =
  193. queryParams?.groupStatsPeriod === 'auto'
  194. ? queryParams?.groupStatsPeriod
  195. : DEFAULT_STREAM_GROUP_STATS_PERIOD;
  196. return (
  197. <React.Fragment>
  198. <Panel>
  199. <GroupListHeader withChart={!!withChart} />
  200. <PanelBody>
  201. {groups.map(({id, project}) => {
  202. const members = memberList?.hasOwnProperty(project.slug)
  203. ? memberList[project.slug]
  204. : undefined;
  205. return (
  206. <StreamGroup
  207. key={id}
  208. id={id}
  209. canSelect={canSelectGroups}
  210. withChart={withChart}
  211. memberList={members}
  212. useFilteredStats={useFilteredStats}
  213. customStatsPeriod={customStatsPeriod}
  214. statsPeriod={statsPeriod}
  215. />
  216. );
  217. })}
  218. </PanelBody>
  219. </Panel>
  220. {withPagination && (
  221. <Pagination pageLinks={pageLinks} onCursor={this.handleCursorChange} />
  222. )}
  223. </React.Fragment>
  224. );
  225. }
  226. }
  227. export {GroupList};
  228. export default withApi(withRouter(GroupList));