modalManager.tsx 7.7 KB

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