]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13363 Prevent loading the component twice for nothing
authorWouter Admiraal <45544358+wouter-admiraal-sonarsource@users.noreply.github.com>
Tue, 24 Aug 2021 11:24:18 +0000 (13:24 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 24 Aug 2021 20:07:42 +0000 (20:07 +0000)
server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
server/sonar-web/src/main/js/types/tasks.ts

index 8453c7b8a62739057d5aaef1987c2b93b2d1dce2..ddf887c160e97e37472dd2e92917ec29df739c87 100644 (file)
@@ -42,7 +42,7 @@ import {
 } from '../../types/alm-settings';
 import { BranchLike } from '../../types/branch-like';
 import { ComponentQualifier, isPortfolioLike } from '../../types/component';
-import { Task, TaskStatuses, TaskWarning } from '../../types/tasks';
+import { Task, TaskStatuses, TaskTypes, TaskWarning } from '../../types/tasks';
 import ComponentContainerNotFound from './ComponentContainerNotFound';
 import { ComponentContext } from './ComponentContext';
 import PageUnavailableDueToIndexation from './indexation/PageUnavailableDueToIndexation';
@@ -147,7 +147,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
         loading: false
       });
 
-      this.fetchStatus(componentWithQualifier);
+      this.fetchStatus(componentWithQualifier.key);
       this.fetchWarnings(componentWithQualifier, branchLike);
       this.fetchProjectBindingErrors(componentWithQualifier);
     }
@@ -181,37 +181,30 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
     return { branchLike, branchLikes };
   };
 
