您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

NewCodeDefinitionSelection.tsx 8.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  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 {
  21. ButtonPrimary,
  22. ButtonSecondary,
  23. CloseIcon,
  24. FlagMessage,
  25. InteractiveIcon,
  26. Link,
  27. Spinner,
  28. Title,
  29. addGlobalErrorMessage,
  30. addGlobalSuccessMessage,
  31. } from 'design-system';
  32. import { omit } from 'lodash';
  33. import * as React from 'react';
  34. import { useEffect } from 'react';
  35. import { FormattedMessage, useIntl } from 'react-intl';
  36. import { useNavigate, unstable_usePrompt as usePrompt } from 'react-router-dom';
  37. import { useLocation } from '~sonar-aligned/components/hoc/withRouter';
  38. import { queryToSearchString } from '~sonar-aligned/helpers/urls';
  39. import NewCodeDefinitionSelector from '../../../../components/new-code-definition/NewCodeDefinitionSelector';
  40. import { useDocUrl } from '../../../../helpers/docs';
  41. import { translate } from '../../../../helpers/l10n';
  42. import { getProjectUrl } from '../../../../helpers/urls';
  43. import {
  44. MutationArg,
  45. useImportProjectMutation,
  46. useImportProjectProgress,
  47. } from '../../../../queries/import-projects';
  48. import { NewCodeDefinitiondWithCompliance } from '../../../../types/new-code-definition';
  49. import { ImportProjectParam } from '../CreateProjectPage';
  50. const listener = (event: BeforeUnloadEvent) => {
  51. event.returnValue = true;
  52. };
  53. interface Props {
  54. importProjects: ImportProjectParam;
  55. onClose: () => void;
  56. redirectTo: string;
  57. }
  58. export default function NewCodeDefinitionSelection(props: Props) {
  59. const { importProjects, redirectTo, onClose } = props;
  60. const [selectedDefinition, selectDefinition] = React.useState<NewCodeDefinitiondWithCompliance>();
  61. const [failedImports, setFailedImports] = React.useState<number>(0);
  62. const { mutateAsync, data, reset, isIdle } = useImportProjectMutation();
  63. const mutateCount = useImportProjectProgress();
  64. const isImporting = mutateCount > 0;
  65. const intl = useIntl();
  66. const location = useLocation();
  67. const navigate = useNavigate();
  68. const getDocUrl = useDocUrl();
  69. usePrompt({
  70. when: isImporting,
  71. message: translate('onboarding.create_project.please_dont_leave'),
  72. });
  73. const projectCount = importProjects.projects.length;
  74. const isMultipleProjects = projectCount > 1;
  75. const isMonorepo = location.query?.mono === 'true';
  76. useEffect(() => {
  77. const redirect = (projectCount: number) => {
  78. if (!isMonorepo && projectCount === 1 && data) {
  79. if (redirectTo === '/projects') {
  80. navigate(getProjectUrl(data.project.key));
  81. } else {
  82. onClose();
  83. }
  84. } else {
  85. navigate({
  86. pathname: '/projects',
  87. search: queryToSearchString({ sort: '-creation_date' }),
  88. });
  89. }
  90. };
  91. if (mutateCount > 0 || isIdle) {
  92. return;
  93. }
  94. if (failedImports > 0) {
  95. addGlobalErrorMessage(
  96. intl.formatMessage(
  97. { id: 'onboarding.create_project.failure' },
  98. {
  99. count: failedImports,
  100. },
  101. ),
  102. );
  103. }
  104. if (projectCount > failedImports) {
  105. if (redirectTo === '/projects') {
  106. addGlobalSuccessMessage(
  107. intl.formatMessage(
  108. {
  109. id: isMonorepo
  110. ? 'onboarding.create_project.monorepo.success'
  111. : 'onboarding.create_project.success',
  112. },
  113. {
  114. count: projectCount - failedImports,
  115. },
  116. ),
  117. );
  118. } else if (data) {
  119. addGlobalSuccessMessage(
  120. <FormattedMessage
  121. defaultMessage={translate('onboarding.create_project.success.admin')}
  122. id="onboarding.create_project.success.admin"
  123. values={{
  124. project_link: <Link to={getProjectUrl(data.project.key)}>{data.project.name}</Link>,
  125. }}
  126. />,
  127. );
  128. }
  129. redirect(projectCount);
  130. }
  131. reset();
  132. setFailedImports(0);
  133. }, [
  134. data,
  135. projectCount,
  136. failedImports,
  137. mutateCount,
  138. reset,
  139. intl,
  140. navigate,
  141. isIdle,
  142. redirectTo,
  143. onClose,
  144. ]);
  145. React.useEffect(() => {
  146. if (isImporting) {
  147. window.addEventListener('beforeunload', listener);
  148. }
  149. return () => window.removeEventListener('beforeunload', listener);
  150. }, [isImporting]);
  151. const handleProjectCreation = () => {
  152. if (selectedDefinition) {
  153. importProjects.projects.forEach((p) => {
  154. const arg = {
  155. // eslint-disable-next-line local-rules/use-metrickey-enum
  156. ...omit(importProjects, 'projects'),
  157. ...p,
  158. } as MutationArg;
  159. mutateAsync({
  160. newCodeDefinitionType: selectedDefinition.type,
  161. newCodeDefinitionValue: selectedDefinition.value,
  162. ...arg,
  163. }).catch(() => {
  164. setFailedImports((prev) => prev + 1);
  165. });
  166. });
  167. }
  168. };
  169. return (
  170. <section
  171. aria-label={translate('onboarding.create_project.new_code_definition.title')}
  172. id="project-ncd-selection"
  173. className="sw-body-sm"
  174. >
  175. <div className="sw-flex sw-justify-between">
  176. <FormattedMessage
  177. id="onboarding.create_project.manual.step2"
  178. defaultMessage={translate('onboarding.create_project.manual.step2')}
  179. />
  180. <InteractiveIcon
  181. Icon={CloseIcon}
  182. aria-label={intl.formatMessage({ id: 'clear' })}
  183. currentColor
  184. onClick={onClose}
  185. size="small"
  186. />
  187. </div>
  188. <Title>
  189. <FormattedMessage
  190. defaultMessage={translate('onboarding.create_x_project.new_code_definition.title')}
  191. id="onboarding.create_x_project.new_code_definition.title"
  192. values={{
  193. count: projectCount,
  194. }}
  195. />
  196. </Title>
  197. <p className="sw-mb-2">
  198. <FormattedMessage
  199. defaultMessage={translate('onboarding.create_project.new_code_definition.description')}
  200. id="onboarding.create_project.new_code_definition.description"
  201. values={{
  202. link: (
  203. <Link to={getDocUrl('/project-administration/defining-new-code/')}>
  204. {translate('onboarding.create_project.new_code_definition.description.link')}
  205. </Link>
  206. ),
  207. }}
  208. />
  209. </p>
  210. <NewCodeDefinitionSelector
  211. onNcdChanged={selectDefinition}
  212. isMultipleProjects={isMultipleProjects}
  213. />
  214. {isMultipleProjects && (
  215. <FlagMessage variant="info">
  216. {translate('onboarding.create_projects.new_code_definition.change_info')}
  217. </FlagMessage>
  218. )}
  219. <div className="sw-mt-10 sw-mb-8 sw-flex sw-gap-2 sw-items-center">
  220. <ButtonSecondary onClick={() => navigate(-1)}>{translate('back')}</ButtonSecondary>
  221. <ButtonPrimary
  222. onClick={handleProjectCreation}
  223. disabled={!selectedDefinition?.isCompliant || isImporting}
  224. type="submit"
  225. >
  226. <FormattedMessage
  227. defaultMessage={translate(
  228. 'onboarding.create_project.new_code_definition.create_x_projects',
  229. )}
  230. id="onboarding.create_project.new_code_definition.create_x_projects"
  231. values={{
  232. count: projectCount,
  233. }}
  234. />
  235. <Spinner className="sw-ml-2" loading={isImporting} />
  236. </ButtonPrimary>
  237. {isImporting && projectCount > 1 && (
  238. <FlagMessage variant="warning">
  239. <FormattedMessage
  240. id="onboarding.create_project.import_in_progress"
  241. values={{
  242. count: projectCount - mutateCount,
  243. total: projectCount,
  244. }}
  245. />
  246. </FlagMessage>
  247. )}
  248. </div>
  249. </section>
  250. );
  251. }