index.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. import {Component} from 'react';
  2. import styled from '@emotion/styled';
  3. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  4. import {openModal} from 'sentry/actionCreators/modal';
  5. import {Client} from 'sentry/api';
  6. import Button from 'sentry/components/button';
  7. import ExternalLink from 'sentry/components/links/externalLink';
  8. import {Panel, PanelAlert, PanelBody, PanelHeader} from 'sentry/components/panels';
  9. import {t, tct} from 'sentry/locale';
  10. import space from 'sentry/styles/space';
  11. import {Organization, Project} from 'sentry/types';
  12. import Add from './modals/add';
  13. import Edit from './modals/edit';
  14. import Content from './content';
  15. import convertRelayPiiConfig from './convertRelayPiiConfig';
  16. import OrganizationRules from './organizationRules';
  17. import submitRules from './submitRules';
  18. import {ProjectId, Rule} from './types';
  19. const ADVANCED_DATASCRUBBING_LINK =
  20. 'https://docs.sentry.io/product/data-management-settings/scrubbing/advanced-datascrubbing/';
  21. type Props<T extends ProjectId> = {
  22. endpoint: string;
  23. organization: Organization;
  24. additionalContext?: React.ReactNode;
  25. disabled?: boolean;
  26. onSubmitSuccess?: (data: T extends undefined ? Organization : Project) => void;
  27. projectId?: T;
  28. relayPiiConfig?: string;
  29. };
  30. type State = {
  31. orgRules: Array<Rule>;
  32. rules: Array<Rule>;
  33. savedRules: Array<Rule>;
  34. relayPiiConfig?: string;
  35. };
  36. class DataScrubbing<T extends ProjectId = undefined> extends Component<Props<T>, State> {
  37. state: State = {
  38. rules: [],
  39. savedRules: [],
  40. relayPiiConfig: this.props.relayPiiConfig,
  41. orgRules: [],
  42. };
  43. componentDidMount() {
  44. this.loadRules();
  45. this.loadOrganizationRules();
  46. }
  47. componentDidUpdate(_prevProps: Props<T>, prevState: State) {
  48. if (prevState.relayPiiConfig !== this.state.relayPiiConfig) {
  49. this.loadRules();
  50. }
  51. }
  52. componentWillUnmount() {
  53. this.api.clear();
  54. }
  55. api = new Client();
  56. loadOrganizationRules() {
  57. const {organization, projectId} = this.props;
  58. if (projectId) {
  59. try {
  60. this.setState({
  61. orgRules: convertRelayPiiConfig(organization.relayPiiConfig),
  62. });
  63. } catch {
  64. addErrorMessage(t('Unable to load organization rules'));
  65. }
  66. }
  67. }
  68. loadRules() {
  69. try {
  70. const convertedRules = convertRelayPiiConfig(this.state.relayPiiConfig);
  71. this.setState({
  72. rules: convertedRules,
  73. savedRules: convertedRules,
  74. });
  75. } catch {
  76. addErrorMessage(t('Unable to load project rules'));
  77. }
  78. }
  79. successfullySaved(
  80. response: T extends undefined ? Organization : Project,
  81. successMessage: string
  82. ) {
  83. const {onSubmitSuccess} = this.props;
  84. this.setState({rules: convertRelayPiiConfig(response.relayPiiConfig)});
  85. addSuccessMessage(successMessage);
  86. onSubmitSuccess?.(response);
  87. }
  88. handleOpenAddModal = () => {
  89. const {rules} = this.state;
  90. openModal(modalProps => (
  91. <Add
  92. {...modalProps}
  93. projectId={this.props.projectId}
  94. savedRules={rules}
  95. api={this.api}
  96. endpoint={this.props.endpoint}
  97. orgSlug={this.props.organization.slug}
  98. onSubmitSuccess={response => {
  99. this.successfullySaved(response, t('Successfully added data scrubbing rule'));
  100. }}
  101. />
  102. ));
  103. };
  104. handleOpenEditModal = (id: Rule['id']) => () => {
  105. const {rules} = this.state;
  106. openModal(modalProps => (
  107. <Edit
  108. {...modalProps}
  109. rule={rules[id]}
  110. projectId={this.props.projectId}
  111. savedRules={rules}
  112. api={this.api}
  113. endpoint={this.props.endpoint}
  114. orgSlug={this.props.organization.slug}
  115. onSubmitSuccess={response => {
  116. this.successfullySaved(response, t('Successfully updated data scrubbing rule'));
  117. }}
  118. />
  119. ));
  120. };
  121. handleDelete = (id: Rule['id']) => async () => {
  122. const {rules} = this.state;
  123. const filteredRules = rules.filter(rule => rule.id !== id);
  124. try {
  125. const data = await submitRules(this.api, this.props.endpoint, filteredRules);
  126. if (data?.relayPiiConfig) {
  127. const convertedRules = convertRelayPiiConfig(data.relayPiiConfig);
  128. this.setState({rules: convertedRules});
  129. addSuccessMessage(t('Successfully deleted data scrubbing rule'));
  130. }
  131. } catch {
  132. addErrorMessage(t('An unknown error occurred while deleting data scrubbing rule'));
  133. }
  134. };
  135. render() {
  136. const {additionalContext, disabled, projectId} = this.props;
  137. const {orgRules, rules} = this.state;
  138. return (
  139. <Panel data-test-id="advanced-data-scrubbing" id="advanced-data-scrubbing">
  140. <PanelHeader>
  141. <div>{t('Advanced Data Scrubbing')}</div>
  142. </PanelHeader>
  143. <PanelAlert type="info">
  144. {additionalContext}{' '}
  145. {`${t('The new rules will only apply to upcoming events. ')}`}{' '}
  146. {tct('For more details, see [linkToDocs].', {
  147. linkToDocs: (
  148. <ExternalLink href={ADVANCED_DATASCRUBBING_LINK}>
  149. {t('full documentation on data scrubbing')}
  150. </ExternalLink>
  151. ),
  152. })}
  153. </PanelAlert>
  154. <PanelBody>
  155. {projectId && <OrganizationRules rules={orgRules} />}
  156. <Content
  157. rules={rules}
  158. onDeleteRule={this.handleDelete}
  159. onEditRule={this.handleOpenEditModal}
  160. disabled={disabled}
  161. />
  162. <PanelAction>
  163. <Button href={ADVANCED_DATASCRUBBING_LINK} external>
  164. {t('Read Docs')}
  165. </Button>
  166. <Button
  167. disabled={disabled}
  168. onClick={this.handleOpenAddModal}
  169. priority="primary"
  170. >
  171. {t('Add Rule')}
  172. </Button>
  173. </PanelAction>
  174. </PanelBody>
  175. </Panel>
  176. );
  177. }
  178. }
  179. export default DataScrubbing;
  180. const PanelAction = styled('div')`
  181. padding: ${space(1)} ${space(2)};
  182. position: relative;
  183. display: grid;
  184. gap: ${space(1)};
  185. grid-template-columns: auto auto;
  186. justify-content: flex-end;
  187. border-top: 1px solid ${p => p.theme.border};
  188. `;