-  fetchStatus = (component: T.Component) => {
-    getTasksForComponent(component.key).then(
+  fetchStatus = (componentKey: string) => {
+    getTasksForComponent(componentKey).then(
       ({ current, queue }) => {
         if (this.mounted) {
           let shouldFetchComponent = false;
           this.setState(
             ({ branchLike, component, currentTask, tasksInProgress }) => {
               const newCurrentTask = this.getCurrentTask(current, branchLike);
-              const pendingTasks = this.getPendingTasks(queue, branchLike);
-              const newTasksInProgress = pendingTasks.filter(
-                task => task.status === TaskStatuses.InProgress
+              const pendingTasks = this.getPendingTasksForBranchLike(queue, branchLike);
+              const newTasksInProgress = this.getInProgressTasks(pendingTasks);
+
+              shouldFetchComponent = this.computeShouldFetchComponent(
+                tasksInProgress,
+                newTasksInProgress,
+                currentTask,
+                newCurrentTask,
+                component
               );
 
-              const currentTaskChanged =
-                (!currentTask && newCurrentTask) ||
-                (currentTask && newCurrentTask && currentTask.id !== newCurrentTask.id);
-              const progressChanged =
-                tasksInProgress &&
-                (newTasksInProgress.length !== tasksInProgress.length ||
-                  differenceBy(newTasksInProgress, tasksInProgress, 'id').length > 0);
-
-              shouldFetchComponent = Boolean(currentTaskChanged || progressChanged);
-              if (
-                !shouldFetchComponent &&
-                component &&
-                (newTasksInProgress.length > 0 || !component.analysisDate)
-              ) {
+              if (this.needsAnotherCheck(shouldFetchComponent, component, newTasksInProgress)) {
                 // Refresh the status as long as there is tasks in progress or no analysis
                 window.clearTimeout(this.watchStatusTimer);
                 this.watchStatusTimer = window.setTimeout(
-                  () => this.fetchStatus(component),
+                  () => this.fetchStatus(componentKey),
                   FETCH_STATUS_WAIT_TIME
                 );
               }
@@ -277,7 +270,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
   };
 
   getCurrentTask = (current: Task, branchLike?: BranchLike) => {
-    if (!current) {
+    if (!current || !this.isReportRelatedTask(current)) {
       return undefined;
     }
 
@@ -286,8 +279,64 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
       : undefined;
   };
 
-  getPendingTasks = (pendingTasks: Task[], branchLike?: BranchLike) => {
-    return pendingTasks.filter(task => this.isSameBranch(task, branchLike));
+  getPendingTasksForBranchLike = (pendingTasks: Task[], branchLike?: BranchLike) => {
+    return pendingTasks.filter(
+      task => this.isReportRelatedTask(task) && this.isSameBranch(task, branchLike)
+    );
+  };
+
+  getInProgressTasks = (pendingTasks: Task[]) => {
+    return pendingTasks.filter(task => task.status === TaskStatuses.InProgress);
+  };
+
+  isReportRelatedTask = (task: Task) => {
+    return [TaskTypes.AppRefresh, TaskTypes.Report, TaskTypes.ViewRefresh].includes(task.type);
+  };
+
+  computeShouldFetchComponent = (
+    tasksInProgress: Task[] | undefined,
+    newTasksInProgress: Task[],
+    currentTask: Task | undefined,
+    newCurrentTask: Task | undefined,
+    component: T.Component | undefined
+  ) => {
+    const progressHasChanged = Boolean(
+      tasksInProgress &&
+        (newTasksInProgress.length !== tasksInProgress.length ||
+          differenceBy(newTasksInProgress, tasksInProgress, 'id').length > 0)
+    );
+
+    const currentTaskHasChanged = Boolean(
+      (!currentTask && newCurrentTask) ||
+        (currentTask && newCurrentTask && currentTask.id !== newCurrentTask.id)
+    );
+
+    if (progressHasChanged) {
+      return true;
+    } else if (currentTaskHasChanged && component) {
+      // We return true if:
+      // - there was no prior analysis date (means this is an empty project, and
+      //   a new analysis came in)
+      // - OR, there was a prior analysis date (non-empty project) AND there were
+      //   some tasks in progress before
+      return (
+        Boolean(!component.analysisDate) ||
+        Boolean(component.analysisDate && tasksInProgress?.length)
+      );
+    }
+    return false;
+  };
+
+  needsAnotherCheck = (
+    shouldFetchComponent: boolean,
+    component: T.Component | undefined,
+    newTasksInProgress: Task[]
+  ) => {
+    return (
+      !shouldFetchComponent &&
+      component &&
+      (newTasksInProgress.length > 0 || !component.analysisDate)
+    );
   };
 
   isSameBranch = (task: Pick<Task, 'branch' | 'pullRequest'>, branchLike?: BranchLike) => {
index 4b94887e4f425825d28e40bef33ef5f234293376..d3e921899fefa716d29851bdb6cbab88f8780847 100644 (file)
@@ -31,7 +31,7 @@ import { mockAppState, mockComponent, mockLocation, mockRouter } from '../../../
 import { waitAndUpdate } from '../../../helpers/testUtils';
 import { AlmKeys } from '../../../types/alm-settings';
 import { ComponentQualifier } from '../../../types/component';
-import { TaskStatuses } from '../../../types/tasks';
+import { TaskStatuses, TaskTypes } from '../../../types/tasks';
 import { ComponentContainer } from '../ComponentContainer';
 import PageUnavailableDueToIndexation from '../indexation/PageUnavailableDueToIndexation';
 
@@ -168,7 +168,7 @@ it('filters correctly the pending tasks for a main branch', () => {
   const branch2 = mockBranch({ name: 'branch-2' });
   const pullRequest = mockPullRequest();
 
-  expect(component.isSameBranch({} /*, undefined*/)).toBe(true);
+  expect(component.isSameBranch({})).toBe(true);
   expect(component.isSameBranch({}, mainBranch)).toBe(true);
   expect(component.isSameBranch({ branch: mainBranch.name }, mainBranch)).toBe(true);
   expect(component.isSameBranch({}, branch3)).toBe(false);
@@ -182,19 +182,21 @@ it('filters correctly the pending tasks for a main branch', () => {
   const currentTask = mockTask({ pullRequest: pullRequest.key, status: TaskStatuses.InProgress });
   const failedTask = { ...currentTask, status: TaskStatuses.Failed };
   const pendingTasks = [currentTask, mockTask({ branch: branch3.name }), mockTask()];
-  expect(component.getCurrentTask(currentTask, undefined)).toBeUndefined();
+  expect(component.getCurrentTask(currentTask)).toBeUndefined();
   expect(component.getCurrentTask(failedTask, mainBranch)).toBe(failedTask);
   expect(component.getCurrentTask(currentTask, mainBranch)).toBeUndefined();
   expect(component.getCurrentTask(currentTask, pullRequest)).toMatchObject(currentTask);
-  expect(component.getPendingTasks(pendingTasks, mainBranch)).toMatchObject([{}]);
-  expect(component.getPendingTasks(pendingTasks, pullRequest)).toMatchObject([currentTask]);
+  expect(component.getPendingTasksForBranchLike(pendingTasks, mainBranch)).toMatchObject([{}]);
+  expect(component.getPendingTasksForBranchLike(pendingTasks, pullRequest)).toMatchObject([
+    currentTask
+  ]);
 });
 
 it('reload component after task progress finished', async () => {
   jest.useFakeTimers();
   (getTasksForComponent as jest.Mock<any>)
     .mockResolvedValueOnce({
-      queue: [{ id: 'foo', status: TaskStatuses.InProgress }]
+      queue: [{ id: 'foo', status: TaskStatuses.InProgress, type: TaskTypes.ViewRefresh }]
     })
     .mockResolvedValueOnce({
       queue: []
@@ -237,7 +239,10 @@ it('reloads component after task progress finished, and moves straight to curren
   });
   (getTasksForComponent as jest.Mock<any>)
     .mockResolvedValueOnce({ queue: [] })
-    .mockResolvedValueOnce({ queue: [], current: { id: 'foo', status: TaskStatuses.Success } });
+    .mockResolvedValueOnce({
+      queue: [],
+      current: { id: 'foo', status: TaskStatuses.Success, type: TaskTypes.AppRefresh }
+    });
   const wrapper = shallowRender();
 
   // First round, nothing in the queue, and component navigation was not called
@@ -262,6 +267,56 @@ it('reloads component after task progress finished, and moves straight to curren
   expect(getTasksForComponent).toHaveBeenCalledTimes(3);
 });
 
+it('only fully loads a non-empty component once', async () => {
+  (getComponentData as jest.Mock<any>).mockResolvedValueOnce({
+    component: { key: 'bar', analysisDate: '2019-01-01' }
+  });
+  (getTasksForComponent as jest.Mock<any>).mockResolvedValueOnce({
+    queue: [],
+    current: { id: 'foo', status: TaskStatuses.Success, type: TaskTypes.Report }
+  });
+  const wrapper = shallowRender();
+
+  await waitAndUpdate(wrapper);
+  expect(getComponentNavigation).toHaveBeenCalledTimes(1);
+  expect(getTasksForComponent).toHaveBeenCalledTimes(1);
+});
+
+it('only fully reloads a non-empty component if there was previously some task in progress', async () => {
+  jest.useFakeTimers();
+  (getComponentData as jest.Mock<any>).mockResolvedValueOnce({
+    component: { key: 'bar', analysisDate: '2019-01-01' }
+  });
+  (getTasksForComponent as jest.Mock<any>)
+    .mockResolvedValueOnce({
+      queue: [{ id: 'foo', status: TaskStatuses.InProgress, type: TaskTypes.AppRefresh }]
+    })
+    .mockResolvedValueOnce({
+      queue: [],
+      current: { id: 'foo', status: TaskStatuses.Success, type: TaskTypes.AppRefresh }
+    });
+  const wrapper = shallowRender();
+
+  // First round, a pending task in the queue. This should trigger a reload of the
+  // status endpoint.
+  await waitAndUpdate(wrapper);
+  jest.runOnlyPendingTimers();
+
+  // Second round, nothing in the queue, and a success task is current. This
+  // implies the current task was updated, and previously we displayed some information
+  // about a pending task. This new information must prompt the component to reload
+  // all data.
+  expect(getTasksForComponent).toHaveBeenCalledTimes(2);
+
+  // Trigger the update.
+  await waitAndUpdate(wrapper);
+  // The component was correctly re-loaded.
+  expect(getComponentNavigation).toHaveBeenCalledTimes(2);
+  // The status API call will be called 1 final time after the component is
+  // fully loaded, so the total will be 3.
+  expect(getTasksForComponent).toHaveBeenCalledTimes(3);
+});
+
 it('should show component not found if it does not exist', async () => {
   (getComponentNavigation as jest.Mock).mockRejectedValueOnce({ status: 404 });
   const wrapper = shallowRender();
index 954918c2247afb7062ea12c82fca57201269a3e7..b23ae39d7f44038d94dc0400a2a220ad5b5def0d 100644 (file)
@@ -19,7 +19,9 @@
  */
 export enum TaskTypes {
   Report = 'REPORT',
-  IssueSync = 'ISSUE_SYNC'
+  IssueSync = 'ISSUE_SYNC',
+  AppRefresh = 'APP_REFRESH',
+  ViewRefresh = 'VIEW_REFRESH'
 }
 
 export enum TaskStatuses {