diff options
6 files changed, 136 insertions, 86 deletions
diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx index 1b800f04a37..985c80ab56b 100644 --- a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx +++ b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx @@ -24,7 +24,12 @@ import * as React from 'react'; import { render } from 'react-dom'; import { Helmet, HelmetProvider } from 'react-helmet-async'; import { IntlShape, RawIntlProvider } from 'react-intl'; -import { BrowserRouter, Route, Routes } from 'react-router-dom'; +import { + Route, + RouterProvider, + createBrowserRouter, + createRoutesFromElements, +} from 'react-router-dom'; import accountRoutes from '../../apps/account/routes'; import auditLogsRoutes from '../../apps/audit-logs/routes'; import backgroundTasksRoutes from '../../apps/background-tasks/routes'; @@ -174,6 +179,75 @@ function renderRedirects() { ); } +const router = createBrowserRouter( + createRoutesFromElements( + <> + {renderRedirects()} + + <Route path="formatting/help" element={<FormattingHelp />} /> + + <Route element={<SimpleContainer />}>{maintenanceRoutes()}</Route> + + <Route element={<MigrationContainer />}> + {sessionsRoutes()} + + <Route path="/" element={<App />}> + <Route index element={<Landing />} /> + + <Route element={<GlobalContainer />}> + {accountRoutes()} + + {codingRulesRoutes()} + + <Route path="extension/:pluginKey/:extensionKey" element={<GlobalPageExtension />} /> + + {globalIssuesRoutes()} + + {projectsRoutes()} + + {qualityGatesRoutes()} + {qualityProfilesRoutes()} + + <Route path="portfolios" element={<PortfoliosPage />} /> + + <Route path="sonarlint/auth" element={<SonarLintConnection />} /> + + {webAPIRoutes()} + {webAPIRoutesV2()} + + {renderComponentRoutes()} + + {renderAdminRoutes()} + </Route> + <Route + // We don't want this route to have any menu. + // That is why we can not have it under the accountRoutes + path="account/reset_password" + element={<ResetPassword />} + /> + + <Route + // We don't want this route to have any menu. This is why we define it here + // rather than under the admin routes. + path="admin/change_admin_password" + element={<ChangeAdminPasswordApp />} + /> + + <Route + // We don't want this route to have any menu. This is why we define it here + // rather than under the admin routes. + path="admin/plugin_risk_consent" + element={<PluginRiskConsent />} + /> + <Route path="not_found" element={<NotFound />} /> + <Route path="*" element={<NotFound />} /> + </Route> + </Route> + </>, + ), + { basename: getBaseUrl() }, +); + const queryClient = new QueryClient(); export default function startReactApp( @@ -195,75 +269,8 @@ export default function startReactApp( <ThemeProvider theme={lightTheme}> <QueryClientProvider client={queryClient}> <GlobalMessagesContainer /> - <BrowserRouter basename={getBaseUrl()}> - <Helmet titleTemplate={translate('page_title.template.default')} /> - <Routes> - {renderRedirects()} - - <Route path="formatting/help" element={<FormattingHelp />} /> - - <Route element={<SimpleContainer />}>{maintenanceRoutes()}</Route> - - <Route element={<MigrationContainer />}> - {sessionsRoutes()} - - <Route path="/" element={<App />}> - <Route index element={<Landing />} /> - - <Route element={<GlobalContainer />}> - {accountRoutes()} - - {codingRulesRoutes()} - - <Route - path="extension/:pluginKey/:extensionKey" - element={<GlobalPageExtension />} - /> - - {globalIssuesRoutes()} - - {projectsRoutes()} - - {qualityGatesRoutes()} - {qualityProfilesRoutes()} - - <Route path="portfolios" element={<PortfoliosPage />} /> - - <Route path="sonarlint/auth" element={<SonarLintConnection />} /> - - {webAPIRoutes()} - {webAPIRoutesV2()} - - {renderComponentRoutes()} - - {renderAdminRoutes()} - </Route> - <Route - // We don't want this route to have any menu. - // That is why we can not have it under the accountRoutes - path="account/reset_password" - element={<ResetPassword />} - /> - - <Route - // We don't want this route to have any menu. This is why we define it here - // rather than under the admin routes. - path="admin/change_admin_password" - element={<ChangeAdminPasswordApp />} - /> - - <Route - // We don't want this route to have any menu. This is why we define it here - // rather than under the admin routes. - path="admin/plugin_risk_consent" - element={<PluginRiskConsent />} - /> - <Route path="not_found" element={<NotFound />} /> - <Route path="*" element={<NotFound />} /> - </Route> - </Route> - </Routes> - </BrowserRouter> + <Helmet titleTemplate={translate('page_title.template.default')} /> + <RouterProvider router={router} /> </QueryClientProvider> </ThemeProvider> </RawIntlProvider> diff --git a/server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreate.tsx index 6572243a44a..0fec8dd39a0 100644 --- a/server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreate.tsx @@ -55,7 +55,7 @@ interface State { selectedAlmInstance?: AlmSettingsInstance; } -const REPOSITORY_PAGE_SIZE = 20; +const REPOSITORY_PAGE_SIZE = 50; export default class GitHubProjectCreate extends React.Component<Props, State> { mounted = false; diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitHub-it.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitHub-it.tsx index c80ae871168..5d74c34d0a2 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitHub-it.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitHub-it.tsx @@ -235,7 +235,7 @@ it('should show search filter when the authentication is successful', async () = almSetting: 'conf-github-2', organization: 'org-1', page: 1, - pageSize: 20, + pageSize: 50, query: 'search', }); }); @@ -263,7 +263,7 @@ it('should have load more', async () => { almSetting: 'conf-github-2', organization: 'org-1', page: 2, - pageSize: 20, + pageSize: 50, query: '', }); expect(loadMore).not.toBeInTheDocument(); diff --git a/server/sonar-web/src/main/js/apps/create/project/components/NewCodeDefinitionSelection.tsx b/server/sonar-web/src/main/js/apps/create/project/components/NewCodeDefinitionSelection.tsx index e49ae22d118..521fb01226c 100644 --- a/server/sonar-web/src/main/js/apps/create/project/components/NewCodeDefinitionSelection.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/components/NewCodeDefinitionSelection.tsx @@ -22,7 +22,7 @@ import { omit } from 'lodash'; import * as React from 'react'; import { useEffect } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, unstable_usePrompt as usePrompt } from 'react-router-dom'; import NewCodeDefinitionSelector from '../../../../components/new-code-definition/NewCodeDefinitionSelector'; import { useDocUrl } from '../../../../helpers/docs'; import { addGlobalSuccessMessage } from '../../../../helpers/globalMessages'; @@ -36,6 +36,10 @@ import { import { NewCodeDefinitiondWithCompliance } from '../../../../types/new-code-definition'; import { ImportProjectParam } from '../CreateProjectPage'; +const listener = (event: BeforeUnloadEvent) => { + event.returnValue = true; +}; + interface Props { importProjects: ImportProjectParam; } @@ -49,6 +53,10 @@ export default function NewCodeDefinitionSelection(props: Props) { const intl = useIntl(); const navigate = useNavigate(); const getDocUrl = useDocUrl(); + usePrompt({ + when: isLoading, + message: translate('onboarding.create_project.please_dont_leave'), + }); const projectCount = importProjects.projects.length; const isMultipleProjects = projectCount > 1; @@ -77,6 +85,14 @@ export default function NewCodeDefinitionSelection(props: Props) { } }, [data, projectCount, mutateCount, reset, intl, navigate]); + React.useEffect(() => { + if (isLoading) { + window.addEventListener('beforeunload', listener); + } + + return () => window.removeEventListener('beforeunload', listener); + }, [isLoading]); + const handleProjectCreation = () => { if (selectedDefinition) { importProjects.projects.forEach((p) => { @@ -131,10 +147,8 @@ export default function NewCodeDefinitionSelection(props: Props) { </FlagMessage> )} - <div className="sw-mt-10 sw-mb-8"> - <ButtonSecondary className="sw-mr-2" onClick={() => navigate(-1)}> - {translate('back')} - </ButtonSecondary> + <div className="sw-mt-10 sw-mb-8 sw-flex sw-gap-2 sw-items-center"> + <ButtonSecondary onClick={() => navigate(-1)}>{translate('back')}</ButtonSecondary> <ButtonPrimary onClick={handleProjectCreation} disabled={!selectedDefinition?.isCompliant || isLoading} @@ -151,6 +165,17 @@ export default function NewCodeDefinitionSelection(props: Props) { /> <Spinner className="sw-ml-2" loading={isLoading} /> </ButtonPrimary> + {isLoading && ( + <FlagMessage variant="warning"> + <FormattedMessage + id="onboarding.create_project.import_in_progress" + values={{ + count: projectCount - mutateCount, + total: projectCount, + }} + /> + </FlagMessage> + )} </div> </div> ); diff --git a/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx b/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx index 4b21b5df3fe..efea2369189 100644 --- a/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx +++ b/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx @@ -24,7 +24,16 @@ import { omit } from 'lodash'; import * as React from 'react'; import { HelmetProvider } from 'react-helmet-async'; import { IntlProvider, ReactIntlErrorCode } from 'react-intl'; -import { MemoryRouter, Outlet, Route, Routes, parsePath } from 'react-router-dom'; +import { + MemoryRouter, + Outlet, + Route, + RouterProvider, + Routes, + createMemoryRouter, + createRoutesFromElements, + parsePath, +} from 'react-router-dom'; import AdminContext from '../app/components/AdminContext'; import GlobalMessagesContainer from '../app/components/GlobalMessagesContainer'; import AppStateContextProvider from '../app/components/app-state/AppStateContextProvider'; @@ -189,9 +198,21 @@ function renderRoutedApp( }: RenderContext = {}, ): RenderResult { const path = parsePath(navigateTo); - path.pathname = `/${path.pathname}`; + if (!path.pathname?.startsWith('/')) { + path.pathname = `/${path.pathname}`; + } const queryClient = new QueryClient(); + const router = createMemoryRouter( + createRoutesFromElements( + <> + {children} + <Route path="*" element={<CatchAll />} /> + </>, + ), + { initialEntries: [path] }, + ); + return render( <HelmetProvider context={{}}> <IntlWrapper> @@ -203,12 +224,7 @@ function renderRoutedApp( <IndexationContextProvider> <QueryClientProvider client={queryClient}> <GlobalMessagesContainer /> - <MemoryRouter initialEntries={[path]}> - <Routes> - {children} - <Route path="*" element={<CatchAll />} /> - </Routes> - </MemoryRouter> + <RouterProvider router={router} /> </QueryClientProvider> </IndexationContextProvider> </AppStateContextProvider> diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 463c1e7b682..6d2d013165a 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -4193,6 +4193,8 @@ onboarding.create_project.bitbucket.title=Bitbucket Server project onboarding onboarding.create_project.bitbucket.subtitle=Import projects from one of your Bitbucket server workspaces onboarding.create_project.x_repositories_selected={count} {count, plural, one {repository} other {repositories}} selected onboarding.create_project.x_repository_created={count} {count, plural, one {repository} other {repositories}} will be created as a project on SonarQube +onboarding.create_project.please_dont_leave=If you leave the page the import could fail. Are you sure you want to leave? +onboarding.create_project.import_in_progress={count} of {total} projects is imported. Please do not close this page until the import is done. onboarding.create_project.new_code_definition.title=Set up project for Clean as You Code onboarding.create_x_project.new_code_definition.title=Set up {count, plural, one {project} other {# projects}} for Clean as You Code |