index.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import AsyncComponent from 'sentry/components/asyncComponent';
  4. import Button from 'sentry/components/button';
  5. import EventDataSection from 'sentry/components/events/eventDataSection';
  6. import {FeatureFeedback} from 'sentry/components/featureFeedback';
  7. import LoadingIndicator from 'sentry/components/loadingIndicator';
  8. import {t} from 'sentry/locale';
  9. import space from 'sentry/styles/space';
  10. import {EventGroupInfo, Organization} from 'sentry/types';
  11. import {Event} from 'sentry/types/event';
  12. import withOrganization from 'sentry/utils/withOrganization';
  13. import {groupingFeedbackTypes} from 'sentry/views/organizationGroupDetails/grouping/grouping';
  14. import GroupingConfigSelect from './groupingConfigSelect';
  15. import GroupVariant from './groupingVariant';
  16. type Props = AsyncComponent['props'] & {
  17. event: Event;
  18. organization: Organization;
  19. projectId: string;
  20. showGroupingConfig: boolean;
  21. };
  22. type State = AsyncComponent['state'] & {
  23. configOverride: string | null;
  24. groupInfo: EventGroupInfo;
  25. isOpen: boolean;
  26. };
  27. class EventGroupingInfo extends AsyncComponent<Props, State> {
  28. getEndpoints(): ReturnType<AsyncComponent['getEndpoints']> {
  29. const {organization, event, projectId} = this.props;
  30. let path = `/projects/${organization.slug}/${projectId}/events/${event.id}/grouping-info/`;
  31. if (this.state?.configOverride) {
  32. path = `${path}?config=${this.state.configOverride}`;
  33. }
  34. return [['groupInfo', path]];
  35. }
  36. getDefaultState() {
  37. return {
  38. ...super.getDefaultState(),
  39. isOpen: false,
  40. configOverride: null,
  41. };
  42. }
  43. toggle = () => {
  44. this.setState(state => ({
  45. isOpen: !state.isOpen,
  46. configOverride: state.isOpen ? null : state.configOverride,
  47. }));
  48. };
  49. handleConfigSelect = selection => {
  50. this.setState({configOverride: selection.value}, () => this.reloadData());
  51. };
  52. renderGroupInfoSummary() {
  53. const {groupInfo} = this.state;
  54. if (!groupInfo) {
  55. return null;
  56. }
  57. const groupedBy = Object.values(groupInfo)
  58. .filter(variant => variant.hash !== null && variant.description !== null)
  59. .map(variant => variant.description)
  60. .sort((a, b) => a!.toLowerCase().localeCompare(b!.toLowerCase()))
  61. .join(', ');
  62. return (
  63. <SummaryGroupedBy data-test-id="loaded-grouping-info">{`(${t('grouped by')} ${
  64. groupedBy || t('nothing')
  65. })`}</SummaryGroupedBy>
  66. );
  67. }
  68. renderGroupConfigSelect() {
  69. const {configOverride} = this.state;
  70. const {event} = this.props;
  71. if (!event.groupingConfig) {
  72. return null;
  73. }
  74. const configId = configOverride ?? event.groupingConfig?.id;
  75. return (
  76. <GroupingConfigSelect
  77. eventConfigId={event.groupingConfig.id}
  78. configId={configId}
  79. onSelect={this.handleConfigSelect}
  80. />
  81. );
  82. }
  83. renderGroupInfo() {
  84. const {groupInfo, loading} = this.state;
  85. const {showGroupingConfig} = this.props;
  86. const variants = groupInfo
  87. ? Object.values(groupInfo).sort((a, b) =>
  88. a.hash && !b.hash
  89. ? -1
  90. : a.description
  91. ?.toLowerCase()
  92. .localeCompare(b.description?.toLowerCase() ?? '') ?? 1
  93. )
  94. : [];
  95. return (
  96. <Fragment>
  97. <ConfigHeader>
  98. <div>{showGroupingConfig && this.renderGroupConfigSelect()}</div>
  99. <FeatureFeedback
  100. featureName="grouping"
  101. feedbackTypes={groupingFeedbackTypes}
  102. buttonProps={{size: 'sm'}}
  103. />
  104. </ConfigHeader>
  105. {loading ? (
  106. <LoadingIndicator />
  107. ) : (
  108. variants.map((variant, index) => (
  109. <Fragment key={variant.key}>
  110. <GroupVariant variant={variant} showGroupingConfig={showGroupingConfig} />
  111. {index < variants.length - 1 && <VariantDivider />}
  112. </Fragment>
  113. ))
  114. )}
  115. </Fragment>
  116. );
  117. }
  118. renderLoading() {
  119. return this.renderBody();
  120. }
  121. renderBody() {
  122. const {isOpen} = this.state;
  123. const title = (
  124. <Fragment>
  125. {t('Event Grouping Information')}
  126. {!isOpen && this.renderGroupInfoSummary()}
  127. </Fragment>
  128. );
  129. const actions = (
  130. <ToggleButton onClick={this.toggle} priority="link">
  131. {isOpen ? t('Hide Details') : t('Show Details')}
  132. </ToggleButton>
  133. );
  134. return (
  135. <EventDataSection type="grouping-info" title={title} actions={actions}>
  136. {isOpen && this.renderGroupInfo()}
  137. </EventDataSection>
  138. );
  139. }
  140. }
  141. const SummaryGroupedBy = styled('small')`
  142. @media (max-width: ${p => p.theme.breakpoints.small}) {
  143. display: block;
  144. margin: 0 !important;
  145. }
  146. `;
  147. const ConfigHeader = styled('div')`
  148. display: flex;
  149. align-items: center;
  150. justify-content: space-between;
  151. gap: ${space(1)};
  152. margin-bottom: ${space(2)};
  153. `;
  154. const ToggleButton = styled(Button)`
  155. font-weight: 700;
  156. color: ${p => p.theme.subText};
  157. &:hover,
  158. &:focus {
  159. color: ${p => p.theme.textColor};
  160. }
  161. `;
  162. export const GroupingConfigItem = styled('span')<{
  163. isActive?: boolean;
  164. isHidden?: boolean;
  165. }>`
  166. font-family: ${p => p.theme.text.familyMono};
  167. opacity: ${p => (p.isHidden ? 0.5 : null)};
  168. font-weight: ${p => (p.isActive ? 'bold' : null)};
  169. font-size: ${p => p.theme.fontSizeSmall};
  170. `;
  171. const VariantDivider = styled('hr')`
  172. padding-top: ${space(1)};
  173. border-top: 1px solid ${p => p.theme.border};
  174. `;
  175. export default withOrganization(EventGroupingInfo);