]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20708 Please don't leave the page + progress indicator
authorViktor Vorona <viktor.vorona@sonarsource.com>
Mon, 16 Oct 2023 14:38:02 +0000 (16:38 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 18 Oct 2023 20:03:05 +0000 (20:03 +0000)
server/sonar-web/src/main/js/app/utils/startReactApp.tsx
server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreate.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/GitHub-it.tsx
server/sonar-web/src/main/js/apps/create/project/components/NewCodeDefinitionSelection.tsx
server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 1b800f04a37d1a2f2dc7dfa2ec0c1bdaa9f5cc5b..985c80ab56b4adc1d8a07bdc303d0e9f4b3a8075 100644 (file)
@@ -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>
index 6572243a44a6f263d70cd02328235c304681dde8..0fec8dd39a0ddbae0c45638f67344155a8c2ff91 100644 (file)
@@ -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;
index c80ae871168554ebb70642782f1f640e39bc675f..5d74c34d0a29830c5fdeaea277b9475e8c1088e3 100644 (file)
@@ -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();
index e49ae22d11874fde40945c12efcf55654042400d..521fb01226c38ab2f64241844073ebf910a7cd6a 100644 (file)
@@ -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>
   );
index 4b21b5df3fefbd0b7c4fca5f951a8f70ca1d41d8..efea23691896cb0a81a1b6c81dccf3d03f9567f5 100644 (file)
@@ -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>
index 463c1e7b682337f3a628f00ef3b6be03f52db8fe..6d2d013165ab81ea2dcbab1b027aa0e582611f79 100644 (file)
@@ -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