import {ChangeEvent, Fragment, ReactNode} from 'react'; import {browserHistory, RouteComponentProps} from 'react-router'; import {components} from 'react-select'; import styled from '@emotion/styled'; import classNames from 'classnames'; import {Location} from 'history'; import cloneDeep from 'lodash/cloneDeep'; import omit from 'lodash/omit'; import set from 'lodash/set'; import { addErrorMessage, addLoadingMessage, addSuccessMessage, } from 'sentry/actionCreators/indicator'; import {updateOnboardingTask} from 'sentry/actionCreators/onboardingTasks'; import Access from 'sentry/components/acl/access'; import Alert from 'sentry/components/alert'; import Button from 'sentry/components/button'; import Confirm from 'sentry/components/confirm'; import Input from 'sentry/components/forms/controls/input'; import Field from 'sentry/components/forms/field'; import FieldHelp from 'sentry/components/forms/field/fieldHelp'; import Form from 'sentry/components/forms/form'; import FormField from 'sentry/components/forms/formField'; import SelectControl from 'sentry/components/forms/selectControl'; import SelectField from 'sentry/components/forms/selectField'; import TeamSelector from 'sentry/components/forms/teamSelector'; import IdBadge from 'sentry/components/idBadge'; import * as Layout from 'sentry/components/layouts/thirds'; import List from 'sentry/components/list'; import ListItem from 'sentry/components/list/listItem'; import LoadingMask from 'sentry/components/loadingMask'; import {Panel, PanelBody} from 'sentry/components/panels'; import {ALL_ENVIRONMENTS_KEY} from 'sentry/constants'; import {IconChevron} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import ConfigStore from 'sentry/stores/configStore'; import HookStore from 'sentry/stores/hookStore'; import space from 'sentry/styles/space'; import {Environment, OnboardingTaskKey, Organization, Project, Team} from 'sentry/types'; import { IssueAlertRule, IssueAlertRuleAction, IssueAlertRuleActionTemplate, IssueAlertRuleConditionTemplate, UnsavedIssueAlertRule, } from 'sentry/types/alerts'; import {metric} from 'sentry/utils/analytics'; import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent'; import {getDisplayName} from 'sentry/utils/environment'; import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser'; import recreateRoute from 'sentry/utils/recreateRoute'; import routeTitleGen from 'sentry/utils/routeTitle'; import withExperiment from 'sentry/utils/withExperiment'; import withOrganization from 'sentry/utils/withOrganization'; import withProjects from 'sentry/utils/withProjects'; import { CHANGE_ALERT_CONDITION_IDS, CHANGE_ALERT_PLACEHOLDERS_LABELS, } from 'sentry/views/alerts/utils/constants'; import AsyncView from 'sentry/views/asyncView'; import RuleNodeList from './ruleNodeList'; import SetupAlertIntegrationButton from './setupAlertIntegrationButton'; const FREQUENCY_OPTIONS = [ {value: '5', label: t('5 minutes')}, {value: '10', label: t('10 minutes')}, {value: '30', label: t('30 minutes')}, {value: '60', label: t('60 minutes')}, {value: '180', label: t('3 hours')}, {value: '720', label: t('12 hours')}, {value: '1440', label: t('24 hours')}, {value: '10080', label: t('1 week')}, {value: '43200', label: t('30 days')}, ]; const ACTION_MATCH_OPTIONS = [ {value: 'all', label: t('all')}, {value: 'any', label: t('any')}, {value: 'none', label: t('none')}, ]; const ACTION_MATCH_OPTIONS_MIGRATED = [ {value: 'all', label: t('all')}, {value: 'any', label: t('any')}, ]; const defaultRule: UnsavedIssueAlertRule = { actionMatch: 'all', filterMatch: 'all', actions: [], conditions: [], filters: [], name: '', frequency: 30, environment: ALL_ENVIRONMENTS_KEY, }; const POLLING_MAX_TIME_LIMIT = 3 * 60000; type ConditionOrActionProperty = 'conditions' | 'actions' | 'filters'; type RuleTaskResponse = { status: 'pending' | 'failed' | 'success'; error?: string; rule?: IssueAlertRule; }; type RouteParams = {orgId: string; projectId?: string; ruleId?: string}; type Props = { experimentAssignment: 0 | 1; location: Location; logExperiment: () => void; organization: Organization; project: Project; projects: Project[]; userTeamIds: string[]; loadingProjects?: boolean; onChangeTitle?: (data: string) => void; } & RouteComponentProps; type State = AsyncView['state'] & { configs: { actions: IssueAlertRuleActionTemplate[]; conditions: IssueAlertRuleConditionTemplate[]; filters: IssueAlertRuleConditionTemplate[]; } | null; detailedError: null | { [key: string]: string[]; }; environments: Environment[] | null; project: Project; uuid: null | string; duplicateTargetRule?: UnsavedIssueAlertRule | IssueAlertRule | null; rule?: UnsavedIssueAlertRule | IssueAlertRule | null; }; function isSavedAlertRule(rule: State['rule']): rule is IssueAlertRule { return rule?.hasOwnProperty('id') ?? false; } class IssueRuleEditor extends AsyncView { pollingTimeout: number | undefined = undefined; get isDuplicateRule(): boolean { const {location, organization} = this.props; const createFromDuplicate = location?.query.createFromDuplicate === 'true'; const hasDuplicateAlertRules = organization.features.includes('duplicate-alert-rule'); return ( hasDuplicateAlertRules && createFromDuplicate && location?.query.duplicateRuleId ); } get hasAlertWizardV3(): boolean { return this.props.organization.features.includes('alert-wizard-v3'); } componentWillUnmount() { window.clearTimeout(this.pollingTimeout); } componentDidMount() { const {params, organization, experimentAssignment, logExperiment} = this.props; // only new rules if (params.ruleId) { return; } // check if there is a callback registered const callback = HookStore.get('callback:default-action-alert-rule')[0]; if (!callback) { return; } // let hook decide when we want to select a default alert rule callback((showDefaultAction: boolean) => { if (showDefaultAction) { const user = ConfigStore.get('user'); const {rule} = this.state; // always log the experiment if we meet the basic requirements decided by the hook logExperiment(); if (experimentAssignment) { // this will add a default alert rule action // to send notifications in this.setState({ rule: { ...rule, actions: [ { id: 'sentry.mail.actions.NotifyEmailAction', targetIdentifier: user.id, targetType: 'Member', } as any, // Need to fix IssueAlertRuleAction typing ], } as UnsavedIssueAlertRule, }); } } }, organization); } componentDidUpdate(_prevProps: Props, prevState: State) { if (prevState.project.id === this.state.project.id) { return; } this.fetchEnvironments(); } getTitle() { const {organization} = this.props; const {rule, project} = this.state; const ruleName = rule?.name; return routeTitleGen( ruleName ? t('Alert %s', ruleName) : '', organization.slug, false, project?.slug ); } getDefaultState() { const {userTeamIds, project} = this.props; const defaultState = { ...super.getDefaultState(), configs: null, detailedError: null, rule: {...defaultRule}, environments: [], uuid: null, project, }; const projectTeamIds = new Set(project.teams.map(({id}) => id)); const userTeamId = userTeamIds.find(id => projectTeamIds.has(id)) ?? null; defaultState.rule.owner = userTeamId && `team:${userTeamId}`; return defaultState; } getEndpoints(): ReturnType { const { organization, location: {query}, params: {ruleId, orgId}, } = this.props; // project in state isn't initialized when getEndpoints is first called const project = this.state?.project ?? this.props.project; const hasDuplicateAlertRules = organization.features.includes('duplicate-alert-rule'); const endpoints = [ ['environments', `/projects/${orgId}/${project.slug}/environments/`], ['configs', `/projects/${orgId}/${project.slug}/rules/configuration/`], ]; if (ruleId) { endpoints.push(['rule', `/projects/${orgId}/${project.slug}/rules/${ruleId}/`]); } if ( hasDuplicateAlertRules && !ruleId && query.createFromDuplicate && query.duplicateRuleId ) { endpoints.push([ 'duplicateTargetRule', `/projects/${orgId}/${project.slug}/rules/${query.duplicateRuleId}/`, ]); } return endpoints as [string, string][]; } onRequestSuccess({stateKey, data}) { if (stateKey === 'rule' && data.name) { this.props.onChangeTitle?.(data.name); } if (stateKey === 'duplicateTargetRule') { this.setState({ rule: { ...omit(data, ['id']), name: data.name + ' copy', } as UnsavedIssueAlertRule, }); } } onLoadAllEndpointsSuccess() { const {rule} = this.state; if (rule) { ((rule as IssueAlertRule)?.errors || []).map(({detail}) => addErrorMessage(detail, {append: true}) ); } } pollHandler = async (quitTime: number) => { if (Date.now() > quitTime) { addErrorMessage(t('Looking for that channel took too long :(')); this.setState({loading: false}); return; } const {organization} = this.props; const {uuid, project} = this.state; const origRule = this.state.rule; try { const response: RuleTaskResponse = await this.api.requestPromise( `/projects/${organization.slug}/${project.slug}/rule-task/${uuid}/` ); const {status, rule, error} = response; if (status === 'pending') { window.clearTimeout(this.pollingTimeout); this.pollingTimeout = window.setTimeout(() => { this.pollHandler(quitTime); }, 1000); return; } if (status === 'failed') { this.setState({ detailedError: {actions: [error ? error : t('An error occurred')]}, loading: false, }); this.handleRuleSaveFailure(t('An error occurred')); } if (rule) { const ruleId = isSavedAlertRule(origRule) ? `${origRule.id}/` : ''; const isNew = !ruleId; this.handleRuleSuccess(isNew, rule); } } catch { this.handleRuleSaveFailure(t('An error occurred')); this.setState({loading: false}); } }; fetchEnvironments() { const { params: {orgId}, } = this.props; const {project} = this.state; this.api .requestPromise(`/projects/${orgId}/${project.slug}/environments/`, { query: { visibility: 'visible', }, }) .then(response => this.setState({environments: response})) .catch(_err => addErrorMessage(t('Unable to fetch environments'))); } fetchStatus() { // pollHandler calls itself until it gets either a success // or failed status but we don't want to poll forever so we pass // in a hard stop time of 3 minutes before we bail. const quitTime = Date.now() + POLLING_MAX_TIME_LIMIT; window.clearTimeout(this.pollingTimeout); this.pollingTimeout = window.setTimeout(() => { this.pollHandler(quitTime); }, 1000); } handleRuleSuccess = (isNew: boolean, rule: IssueAlertRule) => { const {organization, router} = this.props; const {project} = this.state; this.setState({detailedError: null, loading: false, rule}); // The onboarding task will be completed on the server side when the alert // is created updateOnboardingTask(null, organization, { task: OnboardingTaskKey.ALERT_RULE, status: 'complete', }); metric.endTransaction({name: 'saveAlertRule'}); router.push({ pathname: `/organizations/${organization.slug}/alerts/rules/${project.slug}/${rule.id}/details/`, }); addSuccessMessage(isNew ? t('Created alert rule') : t('Updated alert rule')); }; handleRuleSaveFailure(msg: ReactNode) { addErrorMessage(msg); metric.endTransaction({name: 'saveAlertRule'}); } handleSubmit = async () => { const {project, rule} = this.state; const ruleId = isSavedAlertRule(rule) ? `${rule.id}/` : ''; const isNew = !ruleId; const {organization} = this.props; const endpoint = `/projects/${organization.slug}/${project.slug}/rules/${ruleId}`; if (rule && rule.environment === ALL_ENVIRONMENTS_KEY) { delete rule.environment; } addLoadingMessage(); try { const transaction = metric.startTransaction({name: 'saveAlertRule'}); transaction.setTag('type', 'issue'); transaction.setTag('operation', isNew ? 'create' : 'edit'); if (rule) { for (const action of rule.actions) { // Grab the last part of something like 'sentry.mail.actions.NotifyEmailAction' const splitActionId = action.id.split('.'); const actionName = splitActionId[splitActionId.length - 1]; if (actionName === 'SlackNotifyServiceAction') { transaction.setTag(actionName, true); } } transaction.setData('actions', rule.actions); } const [data, , resp] = await this.api.requestPromise(endpoint, { includeAllArgs: true, method: isNew ? 'POST' : 'PUT', data: rule, query: { duplicateRule: this.isDuplicateRule ? 'true' : 'false', wizardV3: this.hasAlertWizardV3 ? 'true' : 'false', }, }); // if we get a 202 back it means that we have an async task // running to lookup and verify the channel id for Slack. if (resp?.status === 202) { this.setState({detailedError: null, loading: true, uuid: data.uuid}); this.fetchStatus(); addLoadingMessage(t('Looking through all your channels...')); } else { this.handleRuleSuccess(isNew, data); } } catch (err) { this.setState({ detailedError: err.responseJSON || {__all__: 'Unknown error'}, loading: false, }); this.handleRuleSaveFailure(t('An error occurred')); } }; handleDeleteRule = async () => { const {project, rule} = this.state; const ruleId = isSavedAlertRule(rule) ? `${rule.id}/` : ''; const isNew = !ruleId; const {organization} = this.props; if (isNew) { return; } const endpoint = `/projects/${organization.slug}/${project.slug}/rules/${ruleId}`; addLoadingMessage(t('Deleting...')); try { await this.api.requestPromise(endpoint, { method: 'DELETE', }); addSuccessMessage(t('Deleted alert rule')); browserHistory.replace(recreateRoute('', {...this.props, stepBack: -2})); } catch (err) { this.setState({ detailedError: err.responseJSON || {__all__: 'Unknown error'}, }); addErrorMessage(t('There was a problem deleting the alert')); } }; handleCancel = () => { const {organization, router} = this.props; router.push(`/organizations/${organization.slug}/alerts/rules/`); }; hasError = (field: string) => { const {detailedError} = this.state; if (!detailedError) { return false; } return detailedError.hasOwnProperty(field); }; handleEnvironmentChange = (val: string) => { // If 'All Environments' is selected the value should be null if (val === ALL_ENVIRONMENTS_KEY) { this.handleChange('environment', null); } else { this.handleChange('environment', val); } }; handleChange = (prop: T, val: IssueAlertRule[T]) => { this.setState(prevState => { const clonedState = cloneDeep(prevState); set(clonedState, `rule[${prop}]`, val); return {...clonedState, detailedError: omit(prevState.detailedError, prop)}; }); }; handlePropertyChange = ( type: ConditionOrActionProperty, idx: number, prop: T, val: IssueAlertRuleAction[T] ) => { this.setState(prevState => { const clonedState = cloneDeep(prevState); set(clonedState, `rule[${type}][${idx}][${prop}]`, val); return clonedState; }); }; getInitialValue = (type: ConditionOrActionProperty, id: string) => { const configuration = this.state.configs?.[type]?.find(c => c.id === id); const hasChangeAlerts = configuration?.id && this.props.organization.features.includes('change-alerts') && CHANGE_ALERT_CONDITION_IDS.includes(configuration.id); return configuration?.formFields ? Object.fromEntries( Object.entries(configuration.formFields) // TODO(ts): Doesn't work if I cast formField as IssueAlertRuleFormField .map(([key, formField]: [string, any]) => [ key, hasChangeAlerts && key === 'interval' ? '1h' : formField?.initial ?? formField?.choices?.[0]?.[0], ]) .filter(([, initial]) => !!initial) ) : {}; }; handleResetRow = ( type: ConditionOrActionProperty, idx: number, prop: T, val: IssueAlertRuleAction[T] ) => { this.setState(prevState => { const clonedState = cloneDeep(prevState); // Set initial configuration, but also set const id = (clonedState.rule as IssueAlertRule)[type][idx].id; const newRule = { ...this.getInitialValue(type, id), id, [prop]: val, }; set(clonedState, `rule[${type}][${idx}]`, newRule); return clonedState; }); }; handleAddRow = (type: ConditionOrActionProperty, id: string) => { this.setState(prevState => { const clonedState = cloneDeep(prevState); // Set initial configuration const newRule = { ...this.getInitialValue(type, id), id, }; const newTypeList = prevState.rule ? prevState.rule[type] : []; set(clonedState, `rule[${type}]`, [...newTypeList, newRule]); return clonedState; }); const {organization} = this.props; const {project} = this.state; trackAdvancedAnalyticsEvent('edit_alert_rule.add_row', { organization, project_id: project.id, type, name: id, }); }; handleDeleteRow = (type: ConditionOrActionProperty, idx: number) => { this.setState(prevState => { const clonedState = cloneDeep(prevState); const newTypeList = prevState.rule ? prevState.rule[type] : []; if (prevState.rule) { newTypeList.splice(idx, 1); } set(clonedState, `rule[${type}]`, newTypeList); return clonedState; }); }; handleAddCondition = (id: string) => this.handleAddRow('conditions', id); handleAddAction = (id: string) => this.handleAddRow('actions', id); handleAddFilter = (id: string) => this.handleAddRow('filters', id); handleDeleteCondition = (ruleIndex: number) => this.handleDeleteRow('conditions', ruleIndex); handleDeleteAction = (ruleIndex: number) => this.handleDeleteRow('actions', ruleIndex); handleDeleteFilter = (ruleIndex: number) => this.handleDeleteRow('filters', ruleIndex); handleChangeConditionProperty = (ruleIndex: number, prop: string, val: string) => this.handlePropertyChange('conditions', ruleIndex, prop, val); handleChangeActionProperty = (ruleIndex: number, prop: string, val: string) => this.handlePropertyChange('actions', ruleIndex, prop, val); handleChangeFilterProperty = (ruleIndex: number, prop: string, val: string) => this.handlePropertyChange('filters', ruleIndex, prop, val); handleResetCondition = (ruleIndex: number, prop: string, value: string) => this.handleResetRow('conditions', ruleIndex, prop, value); handleResetAction = (ruleIndex: number, prop: string, value: string) => this.handleResetRow('actions', ruleIndex, prop, value); handleResetFilter = (ruleIndex: number, prop: string, value: string) => this.handleResetRow('filters', ruleIndex, prop, value); handleValidateRuleName = () => { const isRuleNameEmpty = !this.state.rule?.name.trim(); if (!isRuleNameEmpty) { return; } this.setState(prevState => ({ detailedError: { ...prevState.detailedError, name: [t('Field Required')], }, })); }; getConditions() { const {organization} = this.props; if (!organization.features.includes('change-alerts')) { return this.state.configs?.conditions ?? null; } return ( this.state.configs?.conditions?.map(condition => CHANGE_ALERT_CONDITION_IDS.includes(condition.id) ? ({ ...condition, label: CHANGE_ALERT_PLACEHOLDERS_LABELS[condition.id], } as IssueAlertRuleConditionTemplate) : condition ) ?? null ); } getTeamId = () => { const {rule} = this.state; const owner = rule?.owner; // ownership follows the format team:, just grab the id return owner && owner.split(':')[1]; }; handleOwnerChange = ({value}: {value: string}) => { const ownerValue = value && `team:${value}`; this.handleChange('owner', ownerValue); }; renderLoading() { return this.renderBody(); } renderError() { return ( {t( 'Unable to access this alert rule -- check to make sure you have the correct permissions' )} ); } renderRuleName(disabled: boolean) { const {rule, detailedError} = this.state; const {name} = rule || {}; return ( ) => this.handleChange('name', event.target.value) } onBlur={this.handleValidateRuleName} disabled={disabled} /> ); } renderTeamSelect(disabled: boolean) { const {rule, project} = this.state; const ownerId = rule?.owner?.split(':')[1]; return ( team.isMember || team.id === ownerId} useId includeUnassigned disabled={disabled} /> ); } renderIdBadge(project: Project) { return ( ); } renderProjectSelect(disabled: boolean) { const {project: _selectedProject, projects, organization} = this.props; const hasOpenMembership = organization.features.includes('open-membership'); const myProjects = projects.filter(project => project.hasAccess && project.isMember); const allProjects = projects.filter( project => project.hasAccess && !project.isMember ); const myProjectOptions = myProjects.map(myProject => ({ value: myProject.id, label: myProject.slug, leadingItems: this.renderIdBadge(myProject), })); const openMembershipProjects = [ { label: t('My Projects'), options: myProjectOptions, }, { label: t('All Projects'), options: allProjects.map(allProject => ({ value: allProject.id, label: allProject.slug, leadingItems: this.renderIdBadge(allProject), })), }, ]; const projectOptions = hasOpenMembership || isActiveSuperuser() ? openMembershipProjects : myProjectOptions; return ( {({onChange, onBlur, model}) => { const selectedProject = projects.find(({id}) => id === model.getValue('projectId')) || _selectedProject; return ( ({ ...provided, marginBottom: `${space(1)}`, }), }} options={projectOptions} onChange={({value}: {value: Project['id']}) => { // if the current owner/team isn't part of project selected, update to the first available team const nextSelectedProject = projects.find(({id}) => id === value) ?? selectedProject; const ownerId: String | undefined = model .getValue('owner') ?.split(':')[1]; if ( ownerId && nextSelectedProject.teams.find(({id}) => id === ownerId) === undefined && nextSelectedProject.teams.length ) { this.handleOwnerChange({value: nextSelectedProject.teams[0].id}); } this.setState({project: nextSelectedProject}); onChange(value, {}); onBlur(value, {}); }} components={{ SingleValue: containerProps => ( ), }} /> ); }} ); } renderActionInterval(disabled: boolean) { const {rule} = this.state; const {frequency} = rule || {}; return ( this.handleChange('frequency', val)} disabled={disabled} flexibleControlStateSize={this.hasAlertWizardV3 ? true : undefined} /> ); } renderBody() { const {organization} = this.props; const {environments, project, rule, detailedError, loading} = this.state; const {actions, filters, conditions, frequency} = rule || {}; const environmentOptions = [ { value: ALL_ENVIRONMENTS_KEY, label: t('All Environments'), }, ...(environments?.map(env => ({value: env.name, label: getDisplayName(env)})) ?? []), ]; const environment = !rule || !rule.environment ? ALL_ENVIRONMENTS_KEY : rule.environment; // Note `key` on `
` below is so that on initial load, we show // the form with a loading mask on top of it, but force a re-render by using // a different key when we have fetched the rule so that form inputs are filled in return ( {({hasAccess}) => { // check if superuser or if user is on the alert's team const disabled = loading || !(isActiveSuperuser() || hasAccess); return (
) : null } > {loading && } {t('Add alert settings')} {this.hasAlertWizardV3 ? ( this.handleEnvironmentChange(val)} disabled={disabled} flexibleControlStateSize /> {this.renderProjectSelect(disabled)} ) : ( this.handleEnvironmentChange(val)} disabled={disabled} /> {this.renderTeamSelect(disabled)} {this.renderRuleName(disabled)} )} {t('Set conditions')} {tct( '[when:When] an event is captured by Sentry and [selector] of the following happens', { when: , selector: ( ({ ...provided, minHeight: '20px', height: '20px', }), }} isSearchable={false} isClearable={false} name="actionMatch" required flexibleControlStateSize options={ACTION_MATCH_OPTIONS_MIGRATED} onChange={val => this.handleChange('actionMatch', val) } disabled={disabled} /> ), } )} {detailedError?.conditions[0]} ) } /> {tct('[if:If] [selector] of these filters match', { if: , selector: ( ({ ...provided, minHeight: '20px', height: '20px', }), }} isSearchable={false} isClearable={false} name="filterMatch" required flexibleControlStateSize options={ACTION_MATCH_OPTIONS} onChange={val => this.handleChange('filterMatch', val) } disabled={disabled} /> ), })} {detailedError?.filters[0]} ) } /> {tct('[then:Then] perform these actions', { then: , })} {detailedError?.actions[0]} ) } /> {t('Set action interval')} {t('Perform the actions above once this often for an issue')} {this.hasAlertWizardV3 ? ( this.renderActionInterval(disabled) ) : ( {this.renderActionInterval(disabled)} )} {this.hasAlertWizardV3 && ( {t('Establish ownership')} {this.renderRuleName(disabled)} {this.renderTeamSelect(disabled)} )}
); }}
); } } export default withExperiment(withOrganization(withProjects(IssueRuleEditor)), { experiment: 'DefaultIssueAlertActionExperiment', injectLogExperiment: true, }); // TODO(ts): Understand why styled is not correctly inheriting props here const StyledForm = styled(Form)` position: relative; `; const ConditionsPanel = styled(Panel)` padding-top: ${space(0.5)}; padding-bottom: ${space(2)}; `; const StyledAlert = styled(Alert)` margin-bottom: 0; `; const StyledListItem = styled(ListItem)` margin: ${space(2)} 0 ${space(1)} 0; font-size: ${p => p.theme.fontSizeExtraLarge}; `; const StyledFieldHelp = styled(FieldHelp)` margin-top: 0; `; const SetConditionsListItem = styled(StyledListItem)` display: flex; justify-content: space-between; `; const Step = styled('div')` position: relative; display: flex; align-items: flex-start; margin: ${space(4)} ${space(4)} ${space(3)} ${space(1)}; `; const StepContainer = styled('div')` position: relative; display: flex; align-items: flex-start; flex-grow: 1; `; const StepContent = styled('div')` flex-grow: 1; `; const StepConnector = styled('div')` position: absolute; height: 100%; top: 28px; left: 19px; border-right: 1px ${p => p.theme.gray200} dashed; `; const StepLead = styled('div')` margin-bottom: ${space(0.5)}; `; const ChevronContainer = styled('div')` display: flex; align-items: center; padding: ${space(0.5)} ${space(1.5)}; `; const Badge = styled('span')` display: inline-block; min-width: 56px; background-color: ${p => p.theme.purple300}; padding: 0 ${space(0.75)}; border-radius: ${p => p.theme.borderRadius}; color: ${p => p.theme.white}; text-transform: uppercase; text-align: center; font-size: ${p => p.theme.fontSizeMedium}; font-weight: 600; line-height: 1.5; `; const EmbeddedWrapper = styled('div')` display: inline-block; margin: 0 ${space(0.5)}; width: 80px; `; const EmbeddedSelectField = styled(SelectField)` padding: 0; font-weight: normal; text-transform: none; `; const SemiTransparentLoadingMask = styled(LoadingMask)` opacity: 0.6; z-index: 1; /* Needed so that it sits above form elements */ `; const SettingsContainer = styled('div')` display: grid; grid-template-columns: 1fr 1fr; gap: ${space(1)}; `; const StyledField = styled(Field)<{extraMargin?: boolean; hasAlertWizardV3?: boolean}>` :last-child { padding-bottom: ${space(2)}; } ${p => p.hasAlertWizardV3 && ` border-bottom: none; padding: 0; & > div { padding: 0; width: 100%; } margin-bottom: ${p.extraMargin ? '60px' : space(1)}; `} `; const StyledSelectField = styled(SelectField)<{hasAlertWizardV3?: boolean}>` ${p => p.hasAlertWizardV3 && ` border-bottom: none; padding: 0; & > div { padding: 0; width: 100%; } margin-bottom: ${space(1)}; `} `; const Main = styled(Layout.Main)` padding: ${space(2)} ${space(4)}; `;