modalManager.tsx 7.8 KB

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