index.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  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, LinkButton} 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. // @ts-expect-error TS(7015): Element implicitly has an 'any' type because index... Remove this comment to see the full error message
  78. rule={rules[params.scrubbingId!]}
  79. projectId={project?.id}
  80. savedRules={rules}
  81. api={api}
  82. endpoint={endpoint}
  83. orgSlug={organization.slug}
  84. onSubmitSuccess={response => {
  85. return successfullySaved(
  86. response,
  87. t('Successfully updated data scrubbing rule')
  88. );
  89. }}
  90. />
  91. ),
  92. {onClose: handleCloseModal}
  93. );
  94. }, [
  95. params.scrubbingId,
  96. rules,
  97. project?.id,
  98. api,
  99. endpoint,
  100. organization.slug,
  101. successfullySaved,
  102. handleCloseModal,
  103. ]);
  104. useEffect(() => {
  105. function loadRules() {
  106. try {
  107. setRules(convertRelayPiiConfig(relayPiiConfig));
  108. } catch {
  109. addErrorMessage(t('Unable to load data scrubbing rules'));
  110. }
  111. }
  112. loadRules();
  113. }, [relayPiiConfig]);
  114. function handleEdit(id: Rule['id']) {
  115. const path = project
  116. ? `/settings/${organization.slug}/projects/${project.slug}/security-and-privacy/advanced-data-scrubbing/${id}/`
  117. : `/settings/${organization.slug}/security-and-privacy/advanced-data-scrubbing/${id}/`;
  118. navigate(path);
  119. }
  120. function handleAdd() {
  121. openModal(modalProps => (
  122. <Add
  123. {...modalProps}
  124. projectId={project?.id}
  125. savedRules={rules}
  126. api={api}
  127. endpoint={endpoint}
  128. orgSlug={organization.slug}
  129. onSubmitSuccess={response => {
  130. return successfullySaved(response, t('Successfully added data scrubbing rule'));
  131. }}
  132. />
  133. ));
  134. }
  135. async function handleDelete(id: Rule['id']) {
  136. try {
  137. const filteredRules = rules.filter(rule => rule.id !== id);
  138. const data = await submitRules(api, endpoint, filteredRules);
  139. if (data?.relayPiiConfig) {
  140. setRules(convertRelayPiiConfig(data.relayPiiConfig));
  141. addSuccessMessage(t('Successfully deleted data scrubbing rule'));
  142. }
  143. } catch {
  144. addErrorMessage(t('An unknown error occurred while deleting data scrubbing rule'));
  145. }
  146. }
  147. return (
  148. <Panel data-test-id="advanced-data-scrubbing">
  149. <PanelHeader>
  150. <div>{t('Advanced Data Scrubbing')}</div>
  151. </PanelHeader>
  152. <PanelAlert type="info">
  153. {additionalContext}{' '}
  154. {tct(
  155. 'The new rules will only apply to upcoming events. For more details, see [linkToDocs].',
  156. {
  157. linkToDocs: (
  158. <ExternalLink href={ADVANCED_DATASCRUBBING_LINK}>
  159. {t('full documentation on data scrubbing')}
  160. </ExternalLink>
  161. ),
  162. }
  163. )}
  164. </PanelAlert>
  165. <PanelBody>
  166. {project && <OrganizationRules organization={organization} />}
  167. {!rules.length ? (
  168. <EmptyMessage
  169. icon={<IconWarning size="xl" />}
  170. description={t('You have no data scrubbing rules')}
  171. />
  172. ) : (
  173. <Rules
  174. rules={rules}
  175. onDeleteRule={handleDelete}
  176. onEditRule={handleEdit}
  177. disabled={disabled}
  178. />
  179. )}
  180. <PanelAction>
  181. <LinkButton href={ADVANCED_DATASCRUBBING_LINK} external>
  182. {t('Read Docs')}
  183. </LinkButton>
  184. <Button disabled={disabled} onClick={handleAdd} priority="primary">
  185. {t('Add Rule')}
  186. </Button>
  187. </PanelAction>
  188. </PanelBody>
  189. </Panel>
  190. );
  191. }
  192. const PanelAction = styled('div')`
  193. padding: ${space(1)} ${space(2)};
  194. position: relative;
  195. display: grid;
  196. gap: ${space(1)};
  197. grid-template-columns: auto auto;
  198. justify-content: flex-end;
  199. border-top: 1px solid ${p => p.theme.border};
  200. `;