import {Component} from 'react'; import isEqual from 'lodash/isEqual'; import omit from 'lodash/omit'; import {addErrorMessage} from 'sentry/actionCreators/indicator'; import type {ModalRenderProps} from 'sentry/actionCreators/modal'; import type {Client} from 'sentry/api'; import {t} from 'sentry/locale'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import submitRules from '../submitRules'; import type {KeysOfUnion, Rule} from '../types'; import {EventIdStatus, MethodType, RuleType} from '../types'; import {valueSuggestions} from '../utils'; import Form from './form'; import handleError, {ErrorType} from './handleError'; import Modal from './modal'; import {fetchSourceGroupData, saveToSourceGroupData} from './utils'; type FormProps = React.ComponentProps; type Values = FormProps['values']; type EventId = NonNullable; type SourceSuggestions = FormProps['sourceSuggestions']; type Props = ModalRenderProps & { api: Client; endpoint: string; onGetNewRules: (values: Values) => Rule[]; onSubmitSuccess: (data: {relayPiiConfig: string}) => void; orgSlug: Organization['slug']; savedRules: Rule[]; title: string; initialState?: Partial; projectId?: Project['id']; }; type State = { errors: FormProps['errors']; eventId: EventId; isFormValid: boolean; requiredValues: Array; sourceSuggestions: SourceSuggestions; values: Values; }; class ModalManager extends Component { state = this.getDefaultState(); componentDidMount() { this.handleValidateForm(); } componentDidUpdate(_prevProps: Props, prevState: State) { if (!isEqual(prevState.values, this.state.values)) { this.handleValidateForm(); } if (prevState.eventId.value !== this.state.eventId.value) { this.loadSourceSuggestions(); } if (prevState.eventId.status !== this.state.eventId.status) { saveToSourceGroupData(this.state.eventId, this.state.sourceSuggestions); } } getDefaultState(): Readonly { const {eventId, sourceSuggestions} = fetchSourceGroupData(); const values = this.getInitialValues(); return { values, requiredValues: this.getRequiredValues(values), errors: {}, isFormValid: false, eventId: { value: eventId, status: !eventId ? EventIdStatus.UNDEFINED : EventIdStatus.LOADED, }, sourceSuggestions, } as Readonly; } getInitialValues() { const {initialState} = this.props; return { type: initialState?.type ?? RuleType.CREDITCARD, method: initialState?.method ?? MethodType.MASK, source: initialState?.source ?? '', placeholder: initialState?.placeholder ?? '', pattern: initialState?.pattern ?? '', }; } getRequiredValues(values: Values) { const {type} = values; const requiredValues: Array> = ['type', 'method', 'source']; if (type === RuleType.PATTERN) { requiredValues.push('pattern'); } return requiredValues; } clearError(field: F) { this.setState(prevState => ({ errors: omit(prevState.errors, field), })); } async loadSourceSuggestions() { const {orgSlug, projectId, api} = this.props; const {eventId} = this.state; if (!eventId.value) { this.setState(prevState => ({ sourceSuggestions: valueSuggestions, eventId: { ...prevState.eventId, status: EventIdStatus.UNDEFINED, }, })); return; } this.setState(prevState => ({ sourceSuggestions: valueSuggestions, eventId: { ...prevState.eventId, status: EventIdStatus.LOADING, }, })); try { const query: {eventId: string; projectId?: string} = {eventId: eventId.value}; if (projectId) { query.projectId = projectId; } const rawSuggestions = await api.requestPromise( `/organizations/${orgSlug}/data-scrubbing-selector-suggestions/`, {query} ); const sourceSuggestions: SourceSuggestions = rawSuggestions.suggestions; if (sourceSuggestions && sourceSuggestions.length > 0) { this.setState(prevState => ({ sourceSuggestions, eventId: { ...prevState.eventId, status: EventIdStatus.LOADED, }, })); return; } this.setState(prevState => ({ sourceSuggestions: valueSuggestions, eventId: { ...prevState.eventId, status: EventIdStatus.NOT_FOUND, }, })); } catch { this.setState(prevState => ({ eventId: { ...prevState.eventId, status: EventIdStatus.ERROR, }, })); } } convertRequestError(error: ReturnType) { switch (error.type) { case ErrorType.INVALID_SELECTOR: this.setState(prevState => ({ errors: { ...prevState.errors, source: error.message, }, })); break; case ErrorType.REGEX_PARSE: this.setState(prevState => ({ errors: { ...prevState.errors, pattern: error.message, }, })); break; default: addErrorMessage(error.message); } } handleChange = >(field: K, value: R[K]) => { const values = { ...this.state.values, [field]: value, }; if (values.type !== RuleType.PATTERN && values.pattern) { values.pattern = ''; } if (values.method !== MethodType.REPLACE && values.placeholder) { values.placeholder = ''; } this.setState(prevState => ({ values, requiredValues: this.getRequiredValues(values), errors: omit(prevState.errors, field), })); }; handleSave = async () => { const {endpoint, api, onSubmitSuccess, closeModal, onGetNewRules} = this.props; const newRules = onGetNewRules(this.state.values); try { const data = await submitRules(api, endpoint, newRules); onSubmitSuccess(data); closeModal(); } catch (error) { this.convertRequestError(handleError(error)); } }; handleValidateForm() { const {values, requiredValues} = this.state; const isFormValid = requiredValues.every(requiredValue => !!values[requiredValue]); this.setState({isFormValid}); } handleValidate = (field: K) => () => { const isFieldValueEmpty = !this.state.values[field].trim(); const fieldErrorAlreadyExist = this.state.errors[field]; if (isFieldValueEmpty && fieldErrorAlreadyExist) { return; } if (isFieldValueEmpty && !fieldErrorAlreadyExist) { this.setState(prevState => ({ errors: { ...prevState.errors, [field]: t('Field Required'), }, })); return; } if (!isFieldValueEmpty && fieldErrorAlreadyExist) { this.clearError(field); } }; handleUpdateEventId = (eventId: string) => { if (eventId === this.state.eventId.value) { return; } this.setState({ eventId: {value: eventId, status: EventIdStatus.UNDEFINED}, }); }; render() { const {values, errors, isFormValid, eventId, sourceSuggestions} = this.state; const {title} = this.props; return ( } /> ); } } export default ModalManager;