index.tsx 6.2 KB

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