index.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import {useCallback, useEffect, useState} 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 {Button} from 'sentry/components/button';
  6. import EmptyMessage from 'sentry/components/emptyMessage';
  7. import ExternalLink from 'sentry/components/links/externalLink';
  8. import Panel from 'sentry/components/panels/panel';
  9. import PanelAlert from 'sentry/components/panels/panelAlert';
  10. import PanelBody from 'sentry/components/panels/panelBody';
  11. import PanelHeader from 'sentry/components/panels/panelHeader';
  12. import {IconWarning} from 'sentry/icons';
  13. import {t, tct} from 'sentry/locale';
  14. import {space} from 'sentry/styles/space';
  15. import type {Organization} from 'sentry/types/organization';
  16. import type {Project} from 'sentry/types/project';
  17. import {defined} from 'sentry/utils';
  18. import useApi from 'sentry/utils/useApi';
  19. import {useNavigate} from 'sentry/utils/useNavigate';
  20. import {useParams} from 'sentry/utils/useParams';
  21. import Add from './modals/add';
  22. import Edit from './modals/edit';
  23. import {convertRelayPiiConfig} from './convertRelayPiiConfig';
  24. import {OrganizationRules} from './organizationRules';
  25. import Rules from './rules';
  26. import submitRules from './submitRules';
  27. import type {Rule} from './types';
  28. const ADVANCED_DATASCRUBBING_LINK =
  29. 'https://docs.sentry.io/product/data-management-settings/scrubbing/advanced-datascrubbing/';
  30. type Props = {
  31. endpoint: string;
  32. organization: Organization;
  33. additionalContext?: React.ReactNode;
  34. disabled?: boolean;
  35. onSubmitSuccess?: (data: {relayPiiConfig: string}) => void;
  36. project?: Project;
  37. relayPiiConfig?: string | null;
  38. };
  39. export function DataScrubbing({
  40. project,
  41. endpoint,
  42. organization,
  43. disabled,
  44. onSubmitSuccess,
  45. additionalContext,
  46. relayPiiConfig,
  47. }: Props) {
  48. const api = useApi();
  49. const [rules, setRules] = useState<Rule[]>([]);
  50. const navigate = useNavigate();
  51. const params = useParams();
  52. const successfullySaved = useCallback(
  53. (response: {relayPiiConfig: string}, successMessage: string) => {
  54. setRules(convertRelayPiiConfig(response.relayPiiConfig));
  55. addSuccessMessage(successMessage);
  56. onSubmitSuccess?.(response);
  57. },
  58. [onSubmitSuccess]
  59. );
  60. const handleCloseModal = useCallback(() => {
  61. const path = project?.slug
  62. ? `/settings/${organization.slug}/projects/${project.slug}/security-and-privacy/`
  63. : `/settings/${organization.slug}/security-and-privacy/`;
  64. navigate(path);
  65. }, [navigate, organization.slug, project?.slug]);
  66. useEffect(() => {
  67. if (
  68. !defined(params.scrubbingId) ||
  69. !rules.some(rule => String(rule.id) === params.scrubbingId)
  70. ) {
  71. return;
  72. }
  73. openModal(
  74. modalProps => (
  75. <Edit
  76. {...modalProps}
  77. rule={rules[params.scrubbingId]}
  78. projectId={project?.id}
  79. savedRules={rules}
  80. api={api}
  81. endpoint={endpoint}
  82. orgSlug={organization.slug}
  83. onSubmitSuccess={response => {
  84. return successfullySaved(
  85. response,
  86. t('Successfully updated data scrubbing rule')
  87. );
  88. }}
  89. />
  90. ),
  91. {onClose: handleCloseModal}
  92. );
  93. }, [
  94. params.scrubbingId,
  95. rules,
  96. project?.id,
  97. api,
  98. endpoint,
  99. organization.slug,
  100. successfullySaved,
  101. handleCloseModal,
  102. ]);
  103. useEffect(() => {
  104. function loadRules() {
  105. try {
  106. setRules(convertRelayPiiConfig(relayPiiConfig));
  107. } catch {
  108. addErrorMessage(t('Unable to load data scrubbing rules'));
  109. }
  110. }
  111. loadRules();
  112. }, [relayPiiConfig]);
  113. function handleEdit(id: Rule['id']) {
  114. const path = project
  115. ? `/settings/${organization.slug}/projects/${project.slug}/security-and-privacy/advanced-data-scrubbing/${id}/`
  116. : `/settings/${organization.slug}/security-and-privacy/advanced-data-scrubbing/${id}/`;
  117. navigate(path);
  118. }
  119. function handleAdd() {
  120. openModal(modalProps => (
  121. <Add
  122. {...modalProps}
  123. projectId={project?.id}
  124. savedRules={rules}
  125. api={api}
  126. endpoint={endpoint}
  127. orgSlug={organization.slug}
  128. onSubmitSuccess={response => {
  129. return successfullySaved(response, t('Successfully added data scrubbing rule'));
  130. }}
  131. />
  132. ));
  133. }
  134. async function handleDelete(id: Rule['id']) {
  135. try {
  136. const filteredRules = rules.filter(rule => rule.id !== id);
  137. const data = await submitRules(api, endpoint, filteredRules);
  138. if (data?.relayPiiConfig) {
  139. setRules(convertRelayPiiConfig(data.relayPiiConfig));
  140. addSuccessMessage(t('Successfully deleted data scrubbing rule'));
  141. }
  142. } catch {
  143. addErrorMessage(t('An unknown error occurred while deleting data scrubbing rule'));
  144. }
  145. }
  146. return (
  147. <Panel data-test-id="advanced-data-scrubbing">
  148. <PanelHeader>
  149. <div>{t('Advanced Data Scrubbing')}</div>
  150. </PanelHeader>
  151. <PanelAlert type="info">
  152. {additionalContext}{' '}
  153. {tct(
  154. 'The new rules will only apply to upcoming events. For more details, see [linkToDocs].',
  155. {
  156. linkToDocs: (
  157. <ExternalLink href={ADVANCED_DATASCRUBBING_LINK}>
  158. {t('full documentation on data scrubbing')}
  159. </ExternalLink>
  160. ),
  161. }
  162. )}
  163. </PanelAlert>
  164. <PanelBody>
  165. {project && <OrganizationRules organization={organization} />}
  166. {!rules.length ? (
  167. <EmptyMessage
  168. icon={<IconWarning size="xl" />}
  169. description={t('You have no data scrubbing rules')}
  170. />
  171. ) : (
  172. <Rules
  173. rules={rules}
  174. onDeleteRule={handleDelete}
  175. onEditRule={handleEdit}
  176. disabled={disabled}
  177. />
  178. )}
  179. <PanelAction>
  180. <Button href={ADVANCED_DATASCRUBBING_LINK} external>
  181. {t('Read Docs')}
  182. </Button>
  183. <Button disabled={disabled} onClick={handleAdd} priority="primary">
  184. {t('Add Rule')}
  185. </Button>
  186. </PanelAction>
  187. </PanelBody>
  188. </Panel>
  189. );
  190. }
  191. const PanelAction = styled('div')`
  192. padding: ${space(1)} ${space(2)};
  193. position: relative;
  194. display: grid;
  195. gap: ${space(1)};
  196. grid-template-columns: auto auto;
  197. justify-content: flex-end;
  198. border-top: 1px solid ${p => p.theme.border};
  199. `;