projectCreationModal.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import {Fragment, useCallback, useState} from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import omit from 'lodash/omit';
  5. import {PlatformIcon} from 'platformicons';
  6. import {
  7. addErrorMessage,
  8. addLoadingMessage,
  9. addSuccessMessage,
  10. clearIndicators,
  11. } from 'sentry/actionCreators/indicator';
  12. import type {ModalRenderProps} from 'sentry/actionCreators/modal';
  13. import {Button} from 'sentry/components/button';
  14. import Input from 'sentry/components/input';
  15. import type {SupportedLanguages} from 'sentry/components/onboarding/frameworkSuggestionModal';
  16. import PlatformPicker, {
  17. type Category,
  18. type Platform,
  19. } from 'sentry/components/platformPicker';
  20. import TeamSelector from 'sentry/components/teamSelector';
  21. import {t} from 'sentry/locale';
  22. import ProjectsStore from 'sentry/stores/projectsStore';
  23. import {space} from 'sentry/styles/space';
  24. import type {OnboardingSelectedSDK, Team} from 'sentry/types';
  25. import {trackAnalytics} from 'sentry/utils/analytics';
  26. import slugify from 'sentry/utils/slugify';
  27. import useApi from 'sentry/utils/useApi';
  28. import useOrganization from 'sentry/utils/useOrganization';
  29. import type {IssueAlertFragment} from 'sentry/views/projectInstall/createProject';
  30. import IssueAlertOptions from 'sentry/views/projectInstall/issueAlertOptions';
  31. type Props = ModalRenderProps & {
  32. defaultCategory?: Category;
  33. };
  34. export default function ProjectCreationModal({
  35. Header,
  36. closeModal,
  37. defaultCategory,
  38. }: Props) {
  39. const [platform, setPlatform] = useState<OnboardingSelectedSDK | undefined>(undefined);
  40. const [step, setStep] = useState(0);
  41. const [alertRuleConfig, setAlertRuleConfig] = useState<IssueAlertFragment | undefined>(
  42. undefined
  43. );
  44. const [projectName, setProjectName] = useState('');
  45. const [team, setTeam] = useState<Team | undefined>(undefined);
  46. const [creating, setCreating] = useState(false);
  47. const api = useApi();
  48. const organization = useOrganization();
  49. function handlePlatformChange(selectedPlatform: Platform | null) {
  50. if (selectedPlatform) {
  51. setPlatform({
  52. ...omit(selectedPlatform, 'id'),
  53. key: selectedPlatform.id,
  54. });
  55. }
  56. }
  57. const createProject = useCallback(async () => {
  58. const {slug} = organization;
  59. const {
  60. shouldCreateCustomRule,
  61. name,
  62. conditions,
  63. actions,
  64. actionMatch,
  65. frequency,
  66. defaultRules,
  67. } = alertRuleConfig || {};
  68. if (platform === undefined) {
  69. return;
  70. }
  71. addLoadingMessage(t('Creating project...'), {
  72. duration: 15000,
  73. });
  74. try {
  75. const url = `/teams/${slug}/${team}/projects/`;
  76. const projectData = await api.requestPromise(url, {
  77. method: 'POST',
  78. data: {
  79. name: projectName,
  80. platform: platform.key,
  81. default_rules: defaultRules ?? true,
  82. },
  83. });
  84. let ruleId: string | undefined;
  85. if (shouldCreateCustomRule) {
  86. const ruleData = await api.requestPromise(
  87. `/projects/${organization.slug}/${projectData.slug}/rules/`,
  88. {
  89. method: 'POST',
  90. data: {
  91. name,
  92. conditions,
  93. actions,
  94. actionMatch,
  95. frequency,
  96. },
  97. }
  98. );
  99. ruleId = ruleData.id;
  100. }
  101. ProjectsStore.onCreateSuccess(projectData, organization.slug);
  102. clearIndicators();
  103. trackAnalytics('project_modal.created', {
  104. organization,
  105. issue_alert: defaultRules
  106. ? 'Default'
  107. : shouldCreateCustomRule
  108. ? 'Custom'
  109. : 'No Rule',
  110. project_id: projectData.id,
  111. rule_id: ruleId || '',
  112. });
  113. addSuccessMessage(`Created project ${projectData.slug}`);
  114. closeModal();
  115. } catch (err) {
  116. setCreating(false);
  117. addErrorMessage(`Failed to create project ${projectName}`);
  118. }
  119. }, [api, alertRuleConfig, organization, platform, projectName, team, closeModal]);
  120. return (
  121. <Fragment>
  122. <Header closeButton>
  123. <h4>{t('Create a Project')}</h4>
  124. </Header>
  125. {step === 0 && (
  126. <Fragment>
  127. <Subtitle>Choose a Platform</Subtitle>
  128. <PlatformPicker
  129. defaultCategory={defaultCategory}
  130. setPlatform={handlePlatformChange}
  131. organization={organization}
  132. platform={platform?.key}
  133. showFilterBar={false}
  134. navClassName="centered"
  135. listClassName="centered"
  136. />
  137. </Fragment>
  138. )}
  139. {step === 1 && (
  140. <Fragment>
  141. <Subtitle>{t('Set your alert frequency')}</Subtitle>
  142. <IssueAlertOptions
  143. platformLanguage={platform?.language as SupportedLanguages}
  144. onChange={updatedData => setAlertRuleConfig(updatedData)}
  145. />
  146. <Subtitle>{t('Name your project and assign it a team')}</Subtitle>
  147. <ProjectNameTeamSection>
  148. <div>
  149. <Label>{t('Project name')}</Label>
  150. <ProjectNameInputWrap>
  151. <StyledPlatformIcon platform={platform?.key ?? 'other'} size={20} />
  152. <ProjectNameInput
  153. type="text"
  154. name="project-name"
  155. placeholder={t('project-name')}
  156. autoComplete="off"
  157. value={projectName}
  158. onChange={e => setProjectName(slugify(e.target.value))}
  159. />
  160. </ProjectNameInputWrap>
  161. </div>
  162. <div>
  163. <Label>{t('Team')}</Label>
  164. <TeamInput
  165. allowCreate
  166. name="select-team"
  167. aria-label={t('Select a Team')}
  168. menuPlacement="auto"
  169. clearable={false}
  170. value={team}
  171. placeholder={t('Select a Team')}
  172. onChange={choice => setTeam(choice.value)}
  173. teamFilter={(tm: Team) => tm.access.includes('team:admin')}
  174. />
  175. </div>
  176. </ProjectNameTeamSection>
  177. </Fragment>
  178. )}
  179. <Footer>
  180. {step === 1 && <Button onClick={() => setStep(step - 1)}>{t('Back')}</Button>}
  181. {step === 0 && (
  182. <Button
  183. priority="primary"
  184. disabled={!platform}
  185. onClick={() => setStep(step + 1)}
  186. >
  187. {t('Next Step')}
  188. </Button>
  189. )}
  190. {step === 1 && (
  191. <Button
  192. priority="primary"
  193. onClick={() => {
  194. setCreating(true);
  195. createProject();
  196. }}
  197. disabled={!projectName || !team || !alertRuleConfig || !platform || creating}
  198. >
  199. {t('Create Project')}
  200. </Button>
  201. )}
  202. </Footer>
  203. </Fragment>
  204. );
  205. }
  206. const Footer = styled('div')`
  207. display: flex;
  208. flex-direction: row;
  209. justify-content: right;
  210. gap: ${space(1)};
  211. margin-top: ${space(2)};
  212. `;
  213. const StyledPlatformIcon = styled(PlatformIcon)`
  214. position: absolute;
  215. top: 50%;
  216. left: ${p => p.theme.formPadding.md.paddingLeft}px;
  217. transform: translateY(-50%);
  218. `;
  219. const ProjectNameInputWrap = styled('div')`
  220. position: relative;
  221. `;
  222. const ProjectNameInput = styled(Input)`
  223. padding-left: calc(${p => p.theme.formPadding.md.paddingLeft}px * 1.5 + 20px);
  224. `;
  225. export const modalCss = css`
  226. width: 100%;
  227. max-width: 1000px;
  228. `;
  229. const ProjectNameTeamSection = styled('div')`
  230. display: flex;
  231. flex-direction: row;
  232. gap: ${space(1)};
  233. `;
  234. const Label = styled('div')`
  235. font-size: ${p => p.theme.fontSizeExtraLarge};
  236. margin-bottom: ${space(1)};
  237. `;
  238. const TeamInput = styled(TeamSelector)`
  239. min-width: 250px;
  240. `;
  241. const Subtitle = styled('p')`
  242. margin: ${space(2)} 0 ${space(1)} 0;
  243. font-size: ${p => p.theme.fontSizeExtraLarge};
  244. font-weight: ${p => p.theme.fontWeightBold};
  245. `;