123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288 |
- 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/organization';
- 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<Props, State> {
- 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 (
- <Fragment>
- <HeaderWithHelp docsUrl="https://docs.sentry.io/product/integrations/cloud-monitoring/aws-lambda/" />
- <StyledList symbol="colored-numeric" initialCounterValue={initialStepNumber}>
- <ListItem>
- <h3>{t("Add Sentry's CloudFormation")}</h3>
- <StyledButton
- priority="primary"
- onClick={this.trackOpenCloudFormation}
- external
- href={this.cloudformationUrl}
- >
- {t('Go to AWS')}
- </StyledButton>
- {!showInputs && (
- <Fragment>
- <p>
- {t(
- "Once you've created Sentry's CloudFormation stack (or if you already have one) press the button below to continue."
- )}
- </p>
- <Button name="showInputs" onClick={this.handleChangeShowInputs}>
- {t("I've created the stack")}
- </Button>
- </Fragment>
- )}
- </ListItem>
- {showInputs ? (
- <ListItem>
- <h3>{t('Add AWS Account Information')}</h3>
- <TextField
- name="accountNumber"
- value={accountNumber}
- onChange={this.handleChangeArn}
- onBlur={this.validateAccountNumber}
- error={accountNumberError}
- inline={false}
- stacked
- label={t('AWS Account ID')}
- showHelpInTooltip
- help={t(
- 'Your Account ID can be found on the right side of the header in AWS'
- )}
- />
- <SelectField
- name="region"
- value={region}
- onChange={this.handleChangeRegion}
- options={this.regionOptions}
- allowClear={false}
- inline={false}
- stacked
- label={t('AWS Region')}
- showHelpInTooltip
- help={t(
- 'Your current region can be found on the right side of the header in AWS'
- )}
- />
- <TextField
- name="awsExternalId"
- value={awsExternalId}
- onChange={this.handleChangeExternalId}
- inline={false}
- stacked
- error={awsExternalId ? '' : t('External ID Required')}
- label={t('External ID')}
- showHelpInTooltip
- help={t(
- 'Do not edit unless you are copying from a previously created CloudFormation stack'
- )}
- />
- </ListItem>
- ) : (
- <Fragment />
- )}
- </StyledList>
- <FooterWithButtons
- buttonText={t('Next')}
- onClick={this.handleSubmit}
- disabled={submitting || !this.formValid}
- />
- </Fragment>
- );
- }
- }
- const StyledList = styled(List)`
- padding: 100px 50px 50px 50px;
- `;
- const StyledButton = styled(Button)`
- margin-bottom: 20px;
- `;
|