import {Component, Fragment} from 'react'; import styled from '@emotion/styled'; import debounce from 'lodash/debounce'; import * as qs from 'query-string'; import {addErrorMessage, addLoadingMessage} from 'sentry/actionCreators/indicator'; import Button from 'sentry/components/actions/button'; import SelectField from 'sentry/components/forms/fields/selectField'; import TextField from 'sentry/components/forms/fields/textField'; import List from 'sentry/components/list'; import ListItem from 'sentry/components/list/listItem'; import {t} from 'sentry/locale'; import type {Organization} from 'sentry/types'; import {uniqueId} from 'sentry/utils/guid'; import {trackIntegrationAnalytics} from 'sentry/utils/integrationUtil'; import FooterWithButtons from './components/footerWithButtons'; import HeaderWithHelp from './components/headerWithHelp'; // let the browser generate and store the external ID // this way the same user always has the same external ID if they restart the pipeline const ID_NAME = 'AWS_EXTERNAL_ID'; const getAwsExternalId = () => { let awsExternalId = window.localStorage.getItem(ID_NAME); if (!awsExternalId) { awsExternalId = uniqueId(); window.localStorage.setItem(ID_NAME, awsExternalId); } return awsExternalId; }; const accountNumberRegex = /^\d{12}$/; const testAccountNumber = (arn: string) => accountNumberRegex.test(arn); type Props = { baseCloudformationUrl: string; initialStepNumber: number; organization: Organization; regionList: string[]; stackName: string; templateUrl: string; accountNumber?: string; awsExternalId?: string; error?: string; region?: string; }; type State = { accountNumber?: string; accountNumberError?: string; awsExternalId?: string; region?: string; showInputs?: boolean; submitting?: boolean; }; export default class AwsLambdaCloudformation extends Component { state: State = { accountNumber: this.props.accountNumber, region: this.props.region, awsExternalId: this.props.awsExternalId ?? getAwsExternalId(), showInputs: !!this.props.awsExternalId, }; componentDidMount() { // show the error if we have it const {error} = this.props; if (error) { addErrorMessage(error, {duration: 10000}); } } get initialData() { const {region, accountNumber} = this.props; const {awsExternalId} = this.state; return { awsExternalId, region, accountNumber, }; } get cloudformationUrl() { // generate the cloudformation URL using the params we get from the server // and the external id we generate const {baseCloudformationUrl, templateUrl, stackName} = this.props; // always us the generated AWS External ID in local storage const awsExternalId = getAwsExternalId(); const query = qs.stringify({ templateURL: templateUrl, stackName, param_ExternalId: awsExternalId, }); return `${baseCloudformationUrl}?${query}`; } get regionOptions() { return this.props.regionList.map(region => ({value: region, label: region})); } handleSubmit = (e: React.MouseEvent) => { this.setState({submitting: true}); e.preventDefault(); // use the external ID from the form on on the submission const {accountNumber, region, awsExternalId} = this.state; const data = { accountNumber, region, awsExternalId, }; addLoadingMessage(t('Submitting\u2026')); const { location: {origin}, } = window; // redirect to the extensions endpoint with the form fields as query params // this is needed so we don't restart the pipeline loading from the original // OrganizationIntegrationSetupView route const newUrl = `${origin}/extensions/aws_lambda/setup/?${qs.stringify(data)}`; window.location.assign(newUrl); }; validateAccountNumber = (value: string) => { // validate the account number let accountNumberError = ''; if (!value) { accountNumberError = t('Account ID required'); } else if (!testAccountNumber(value)) { accountNumberError = t('Invalid Account ID'); } this.setState({accountNumberError}); }; handleChangeArn = (accountNumber: string) => { this.debouncedTrackValueChanged('accountNumber'); // reset the error if we ever get a valid account number if (testAccountNumber(accountNumber)) { this.setState({accountNumberError: ''}); } this.setState({accountNumber}); }; handleChangeRegion = (region: string) => { this.debouncedTrackValueChanged('region'); this.setState({region}); }; handleChangeExternalId = (awsExternalId: string) => { this.debouncedTrackValueChanged('awsExternalId'); awsExternalId = awsExternalId.trim(); this.setState({awsExternalId}); }; handleChangeShowInputs = () => { this.setState({showInputs: true}); trackIntegrationAnalytics('integrations.installation_input_value_changed', { integration: 'aws_lambda', integration_type: 'first_party', field_name: 'showInputs', organization: this.props.organization, }); }; get formValid() { const {accountNumber, region, awsExternalId} = this.state; return !!region && testAccountNumber(accountNumber || '') && !!awsExternalId; } // debounce so we don't send a request on every input change debouncedTrackValueChanged = debounce((fieldName: string) => { trackIntegrationAnalytics('integrations.installation_input_value_changed', { integration: 'aws_lambda', integration_type: 'first_party', field_name: fieldName, organization: this.props.organization, }); }, 200); trackOpenCloudFormation = () => { trackIntegrationAnalytics('integrations.cloudformation_link_clicked', { integration: 'aws_lambda', integration_type: 'first_party', organization: this.props.organization, }); }; render() { const {initialStepNumber} = this.props; const { accountNumber, region, accountNumberError, submitting, awsExternalId, showInputs, } = this.state; return (

{t("Add Sentry's CloudFormation")}

{t('Go to AWS')} {!showInputs && (

{t( "Once you've created Sentry's CloudFormation stack (or if you already have one) press the button below to continue." )}

)}
{showInputs ? (

{t('Add AWS Account Information')}

) : ( )}
); } } const StyledList = styled(List)` padding: 100px 50px 50px 50px; `; const StyledButton = styled(Button)` margin-bottom: 20px; `;