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 trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent'; 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['platform']; type IssueAlertFragment = Parameters< React.ComponentProps['onChange'] >[0]; type State = { dataFragment: IssueAlertFragment | undefined; error: boolean; inFlight: boolean; platform: PlatformName | null; projectName: string; team: string; }; class CreateProject extends Component { 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 = (
{t('Project name')} this.setState({projectName: slugify(e.target.value)})} />
{t('Team')} this.setState({team: choice.value})} teamFilter={(filterTeam: Team) => filterTeam.hasAccess} />
); return ( {t('3. Name your project and assign it a team')} {createProjectForm} ); } 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; } trackAdvancedAnalyticsEvent('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'; const nextUrl = `/${organization.slug}/${projectData.slug}/getting-started/${platformKey}/`; browserHistory.push(normalizeUrl(nextUrl)); } 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 ( {error && {error}}
{t('Create a new project in 3 steps')} {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: ( ), } )} {t('1. Choose your platform')} { this.setState({dataFragment: updatedData}); }} /> {this.renderProjectForm()}
); } } // 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; `;