123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371 |
- import {Component, Fragment} from 'react';
- import {browserHistory, WithRouterProps} from 'react-router';
- import styled from '@emotion/styled';
- import * as Sentry from '@sentry/react';
- import {PlatformIcon} from 'platformicons';
- import {openCreateTeamModal} from 'sentry/actionCreators/modal';
- import {Alert} from 'sentry/components/alert';
- import {Button} from 'sentry/components/button';
- import Input from 'sentry/components/input';
- import * as Layout from 'sentry/components/layouts/thirds';
- import ExternalLink from 'sentry/components/links/externalLink';
- import PlatformPicker from 'sentry/components/platformPicker';
- import TeamSelector from 'sentry/components/teamSelector';
- import categoryList from 'sentry/data/platformCategories';
- import {IconAdd} from 'sentry/icons';
- import {t, tct} from 'sentry/locale';
- import ProjectsStore from 'sentry/stores/projectsStore';
- import {space} from 'sentry/styles/space';
- import {Organization, Team} from 'sentry/types';
- import {trackAnalytics} from 'sentry/utils/analytics';
- import getPlatformName from 'sentry/utils/getPlatformName';
- import withRouteAnalytics, {
- WithRouteAnalyticsProps,
- } from 'sentry/utils/routeAnalytics/withRouteAnalytics';
- import slugify from 'sentry/utils/slugify';
- import withApi from 'sentry/utils/withApi';
- import {normalizeUrl} from 'sentry/utils/withDomainRequired';
- import withOrganization from 'sentry/utils/withOrganization';
- // eslint-disable-next-line no-restricted-imports
- import withSentryRouter from 'sentry/utils/withSentryRouter';
- import withTeams from 'sentry/utils/withTeams';
- import IssueAlertOptions from 'sentry/views/projectInstall/issueAlertOptions';
- const getCategoryName = (category?: string) =>
- categoryList.find(({id}) => id === category)?.id;
- type Props = WithRouterProps &
- WithRouteAnalyticsProps & {
- api: any;
- organization: Organization;
- teams: Team[];
- };
- type PlatformName = React.ComponentProps<typeof PlatformIcon>['platform'];
- type IssueAlertFragment = Parameters<
- React.ComponentProps<typeof IssueAlertOptions>['onChange']
- >[0];
- type State = {
- dataFragment: IssueAlertFragment | undefined;
- error: boolean;
- inFlight: boolean;
- platform: PlatformName | null;
- projectName: string;
- team: string;
- };
- class CreateProject extends Component<Props, State> {
- constructor(props: Props, context) {
- super(props, context);
- const {teams, location} = props;
- const {query} = location;
- const accessTeams = teams.filter((team: Team) => team.hasAccess);
- const team = query.team || (accessTeams.length && accessTeams[0].slug);
- const platform = getPlatformName(query.platform) ? query.platform : '';
- this.state = {
- error: false,
- projectName: getPlatformName(platform) || '',
- team,
- platform,
- inFlight: false,
- dataFragment: undefined,
- };
- }
- componentDidMount() {
- this.props.setEventNames(
- 'project_creation_page.viewed',
- 'Project Create: Creation page viewed'
- );
- }
- get defaultCategory() {
- const {query} = this.props.location;
- return getCategoryName(query.category);
- }
- renderProjectForm() {
- const {organization} = this.props;
- const {projectName, platform, team} = this.state;
- const createProjectForm = (
- <CreateProjectForm onSubmit={this.createProject}>
- <div>
- <FormLabel>{t('Project name')}</FormLabel>
- <ProjectNameInputWrap>
- <StyledPlatformIcon platform={platform ?? ''} size={20} />
- <ProjectNameInput
- type="text"
- name="name"
- placeholder={t('project-name')}
- autoComplete="off"
- value={projectName}
- onChange={e => this.setState({projectName: slugify(e.target.value)})}
- />
- </ProjectNameInputWrap>
- </div>
- <div>
- <FormLabel>{t('Team')}</FormLabel>
- <TeamSelectInput>
- <TeamSelector
- name="select-team"
- menuPlacement="auto"
- clearable={false}
- value={team}
- placeholder={t('Select a Team')}
- onChange={choice => this.setState({team: choice.value})}
- teamFilter={(filterTeam: Team) => filterTeam.hasAccess}
- />
- <Button
- borderless
- data-test-id="create-team"
- icon={<IconAdd isCircled />}
- onClick={() =>
- openCreateTeamModal({
- organization,
- onClose: ({slug}) => this.setState({team: slug}),
- })
- }
- title={t('Create a team')}
- aria-label={t('Create a team')}
- />
- </TeamSelectInput>
- </div>
- <div>
- <Button
- type="submit"
- data-test-id="create-project"
- priority="primary"
- disabled={!this.canSubmitForm}
- >
- {t('Create Project')}
- </Button>
- </div>
- </CreateProjectForm>
- );
- return (
- <Fragment>
- <Layout.Title withMargins>
- {t('3. Name your project and assign it a team')}
- </Layout.Title>
- {createProjectForm}
- </Fragment>
- );
- }
- get canSubmitForm() {
- const {projectName, team, inFlight} = this.state;
- const {shouldCreateCustomRule, conditions} = this.state.dataFragment || {};
- return (
- !inFlight &&
- team &&
- projectName !== '' &&
- (!shouldCreateCustomRule || conditions?.every?.(condition => condition.value))
- );
- }
- createProject = async e => {
- e.preventDefault();
- const {organization, api} = this.props;
- const {projectName, platform, team, dataFragment} = this.state;
- const {slug} = organization;
- const {
- shouldCreateCustomRule,
- name,
- conditions,
- actions,
- actionMatch,
- frequency,
- defaultRules,
- } = dataFragment || {};
- this.setState({inFlight: true});
- if (!projectName) {
- Sentry.withScope(scope => {
- scope.setExtra('props', this.props);
- scope.setExtra('state', this.state);
- Sentry.captureMessage('No project name');
- });
- }
- try {
- const projectData = await api.requestPromise(`/teams/${slug}/${team}/projects/`, {
- method: 'POST',
- data: {
- name: projectName,
- platform,
- default_rules: defaultRules ?? true,
- },
- });
- let ruleId: string | undefined;
- if (shouldCreateCustomRule) {
- const ruleData = await api.requestPromise(
- `/projects/${organization.slug}/${projectData.slug}/rules/`,
- {
- method: 'POST',
- data: {
- name,
- conditions,
- actions,
- actionMatch,
- frequency,
- },
- }
- );
- ruleId = ruleData.id;
- }
- trackAnalytics('project_creation_page.created', {
- organization,
- issue_alert: defaultRules
- ? 'Default'
- : shouldCreateCustomRule
- ? 'Custom'
- : 'No Rule',
- project_id: projectData.id,
- rule_id: ruleId || '',
- });
- ProjectsStore.onCreateSuccess(projectData, organization.slug);
- const platformKey = platform || 'other';
- browserHistory.push(
- normalizeUrl(
- `/${organization.slug}/${projectData.slug}/getting-started/${platformKey}/`
- )
- );
- } catch (err) {
- this.setState({
- inFlight: false,
- error: err.responseJSON.detail,
- });
- // Only log this if the error is something other than:
- // * The user not having access to create a project, or,
- // * A project with that slug already exists
- if (err.status !== 403 && err.status !== 409) {
- Sentry.withScope(scope => {
- scope.setExtra('err', err);
- scope.setExtra('props', this.props);
- scope.setExtra('state', this.state);
- Sentry.captureMessage('Project creation failed');
- });
- }
- }
- };
- setPlatform = (platformKey: PlatformName | null) => {
- if (!platformKey) {
- this.setState({platform: null, projectName: ''});
- return;
- }
- this.setState(({projectName, platform}) => {
- // Avoid replacing project name when the user already modified it
- const userModifiedName = projectName && projectName !== platform;
- const newName = userModifiedName ? projectName : platformKey;
- return {
- platform: platformKey,
- projectName: slugify(newName),
- };
- });
- };
- render() {
- const {platform, error} = this.state;
- return (
- <Fragment>
- {error && <Alert type="error">{error}</Alert>}
- <div data-test-id="onboarding-info">
- <Layout.Title withMargins>{t('Create a new project in 3 steps')}</Layout.Title>
- <HelpText>
- {tct(
- 'Set up a separate project for each part of your application (for example, your API server and frontend client), to quickly pinpoint which part of your application errors are coming from. [link: Read the docs].',
- {
- link: (
- <ExternalLink href="https://docs.sentry.io/product/sentry-basics/integrate-frontend/create-new-project/" />
- ),
- }
- )}
- </HelpText>
- <Layout.Title withMargins>{t('1. Choose your platform')}</Layout.Title>
- <PlatformPicker
- platform={platform}
- defaultCategory={this.defaultCategory}
- setPlatform={selectedPlatform =>
- this.setPlatform(selectedPlatform?.id ?? null)
- }
- organization={this.props.organization}
- showOther
- />
- <IssueAlertOptions
- onChange={updatedData => {
- this.setState({dataFragment: updatedData});
- }}
- />
- {this.renderProjectForm()}
- </div>
- </Fragment>
- );
- }
- }
- // TODO(davidenwang): change to functional component and replace withTeams with useTeams
- export default withRouteAnalytics(
- withApi(withSentryRouter(withOrganization(withTeams(CreateProject))))
- );
- export {CreateProject};
- const CreateProjectForm = styled('form')`
- display: grid;
- grid-template-columns: 300px minmax(250px, max-content) max-content;
- gap: ${space(2)};
- align-items: end;
- padding: ${space(3)} 0;
- box-shadow: 0 -1px 0 rgba(0, 0, 0, 0.1);
- background: ${p => p.theme.background};
- `;
- const FormLabel = styled('div')`
- font-size: ${p => p.theme.fontSizeExtraLarge};
- margin-bottom: ${space(1)};
- `;
- const ProjectNameInputWrap = styled('div')`
- position: relative;
- `;
- const ProjectNameInput = styled(Input)`
- padding-left: calc(${p => p.theme.formPadding.md.paddingLeft}px * 1.5 + 20px);
- `;
- const StyledPlatformIcon = styled(PlatformIcon)`
- position: absolute;
- top: 50%;
- left: ${p => p.theme.formPadding.md.paddingLeft}px;
- transform: translateY(-50%);
- `;
- const TeamSelectInput = styled('div')`
- display: grid;
- gap: ${space(1)};
- grid-template-columns: 1fr min-content;
- align-items: center;
- `;
- const HelpText = styled('p')`
- color: ${p => p.theme.subText};
- max-width: 760px;
- `;
|