You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

MonorepoProjectCreate.tsx 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2024 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import { Link, Spinner } from '@sonarsource/echoes-react';
  21. import {
  22. AddNewIcon,
  23. BlueGreySeparator,
  24. ButtonPrimary,
  25. ButtonSecondary,
  26. DarkLabel,
  27. FlagMessage,
  28. InputSelect,
  29. SubTitle,
  30. Title,
  31. } from 'design-system';
  32. import React, { useEffect, useRef } from 'react';
  33. import { FormattedMessage, useIntl } from 'react-intl';
  34. import { useLocation, useRouter } from '../../../../components/hoc/withRouter';
  35. import { translate } from '../../../../helpers/l10n';
  36. import { LabelValueSelectOption } from '../../../../helpers/search';
  37. import { AlmKeys } from '../../../../types/alm-settings';
  38. import { DopSetting } from '../../../../types/dop-translation';
  39. import { ImportProjectParam } from '../CreateProjectPage';
  40. import DopSettingDropdown from '../components/DopSettingDropdown';
  41. import { ProjectData, ProjectValidationCard } from '../components/ProjectValidation';
  42. import { CreateProjectModes } from '../types';
  43. import { getSanitizedProjectKey } from '../utils';
  44. import { MonorepoProjectHeader } from './MonorepoProjectHeader';
  45. interface MonorepoProjectCreateProps {
  46. canAdmin: boolean;
  47. dopSettings: DopSetting[];
  48. error: boolean;
  49. loadingBindings: boolean;
  50. loadingOrganizations: boolean;
  51. loadingRepositories: boolean;
  52. onProjectSetupDone: (importProjects: ImportProjectParam) => void;
  53. onSearchRepositories: (query: string) => void;
  54. onSelectDopSetting: (instance: DopSetting) => void;
  55. onSelectOrganization: (organizationKey: string) => void;
  56. onSelectRepository: (repositoryIdentifier: string) => void;
  57. organizationOptions?: LabelValueSelectOption[];
  58. repositoryOptions?: LabelValueSelectOption[];
  59. repositorySearchQuery: string;
  60. selectedDopSetting?: DopSetting;
  61. selectedOrganization?: LabelValueSelectOption;
  62. selectedRepository?: LabelValueSelectOption;
  63. }
  64. type ProjectItem = Required<ProjectData<number>>;
  65. export default function MonorepoProjectCreate(props: Readonly<MonorepoProjectCreateProps>) {
  66. const {
  67. dopSettings,
  68. canAdmin,
  69. error,
  70. loadingBindings,
  71. loadingOrganizations,
  72. loadingRepositories,
  73. onProjectSetupDone,
  74. onSearchRepositories,
  75. onSelectDopSetting,
  76. onSelectOrganization,
  77. onSelectRepository,
  78. organizationOptions,
  79. repositoryOptions,
  80. repositorySearchQuery,
  81. selectedDopSetting,
  82. selectedOrganization,
  83. selectedRepository,
  84. } = props;
  85. const projectCounter = useRef(0);
  86. const [projects, setProjects] = React.useState<ProjectItem[]>([]);
  87. const location = useLocation();
  88. const { push } = useRouter();
  89. const { formatMessage } = useIntl();
  90. const projectKeys = React.useMemo(() => projects.map(({ key }) => key), [projects]);
  91. const almKey = location.query.mode as AlmKeys;
  92. const isSetupInvalid =
  93. selectedDopSetting === undefined ||
  94. selectedOrganization === undefined ||
  95. selectedRepository === undefined ||
  96. projects.length === 0 ||
  97. projects.some(({ hasError, key, name }) => hasError || key === '' || name === '');
  98. const addProject = () => {
  99. if (selectedOrganization === undefined || selectedRepository === undefined) {
  100. return;
  101. }
  102. const id = projectCounter.current;
  103. projectCounter.current += 1;
  104. const projectKeySuffix = id === 0 ? '' : `-${id}`;
  105. const projectKey = getSanitizedProjectKey(
  106. `${selectedOrganization.label}_${selectedRepository.label}_add-your-reference${projectKeySuffix}`,
  107. );
  108. const newProjects = [
  109. ...projects,
  110. {
  111. hasError: false,
  112. id,
  113. key: projectKey,
  114. name: projectKey,
  115. touched: false,
  116. },
  117. ];
  118. setProjects(newProjects);
  119. };
  120. const onProjectChange = (project: ProjectItem) => {
  121. const newProjects = projects.filter(({ id }) => id !== project.id);
  122. newProjects.push({
  123. ...project,
  124. });
  125. newProjects.sort((a, b) => a.id - b.id);
  126. setProjects(newProjects);
  127. };
  128. const onProjectRemove = (id: number) => {
  129. const newProjects = projects.filter(({ id: projectId }) => projectId !== id);
  130. setProjects(newProjects);
  131. };
  132. const cancelMonorepoSetup = () => {
  133. push({
  134. pathname: location.pathname,
  135. query: { mode: AlmKeys.GitHub },
  136. });
  137. };
  138. const submitProjects = () => {
  139. if (isSetupInvalid) {
  140. return;
  141. }
  142. const monorepoSetup: ImportProjectParam = {
  143. creationMode: almKey as unknown as CreateProjectModes,
  144. devOpsPlatformSettingId: selectedDopSetting.id,
  145. monorepo: true,
  146. projects: projects.map(({ key: projectKey, name: projectName }) => ({
  147. projectKey,
  148. projectName,
  149. })),
  150. repositoryIdentifier: selectedRepository.value,
  151. };
  152. onProjectSetupDone(monorepoSetup);
  153. };
  154. useEffect(() => {
  155. if (selectedRepository !== undefined && projects.length === 0) {
  156. addProject();
  157. }
  158. // eslint-disable-next-line react-hooks/exhaustive-deps
  159. }, [selectedRepository]);
  160. if (loadingBindings) {
  161. return <Spinner />;
  162. }
  163. return (
  164. <div>
  165. <MonorepoProjectHeader />
  166. <BlueGreySeparator className="sw-my-5" />
  167. <div className="sw-flex sw-flex-col sw-gap-6">
  168. <Title>
  169. <FormattedMessage
  170. id={`onboarding.create_project.monorepo.choose_organization_and_repository.${almKey}`}
  171. />
  172. </Title>
  173. <DopSettingDropdown
  174. almKey={almKey}
  175. dopSettings={dopSettings}
  176. selectedDopSetting={selectedDopSetting}
  177. onChangeSetting={onSelectDopSetting}
  178. />
  179. {error && selectedDopSetting && !loadingOrganizations && (
  180. <FlagMessage variant="warning">
  181. <span>
  182. {canAdmin ? (
  183. <FormattedMessage
  184. id="onboarding.create_project.github.warning.message_admin"
  185. defaultMessage={translate(
  186. 'onboarding.create_project.github.warning.message_admin',
  187. )}
  188. values={{
  189. link: (
  190. <Link to="/admin/settings?category=almintegration">
  191. {translate('onboarding.create_project.github.warning.message_admin.link')}
  192. </Link>
  193. ),
  194. }}
  195. />
  196. ) : (
  197. translate('onboarding.create_project.github.warning.message')
  198. )}
  199. </span>
  200. </FlagMessage>
  201. )}
  202. <div className="sw-flex sw-flex-col">
  203. <Spinner isLoading={loadingOrganizations && !error}>
  204. {!error && (
  205. <>
  206. <DarkLabel htmlFor="monorepo-choose-organization" className="sw-mb-2">
  207. <FormattedMessage
  208. id={`onboarding.create_project.monorepo.choose_organization.${almKey}`}
  209. />
  210. </DarkLabel>
  211. {(organizationOptions?.length ?? 0) > 0 ? (
  212. <InputSelect
  213. size="full"
  214. isSearchable
  215. inputId="monorepo-choose-organization"
  216. options={organizationOptions}
  217. onChange={({ value }: LabelValueSelectOption) => {
  218. onSelectOrganization(value);
  219. }}
  220. placeholder={formatMessage({
  221. id: `onboarding.create_project.monorepo.choose_organization.${almKey}.placeholder`,
  222. })}
  223. value={selectedOrganization}
  224. />
  225. ) : (
  226. !loadingOrganizations && (
  227. <FlagMessage variant="error" className="sw-mb-2">
  228. <span>
  229. {canAdmin ? (
  230. <FormattedMessage
  231. id="onboarding.create_project.github.no_orgs_admin"
  232. defaultMessage={translate(
  233. 'onboarding.create_project.github.no_orgs_admin',
  234. )}
  235. values={{
  236. link: (
  237. <Link to="/admin/settings?category=almintegration">
  238. {translate(
  239. 'onboarding.create_project.github.warning.message_admin.link',
  240. )}
  241. </Link>
  242. ),
  243. }}
  244. />
  245. ) : (
  246. translate('onboarding.create_project.github.no_orgs')
  247. )}
  248. </span>
  249. </FlagMessage>
  250. )
  251. )}
  252. </>
  253. )}
  254. </Spinner>
  255. </div>
  256. <div className="sw-flex sw-flex-col">
  257. {selectedOrganization && (
  258. <DarkLabel className="sw-mb-2" htmlFor="monorepo-choose-repository">
  259. <FormattedMessage
  260. id={`onboarding.create_project.monorepo.choose_repository.${almKey}`}
  261. />
  262. </DarkLabel>
  263. )}
  264. {selectedOrganization && (
  265. <InputSelect
  266. inputId="monorepo-choose-repository"
  267. inputValue={repositorySearchQuery}
  268. isLoading={loadingRepositories}
  269. isSearchable
  270. noOptionsMessage={() => formatMessage({ id: 'no_results' })}
  271. onChange={({ value }: LabelValueSelectOption) => {
  272. onSelectRepository(value);
  273. }}
  274. onInputChange={onSearchRepositories}
  275. options={repositoryOptions}
  276. placeholder={formatMessage({
  277. id: `onboarding.create_project.monorepo.choose_repository.${almKey}.placeholder`,
  278. })}
  279. size="full"
  280. value={selectedRepository}
  281. />
  282. )}
  283. </div>
  284. </div>
  285. {selectedRepository !== undefined && (
  286. <>
  287. <BlueGreySeparator className="sw-my-5" />
  288. <div>
  289. <SubTitle>
  290. <FormattedMessage id="onboarding.create_project.monorepo.project_title" />
  291. </SubTitle>
  292. <div>
  293. {projects.map(({ id, key, name }) => (
  294. <ProjectValidationCard
  295. className="sw-mt-4"
  296. initialKey={key}
  297. initialName={name}
  298. key={id}
  299. monorepoSetupProjectKeys={projectKeys}
  300. onChange={onProjectChange}
  301. onRemove={() => {
  302. onProjectRemove(id);
  303. }}
  304. projectId={id}
  305. />
  306. ))}
  307. </div>
  308. <div className="sw-flex sw-justify-end sw-mt-4">
  309. <ButtonSecondary onClick={addProject}>
  310. <AddNewIcon className="sw-mr-2" />
  311. <FormattedMessage id="onboarding.create_project.monorepo.add_project" />
  312. </ButtonSecondary>
  313. </div>
  314. </div>
  315. </>
  316. )}
  317. <div className="sw-my-5">
  318. <ButtonSecondary onClick={cancelMonorepoSetup}>
  319. <FormattedMessage id="cancel" />
  320. </ButtonSecondary>
  321. <ButtonPrimary className="sw-ml-3" disabled={isSetupInvalid} onClick={submitProjects}>
  322. <FormattedMessage id="next" />
  323. </ButtonPrimary>
  324. </div>
  325. </div>
  326. );
  327. }