modalManager.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. import {Component} from 'react';
  2. import isEqual from 'lodash/isEqual';
  3. import omit from 'lodash/omit';
  4. import {addErrorMessage} from 'sentry/actionCreators/indicator';
  5. import type {ModalRenderProps} from 'sentry/actionCreators/modal';
  6. import type {Client} from 'sentry/api';
  7. import {t} from 'sentry/locale';
  8. import type {Organization, Project} from 'sentry/types';
  9. import submitRules from '../submitRules';
  10. import type {KeysOfUnion, Rule} from '../types';
  11. import {EventIdStatus, MethodType, RuleType} from '../types';
  12. import {valueSuggestions} from '../utils';
  13. import Form from './form';
  14. import handleError, {ErrorType} from './handleError';
  15. import Modal from './modal';
  16. import {fetchSourceGroupData, saveToSourceGroupData} from './utils';
  17. type FormProps = React.ComponentProps<typeof Form>;
  18. type Values = FormProps['values'];
  19. type EventId = NonNullable<FormProps['eventId']>;
  20. type SourceSuggestions = FormProps['sourceSuggestions'];
  21. type Props = ModalRenderProps & {
  22. api: Client;
  23. endpoint: string;
  24. onGetNewRules: (values: Values) => Rule[];
  25. onSubmitSuccess: (data: {relayPiiConfig: string}) => void;
  26. orgSlug: Organization['slug'];
  27. savedRules: Rule[];
  28. title: string;
  29. initialState?: Partial<Values>;
  30. projectId?: Project['id'];
  31. };
  32. type State = {
  33. errors: FormProps['errors'];
  34. eventId: EventId;
  35. isFormValid: boolean;
  36. requiredValues: Array<keyof Values>;
  37. sourceSuggestions: SourceSuggestions;
  38. values: Values;
  39. };
  40. class ModalManager extends Component<Props, State> {
  41. state = this.getDefaultState();
  42. componentDidMount() {
  43. this.handleValidateForm();
  44. }
  45. componentDidUpdate(_prevProps: Props, prevState: State) {
  46. if (!isEqual(prevState.values, this.state.values)) {
  47. this.handleValidateForm();
  48. }
  49. if (prevState.eventId.value !== this.state.eventId.value) {
  50. this.loadSourceSuggestions();
  51. }
  52. if (prevState.eventId.status !== this.state.eventId.status) {
  53. saveToSourceGroupData(this.state.eventId, this.state.sourceSuggestions);
  54. }
  55. }
  56. getDefaultState(): Readonly<State> {
  57. const {eventId, sourceSuggestions} = fetchSourceGroupData();
  58. const values = this.getInitialValues();
  59. return {
  60. values,
  61. requiredValues: this.getRequiredValues(values),
  62. errors: {},
  63. isFormValid: false,
  64. eventId: {
  65. value: eventId,
  66. status: !eventId ? EventIdStatus.UNDEFINED : EventIdStatus.LOADED,
  67. },
  68. sourceSuggestions,
  69. } as Readonly<State>;
  70. }
  71. getInitialValues() {
  72. const {initialState} = this.props;
  73. return {
  74. type: initialState?.type ?? RuleType.CREDITCARD,
  75. method: initialState?.method ?? MethodType.MASK,
  76. source: initialState?.source ?? '',
  77. placeholder: initialState?.placeholder ?? '',
  78. pattern: initialState?.pattern ?? '',
  79. };
  80. }
  81. getRequiredValues(values: Values) {
  82. const {type} = values;
  83. const requiredValues: Array<KeysOfUnion<Values>> = ['type', 'method', 'source'];
  84. if (type === RuleType.PATTERN) {
  85. requiredValues.push('pattern');
  86. }
  87. return requiredValues;
  88. }
  89. clearError<F extends keyof Values>(field: F) {
  90. this.setState(prevState => ({
  91. errors: omit(prevState.errors, field),
  92. }));
  93. }
  94. async loadSourceSuggestions() {
  95. const {orgSlug, projectId, api} = this.props;
  96. const {eventId} = this.state;
  97. if (!eventId.value) {
  98. this.setState(prevState => ({
  99. sourceSuggestions: valueSuggestions,
  100. eventId: {
  101. ...prevState.eventId,
  102. status: EventIdStatus.UNDEFINED,
  103. },
  104. }));
  105. return;
  106. }
  107. this.setState(prevState => ({
  108. sourceSuggestions: valueSuggestions,
  109. eventId: {
  110. ...prevState.eventId,
  111. status: EventIdStatus.LOADING,
  112. },
  113. }));
  114. try {
  115. const query: {eventId: string; projectId?: string} = {eventId: eventId.value};
  116. if (projectId) {
  117. query.projectId = projectId;
  118. }
  119. const rawSuggestions = await api.requestPromise(
  120. `/organizations/${orgSlug}/data-scrubbing-selector-suggestions/`,
  121. {query}
  122. );
  123. const sourceSuggestions: SourceSuggestions = rawSuggestions.suggestions;
  124. if (sourceSuggestions && sourceSuggestions.length > 0) {
  125. this.setState(prevState => ({
  126. sourceSuggestions,
  127. eventId: {
  128. ...prevState.eventId,
  129. status: EventIdStatus.LOADED,
  130. },
  131. }));
  132. return;
  133. }
  134. this.setState(prevState => ({
  135. sourceSuggestions: valueSuggestions,
  136. eventId: {
  137. ...prevState.eventId,
  138. status: EventIdStatus.NOT_FOUND,
  139. },
  140. }));
  141. } catch {
  142. this.setState(prevState => ({
  143. eventId: {
  144. ...prevState.eventId,
  145. status: EventIdStatus.ERROR,
  146. },
  147. }));
  148. }
  149. }
  150. convertRequestError(error: ReturnType<typeof handleError>) {
  151. switch (error.type) {
  152. case ErrorType.INVALID_SELECTOR:
  153. this.setState(prevState => ({
  154. errors: {
  155. ...prevState.errors,
  156. source: error.message,
  157. },
  158. }));
  159. break;
  160. case ErrorType.REGEX_PARSE:
  161. this.setState(prevState => ({
  162. errors: {
  163. ...prevState.errors,
  164. pattern: error.message,
  165. },
  166. }));
  167. break;
  168. default:
  169. addErrorMessage(error.message);
  170. }
  171. }
  172. handleChange = <R extends Rule, K extends KeysOfUnion<R>>(field: K, value: R[K]) => {
  173. const values = {
  174. ...this.state.values,
  175. [field]: value,
  176. };
  177. if (values.type !== RuleType.PATTERN && values.pattern) {
  178. values.pattern = '';
  179. }
  180. if (values.method !== MethodType.REPLACE && values.placeholder) {
  181. values.placeholder = '';
  182. }
  183. this.setState(prevState => ({
  184. values,
  185. requiredValues: this.getRequiredValues(values),
  186. errors: omit(prevState.errors, field),
  187. }));
  188. };
  189. handleSave = async () => {
  190. const {endpoint, api, onSubmitSuccess, closeModal, onGetNewRules} = this.props;
  191. const newRules = onGetNewRules(this.state.values);
  192. try {
  193. const data = await submitRules(api, endpoint, newRules);
  194. onSubmitSuccess(data);
  195. closeModal();
  196. } catch (error) {
  197. this.convertRequestError(handleError(error));
  198. }
  199. };
  200. handleValidateForm() {
  201. const {values, requiredValues} = this.state;
  202. const isFormValid = requiredValues.every(requiredValue => !!values[requiredValue]);
  203. this.setState({isFormValid});
  204. }
  205. handleValidate =
  206. <K extends keyof Values>(field: K) =>
  207. () => {
  208. const isFieldValueEmpty = !this.state.values[field].trim();
  209. const fieldErrorAlreadyExist = this.state.errors[field];
  210. if (isFieldValueEmpty && fieldErrorAlreadyExist) {
  211. return;
  212. }
  213. if (isFieldValueEmpty && !fieldErrorAlreadyExist) {
  214. this.setState(prevState => ({
  215. errors: {
  216. ...prevState.errors,
  217. [field]: t('Field Required'),
  218. },
  219. }));
  220. return;
  221. }
  222. if (!isFieldValueEmpty && fieldErrorAlreadyExist) {
  223. this.clearError(field);
  224. }
  225. };
  226. handleUpdateEventId = (eventId: string) => {
  227. if (eventId === this.state.eventId.value) {
  228. return;
  229. }
  230. this.setState({
  231. eventId: {value: eventId, status: EventIdStatus.UNDEFINED},
  232. });
  233. };
  234. render() {
  235. const {values, errors, isFormValid, eventId, sourceSuggestions} = this.state;
  236. const {title} = this.props;
  237. return (
  238. <Modal
  239. {...this.props}
  240. title={title}
  241. onSave={this.handleSave}
  242. disabled={!isFormValid}
  243. content={
  244. <Form
  245. onChange={this.handleChange}
  246. onValidate={this.handleValidate}
  247. onUpdateEventId={this.handleUpdateEventId}
  248. eventId={eventId}
  249. errors={errors}
  250. values={values}
  251. sourceSuggestions={sourceSuggestions}
  252. />
  253. }
  254. />
  255. );
  256. }
  257. }
  258. export default ModalManager;