]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19789 Display message when permission sync is pending on onboard project page
authorMathieu Suen <mathieu.suen@sonarsource.com>
Tue, 8 Aug 2023 14:09:33 +0000 (16:09 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 9 Aug 2023 20:03:37 +0000 (20:03 +0000)
server/sonar-web/design-system/src/components/index.ts
server/sonar-web/src/main/js/api/ce.ts
server/sonar-web/src/main/js/api/mocks/ComputeEngineServiceMock.ts
server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
server/sonar-web/src/main/js/apps/overview/components/App.tsx
server/sonar-web/src/main/js/apps/overview/components/EmptyOverview.tsx
server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx
server/sonar-web/src/main/js/queries/component.ts [new file with mode: 0644]
server/sonar-web/src/main/js/types/tasks.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 5536c8be6cd42ac4426638f2b40647ec723e880d..484ae86999d4edd02421e92193f86c5d5b447ac8 100644 (file)
@@ -31,7 +31,7 @@ export * from './CodeSnippet';
 export * from './CodeSyntaxHighlighter';
 export * from './ColorsLegend';
 export * from './CoverageIndicator';
-export { DeferredSpinner } from './DeferredSpinner';
+export { DeferredSpinner, Spinner } from './DeferredSpinner';
 export { ActionsDropdown, Dropdown } from './Dropdown';
 export * from './DropdownMenu';
 export { DropdownToggler } from './DropdownToggler';
index fad77f54f62b2ce4e38b28ae2f73b26dc9744053..8ca0c07377ed639c84d6ded0a22574db2e191366 100644 (file)
@@ -66,7 +66,9 @@ export function cancelAllTasks(): Promise<any> {
   return post('/api/ce/cancel_all');
 }
 
-export function getTasksForComponent(component: string): Promise<{ queue: Task[]; current: Task }> {
+export function getTasksForComponent(
+  component: string
+): Promise<{ queue: Task[]; current?: Task }> {
   return getJSON('/api/ce/component', { component }).catch(throwGlobalError);
 }
 
index 51544f39231c3a8c5ac7df67896cd3fd262a237e..3cb3cbbf21a6052616daae6e3bf59aae2a81b226 100644 (file)
@@ -30,6 +30,7 @@ import {
   getActivity,
   getStatus,
   getTask,
+  getTasksForComponent,
   getTypes,
   getWorkers,
   setWorkerCount,
@@ -65,14 +66,15 @@ export default class ComputeEngineServiceMock {
   workers = { ...DEFAULT_WORKERS };
 
   constructor() {
-    (cancelAllTasks as jest.Mock).mockImplementation(this.handleCancelAllTasks);
-    (cancelTask as jest.Mock).mockImplementation(this.handleCancelTask);
+    jest.mocked(cancelAllTasks).mockImplementation(this.handleCancelAllTasks);
+    jest.mocked(cancelTask).mockImplementation(this.handleCancelTask);
     jest.mocked(getActivity).mockImplementation(this.handleGetActivity);
-    (getStatus as jest.Mock).mockImplementation(this.handleGetStatus);
-    (getTypes as jest.Mock).mockImplementation(this.handleGetTypes);
+    jest.mocked(getStatus).mockImplementation(this.handleGetStatus);
+    jest.mocked(getTypes).mockImplementation(this.handleGetTypes);
     jest.mocked(getTask).mockImplementation(this.handleGetTask);
-    (getWorkers as jest.Mock).mockImplementation(this.handleGetWorkers);
-    (setWorkerCount as jest.Mock).mockImplementation(this.handleSetWorkerCount);
+    jest.mocked(getWorkers).mockImplementation(this.handleGetWorkers);
+    jest.mocked(setWorkerCount).mockImplementation(this.handleSetWorkerCount);
+    jest.mocked(getTasksForComponent).mockImplementation(this.handleGetTaskForComponent);
 
     this.tasks = cloneDeep(DEFAULT_TASKS);
   }
@@ -197,6 +199,18 @@ export default class ComputeEngineServiceMock {
     return Promise.resolve();
   };
 
+  handleGetTaskForComponent = (componentKey: string) => {
+    const tasks = this.tasks.filter((t) => t.componentKey === componentKey);
+    return Promise.resolve({
+      queue: tasks.filter(
+        (t) => t.status === TaskStatuses.InProgress || t.status === TaskStatuses.Pending
+      ),
+      current: tasks.find(
+        (t) => t.status === TaskStatuses.Success || t.status === TaskStatuses.Failed
+      ),
+    });
+  };
+
   /*
    * Helpers
    */
index b0d4039c39e43d99dd1407e4bf9579cc82506ab7..2fde9f19ab0ba2bd5837285419ebc419ac6f489c 100644 (file)
@@ -223,7 +223,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
     qualifier: component.breadcrumbs[component.breadcrumbs.length - 1].qualifier,
   });
 
-  getCurrentTask = (current: Task) => {
+  getCurrentTask = (current?: Task) => {
     if (!current || !this.isReportRelatedTask(current)) {
       return undefined;
     }
index d6231334fcdcd09bd3e28897a00b02b28e321cd9..dc8d50d745a19b2a67fee548a977094743ce3705 100644 (file)
@@ -37,13 +37,11 @@ import EmptyOverview from './EmptyOverview';
 
 interface AppProps extends WithAvailableFeaturesProps {
   component: Component;
-  isInProgress?: boolean;
-  isPending?: boolean;
   projectBinding?: ProjectAlmBindingResponse;
 }
 
 export function App(props: AppProps) {
-  const { component, projectBinding, isPending, isInProgress } = props;
+  const { component, projectBinding } = props;
   const branchSupportEnabled = props.hasFeature(Feature.BranchSupport);
   const { data } = useBranchesQuery(component);
 
@@ -70,7 +68,6 @@ export function App(props: AppProps) {
               branchLike={branchLike}
               branchLikes={branchLikes}
               component={component}
-              hasAnalyses={isPending ?? isInProgress}
             />
           )}
 
index 5ef4b9708c033f1367627de1988f39bb2dd6b3de..783b6ed1a89843da7f8b7d72a3f447e5248708e1 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { FlagMessage, LargeCenteredLayout, PageContentFontWrapper } from 'design-system';
+import { FlagMessage, LargeCenteredLayout, PageContentFontWrapper, Spinner } from 'design-system';
 import * as React from 'react';
 import { Navigate } from 'react-router-dom';
 import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
 import { getBranchLikeDisplayName, isBranch, isMainBranch } from '../../../helpers/branch-like';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { getProjectTutorialLocation } from '../../../helpers/urls';
+import { useTaskForComponentQuery } from '../../../queries/component';
 import { BranchLike } from '../../../types/branch-like';
 import { ComponentQualifier } from '../../../types/component';
+import { TaskTypes } from '../../../types/tasks';
 import { Component } from '../../../types/types';
 import { CurrentUser, isLoggedIn } from '../../../types/users';
 
@@ -34,20 +36,26 @@ export interface EmptyOverviewProps {
   branchLikes: BranchLike[];
   component: Component;
   currentUser: CurrentUser;
-  hasAnalyses?: boolean;
 }
 
 export function EmptyOverview(props: EmptyOverviewProps) {
-  const { branchLike, branchLikes, component, currentUser, hasAnalyses } = props;
+  const { branchLike, branchLikes, component, currentUser } = props;
+  const { data, isLoading } = useTaskForComponentQuery(component);
+  const hasQueuedAnalyses =
+    data && data.queue.filter((task) => task.type === TaskTypes.Report).length > 0;
+  const hasPermissionSyncInProgess =
+    data &&
+    data.queue.filter((task) => task.type === TaskTypes.GithubProjectPermissionsProvisioning)
+      .length > 0;
+
+  if (isLoading) {
+    return <Spinner />;
+  }
 
   if (component.qualifier === ComponentQualifier.Application) {
     return (
       <LargeCenteredLayout className="sw-pt-8">
-        <FlagMessage
-          className="sw-w-full"
-          variant="warning"
-          aria-label={translate('provisioning.no_analysis.application')}
-        >
+        <FlagMessage className="sw-w-full" variant="warning">
           {translate('provisioning.no_analysis.application')}
         </FlagMessage>
       </LargeCenteredLayout>
@@ -61,7 +69,20 @@ export function EmptyOverview(props: EmptyOverviewProps) {
     branchLikes.length > 2 ||
     (branchLikes.length === 2 && branchLikes.some((branch) => isBranch(branch)));
 
-  const showTutorial = isMainBranch(branchLike) && !hasBranches && !hasAnalyses;
+  if (hasPermissionSyncInProgess) {
+    return (
+      <LargeCenteredLayout className="sw-pt-8">
+        <PageContentFontWrapper>
+          <FlagMessage variant="warning">
+            {translate('provisioning.permission_synch_in_progress')}
+            <Spinner className="sw-ml-8 sw-hidden" aria-hidden />
+          </FlagMessage>
+        </PageContentFontWrapper>
+      </LargeCenteredLayout>
+    );
+  }
+
+  const showTutorial = isMainBranch(branchLike) && !hasBranches && !hasQueuedAnalyses;
 
   if (showTutorial && isLoggedIn(currentUser)) {
     return <Navigate replace to={getProjectTutorialLocation(component.key)} />;
@@ -87,13 +108,13 @@ export function EmptyOverview(props: EmptyOverviewProps) {
         {isLoggedIn(currentUser) ? (
           <>
             {hasBranches && (
-              <FlagMessage className="sw-w-full" variant="warning" aria-label={warning}>
+              <FlagMessage className="sw-w-full" variant="warning">
                 {warning}
               </FlagMessage>
             )}
           </>
         ) : (
-          <FlagMessage className="sw-w-full" variant="warning" aria-label={warning}>
+          <FlagMessage className="sw-w-full" variant="warning">
             {warning}
           </FlagMessage>
         )}
index eeaeaf0c1c5cdaf16a01768728c9ef2e42f7d0db..5bab683525a5f8e5d2dfafc595b59fff340fcc1e 100644 (file)
 import { screen } from '@testing-library/react';
 import * as React from 'react';
 import BranchesServiceMock from '../../../../api/mocks/BranchesServiceMock';
+import ComputeEngineServiceMock from '../../../../api/mocks/ComputeEngineServiceMock';
 import CurrentUserContextProvider from '../../../../app/components/current-user/CurrentUserContextProvider';
 import { mockBranch } from '../../../../helpers/mocks/branch-like';
 import { mockComponent } from '../../../../helpers/mocks/component';
+import { mockTask } from '../../../../helpers/mocks/tasks';
 import { mockCurrentUser } from '../../../../helpers/testMocks';
 import { renderComponent } from '../../../../helpers/testReactTestingUtils';
 import { ComponentQualifier } from '../../../../types/component';
+import { TaskStatuses, TaskTypes } from '../../../../types/tasks';
 import { App } from '../App';
 
-const handler = new BranchesServiceMock();
+const handlerBranches = new BranchesServiceMock();
+const handlerCe = new ComputeEngineServiceMock();
 
 beforeEach(() => {
-  handler.reset();
+  handlerBranches.reset();
+  handlerCe.reset();
 });
 
 it('should render Empty Overview for Application with no analysis', async () => {
@@ -72,6 +77,45 @@ it('should not render for portfolios and subportfolios', () => {
   expect(rtl.container).toBeEmptyDOMElement();
 });
 
+describe('Permission provisioning', () => {
+  beforeEach(() => {
+    jest.useFakeTimers({ advanceTimers: true });
+  });
+  afterEach(() => {
+    jest.runOnlyPendingTimers();
+    jest.useRealTimers();
+  });
+  it('should render warning when permission is sync', async () => {
+    handlerCe.addTask(
+      mockTask({
+        componentKey: 'my-project',
+        type: TaskTypes.GithubProjectPermissionsProvisioning,
+        status: TaskStatuses.InProgress,
+      })
+    );
+
+    renderApp();
+    await jest.runOnlyPendingTimersAsync();
+
+    expect(
+      await screen.findByText('provisioning.permission_synch_in_progress')
+    ).toBeInTheDocument();
+
+    handlerCe.clearTasks();
+    handlerCe.addTask(
+      mockTask({
+        componentKey: 'my-project',
+        type: TaskTypes.GithubProjectPermissionsProvisioning,
+        status: TaskStatuses.Success,
+      })
+    );
+
+    await jest.runOnlyPendingTimersAsync();
+
+    expect(screen.queryByText('provisioning.permission_synch_in_progress')).not.toBeInTheDocument();
+  });
+});
+
 function renderApp(props = {}, userProps = {}) {
   return renderComponent(
     <CurrentUserContextProvider currentUser={mockCurrentUser({ isLoggedIn: true, ...userProps })}>
diff --git a/server/sonar-web/src/main/js/queries/component.ts b/server/sonar-web/src/main/js/queries/component.ts
new file mode 100644 (file)
index 0000000..6d9a4d3
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { useQuery } from '@tanstack/react-query';
+import { getTasksForComponent } from '../api/ce';
+import { Component } from '../types/types';
+
+const TASK_RETRY = 10_000;
+
+export function useTaskForComponentQuery(component: Component) {
+  return useQuery({
+    queryKey: ['component', component.key, 'tasks'] as const,
+    queryFn: ({ queryKey: [_, key] }) => getTasksForComponent(key),
+    refetchInterval: TASK_RETRY,
+  });
+}
index 52794ab71376bcf372b73ac486f3fe0658d2d69c..1b8998f0b35fc8f0c11508356e06e8e6dacaec9c 100644 (file)
@@ -21,6 +21,7 @@ export enum TaskTypes {
   Report = 'REPORT',
   IssueSync = 'ISSUE_SYNC',
   GithubProvisioning = 'GITHUB_AUTH_PROVISIONING',
+  GithubProjectPermissionsProvisioning = 'GITHUB_PROJECT_PERMISSIONS_PROVISIONING',
   AppRefresh = 'APP_REFRESH',
   ViewRefresh = 'VIEW_REFRESH',
   ProjectExport = 'PROJECT_EXPORT',
index a19a7f5acf5df320669fc3614e8a9da1c75078cf..6dd26e61136a56a3982e7158d5e4f8434e15ed38 100644 (file)
@@ -2426,6 +2426,7 @@ provisioning.no_analysis_on_main_branch.bad_configuration="{0}" branch has not b
 provisioning.only_provisioned=Only Provisioned
 provisioning.only_provisioned.tooltip=Provisioned projects are projects that have been created, but have not been analyzed yet.
 provisioning.no_analysis.application=No analysis has been performed since creation. Analyze a project to see information here.
+provisioning.permission_synch_in_progress=Project permissions are being synchronized.
 
 
 #------------------------------------------------------------------------------