]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19840 Move branch[es], branch-status, branch-warning state to react-query
authorMathieu Suen <mathieu.suen@sonarsource.com>
Tue, 4 Jul 2023 15:51:30 +0000 (17:51 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 18 Jul 2023 20:03:22 +0000 (20:03 +0000)
109 files changed:
server/sonar-web/src/main/js/api/mocks/BranchesServiceMock.ts
server/sonar-web/src/main/js/api/mocks/SecurityHotspotServiceMock.ts
server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
server/sonar-web/src/main/js/app/components/branch-status/BranchStatusContext.tsx [deleted file]
server/sonar-web/src/main/js/app/components/branch-status/BranchStatusContextProvider.tsx [deleted file]
server/sonar-web/src/main/js/app/components/branch-status/__tests__/BranchStatusContextProvider-test.tsx [deleted file]
server/sonar-web/src/main/js/app/components/branch-status/withBranchStatus.tsx [deleted file]
server/sonar-web/src/main/js/app/components/branch-status/withBranchStatusActions.tsx [deleted file]
server/sonar-web/src/main/js/app/components/componentContext/ComponentContext.ts
server/sonar-web/src/main/js/app/components/current-user/withCurrentUserContext.tsx
server/sonar-web/src/main/js/app/components/extensions/Extension.tsx
server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx
server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.tsx
server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectAdminPageExtension-test.tsx
server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectPageExtension-test.tsx
server/sonar-web/src/main/js/app/components/global-search/utils.ts
server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorMessage.tsx
server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorModal.tsx
server/sonar-web/src/main/js/app/components/nav/component/AnalysisStatus.tsx
server/sonar-web/src/main/js/app/components/nav/component/AnalysisWarningsModal.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
server/sonar-web/src/main/js/app/components/nav/component/Header.tsx
server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx
server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisErrorMessage-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/Header-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/HeaderMeta-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/Menu-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx
server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLike.tsx
server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx
server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItem.tsx
server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItemList.tsx
server/sonar-web/src/main/js/app/components/nav/component/branch-like/QualityGateStatus.tsx
server/sonar-web/src/main/js/app/components/promotion-notification/PromotionNotification.tsx
server/sonar-web/src/main/js/app/components/promotion-notification/__tests__/PromotionNotification-test.tsx
server/sonar-web/src/main/js/apps/background-tasks/components/AnalysisWarningsModal.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx
server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx
server/sonar-web/src/main/js/apps/code/components/CodeAppRenderer.tsx
server/sonar-web/src/main/js/apps/code/components/SourceViewerWrapper.tsx
server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx
server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx
server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx
server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx
server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx
server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-it.tsx
server/sonar-web/src/main/js/apps/issues/issues-subnavigation/__tests__/SubnavigationIssues-it.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx
server/sonar-web/src/main/js/apps/overview/components/App.tsx
server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx
server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/PullRequestOverview-it.tsx
server/sonar-web/src/main/js/apps/projectBaseline/components/BranchList.tsx
server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineApp.tsx
server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineApp-it.tsx
server/sonar-web/src/main/js/apps/projectBranches/ProjectBranchesApp.tsx
server/sonar-web/src/main/js/apps/projectBranches/__tests__/ProjectBranchesApp-it.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeRow.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTable.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTabs.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/BranchPurgeSetting.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/DeleteBranchModal.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx
server/sonar-web/src/main/js/apps/projects/filters/__tests__/CoverageFilter-test.tsx
server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguagesFilter-test.tsx
server/sonar-web/src/main/js/apps/projects/filters/__tests__/QualityGateFilter-test.tsx
server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-it.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotHeader.tsx
server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx
server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx
server/sonar-web/src/main/js/components/activity-graph/EventInner.tsx
server/sonar-web/src/main/js/components/activity-graph/__tests__/ActivityGraph-it.tsx
server/sonar-web/src/main/js/components/activity-graph/__tests__/EventInner-it.tsx
server/sonar-web/src/main/js/components/common/AnalysisWarningsModal.tsx [deleted file]
server/sonar-web/src/main/js/components/common/BranchStatus.tsx
server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx
server/sonar-web/src/main/js/components/common/__tests__/BranchStatus-test.tsx
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchStatus-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/components/controls/BoxedTabs.tsx
server/sonar-web/src/main/js/components/issue/Issue.tsx
server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx
server/sonar-web/src/main/js/components/tutorials/TutorialSelection.tsx
server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx
server/sonar-web/src/main/js/components/tutorials/__tests__/TutorialSelection-it.tsx
server/sonar-web/src/main/js/components/workspace/WorkspaceComponentViewer.tsx
server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceComponentViewer-test.tsx
server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/Workspace-test.tsx.snap
server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceComponentViewer-test.tsx.snap
server/sonar-web/src/main/js/helpers/branch-like.ts
server/sonar-web/src/main/js/helpers/mocks/quality-gates.ts
server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx
server/sonar-web/src/main/js/helpers/testSelector.ts
server/sonar-web/src/main/js/helpers/testUtils.ts
server/sonar-web/src/main/js/helpers/urls.ts
server/sonar-web/src/main/js/queries/branch.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/queries/withQueryClientHoc.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/types/__tests__/__snapshots__/component-test.ts.snap
server/sonar-web/src/main/js/types/component.ts
server/sonar-web/src/main/js/types/extension.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index fa6847c1382f854985a3aeace556e5e686228097..af60faa5205e13f4870e9c826bc3c22f7ecf10bb 100644 (file)
@@ -32,7 +32,7 @@ import {
 jest.mock('../branches');
 
 const defaultBranches: Branch[] = [
-  mockBranch({ isMain: true, name: 'master', status: { qualityGateStatus: 'OK' } }),
+  mockBranch({ isMain: true, name: 'main', status: { qualityGateStatus: 'OK' } }),
   mockBranch({
     excludedFromPurge: false,
     name: 'delete-branch',
@@ -113,10 +113,19 @@ export default class BranchesServiceMock {
     this.branches = [];
   };
 
+  emptyBranchesAndPullRequest = () => {
+    this.branches = [];
+    this.pullRequests = [];
+  };
+
   addBranch = (branch: Branch) => {
     this.branches.push(branch);
   };
 
+  addPullRequest = (branch: PullRequest) => {
+    this.pullRequests.push(branch);
+  };
+
   reset = () => {
     this.branches = cloneDeep(defaultBranches);
     this.pullRequests = cloneDeep(defaultPullRequests);
index c5085c5ee9694a358ccded7b085bb07ebff24858..a2086d31ab73dfcfa393af4dd4136344fe958cda 100644 (file)
@@ -137,7 +137,7 @@ export default class SecurityHotspotServiceMock {
       branch?: string;
     }
   ) => {
-    if (data?.branch === 'b1') {
+    if (data?.branch === 'normal-branch') {
       return this.reply({
         paging: mockPaging(),
         hotspots: [
@@ -198,7 +198,7 @@ export default class SecurityHotspotServiceMock {
     inNewCodePeriod?: boolean;
     branch?: string;
   }) => {
-    if (data?.branch === 'b1') {
+    if (data?.branch === 'normal-branch') {
       return this.reply({
         paging: mockPaging({ pageIndex: 1, pageSize: data.ps, total: 2 }),
         hotspots: [
index 0f01470dd74a52a2e4372d017bd53def96effcd7..d9c9c9e9864339d3fd09077fb67408c45f06957f 100644 (file)
@@ -22,17 +22,10 @@ import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
 import { Outlet } from 'react-router-dom';
 import { getProjectAlmBinding, validateProjectAlmBinding } from '../../api/alm-settings';
-import { getBranches, getPullRequests } from '../../api/branches';
-import { getAnalysisStatus, getTasksForComponent } from '../../api/ce';
+import { getTasksForComponent } from '../../api/ce';
 import { getComponentData } from '../../api/components';
 import { getComponentNavigation } from '../../api/navigation';
 import { Location, Router, withRouter } from '../../components/hoc/withRouter';
-import {
-  getBranchLikeQuery,
-  isBranch,
-  isMainBranch,
-  isPullRequest,
-} from '../../helpers/branch-like';
 import { translateWithParameters } from '../../helpers/l10n';
 import { HttpStatus } from '../../helpers/request';
 import { getPortfolioUrl } from '../../helpers/urls';
@@ -40,30 +33,25 @@ import {
   ProjectAlmBindingConfigurationErrors,
   ProjectAlmBindingResponse,
 } from '../../types/alm-settings';
-import { BranchLike } from '../../types/branch-like';
 import { ComponentQualifier, isPortfolioLike } from '../../types/component';
 import { Feature } from '../../types/features';
-import { Task, TaskStatuses, TaskTypes, TaskWarning } from '../../types/tasks';
-import { Component, Status } from '../../types/types';
+import { Task, TaskStatuses, TaskTypes } from '../../types/tasks';
+import { Component } from '../../types/types';
 import handleRequiredAuthorization from '../utils/handleRequiredAuthorization';
 import ComponentContainerNotFound from './ComponentContainerNotFound';
 import withAvailableFeatures, {
   WithAvailableFeaturesProps,
 } from './available-features/withAvailableFeatures';
-import withBranchStatusActions from './branch-status/withBranchStatusActions';
 import { ComponentContext } from './componentContext/ComponentContext';
 import PageUnavailableDueToIndexation from './indexation/PageUnavailableDueToIndexation';
 import ComponentNav from './nav/component/ComponentNav';
 
 interface Props extends WithAvailableFeaturesProps {
   location: Location;
-  updateBranchStatus: (branchLike: BranchLike, component: string, status: Status) => void;
   router: Router;
 }
 
 interface State {
-  branchLike?: BranchLike;
-  branchLikes: BranchLike[];
   component?: Component;
   currentTask?: Task;
   isPending: boolean;
@@ -71,7 +59,6 @@ interface State {
   projectBinding?: ProjectAlmBindingResponse;
   projectBindingErrors?: ProjectAlmBindingConfigurationErrors;
   tasksInProgress?: Task[];
-  warnings: TaskWarning[];
 }
 
 const FETCH_STATUS_WAIT_TIME = 3000;
@@ -79,7 +66,7 @@ const FETCH_STATUS_WAIT_TIME = 3000;
 export class ComponentContainer extends React.PureComponent<Props, State> {
   watchStatusTimer?: number;
   mounted = false;
-  state: State = { branchLikes: [], isPending: false, loading: true, warnings: [] };
+  state: State = { isPending: false, loading: true };
 
   componentDidMount() {
     this.mounted = true;
@@ -135,8 +122,6 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
       this.props.router.replace(getPortfolioUrl(componentWithQualifier.key));
     }
 
-    const { branchLike, branchLikes } = await this.fetchBranches(componentWithQualifier);
-
     let projectBinding;
     if (componentWithQualifier.qualifier === ComponentQualifier.Project) {
       projectBinding = await getProjectAlmBinding(key).catch(() => undefined);
@@ -144,59 +129,25 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
 
     if (this.mounted) {
       this.setState({
-        branchLike,
-        branchLikes,
         component: componentWithQualifier,
         projectBinding,
         loading: false,
       });
 
       this.fetchStatus(componentWithQualifier.key);
-      this.fetchWarnings(componentWithQualifier, branchLike);
       this.fetchProjectBindingErrors(componentWithQualifier);
     }
   };
 
-  fetchBranches = async (componentWithQualifier: Component) => {
-    const { hasFeature } = this.props;
-
-    const breadcrumb = componentWithQualifier.breadcrumbs.find(({ qualifier }) => {
-      return ([ComponentQualifier.Application, ComponentQualifier.Project] as string[]).includes(
-        qualifier
-      );
-    });
-
-    let branchLike = undefined;
-    let branchLikes: BranchLike[] = [];
-
-    if (breadcrumb) {
-      const { key } = breadcrumb;
-      const [branches, pullRequests] = await Promise.all([
-        getBranches(key),
-        !hasFeature(Feature.BranchSupport) ||
-        breadcrumb.qualifier === ComponentQualifier.Application
-          ? Promise.resolve([])
-          : getPullRequests(key),
-      ]);
-
-      branchLikes = [...branches, ...pullRequests];
-      branchLike = this.getCurrentBranchLike(branchLikes);
-
-      this.registerBranchStatuses(branchLikes, componentWithQualifier);
-    }
-
-    return { branchLike, branchLikes };
-  };
-
   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.getPendingTasksForBranchLike(queue, branchLike);
+            ({ component, currentTask, tasksInProgress }) => {
+              const newCurrentTask = this.getCurrentTask(current);
+              const pendingTasks = this.getPendingTasksForBranchLike(queue);
               const newTasksInProgress = this.getInProgressTasks(pendingTasks);
 
               shouldFetchComponent = this.computeShouldFetchComponent(
@@ -235,20 +186,6 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
     );
   };
 
-  fetchWarnings = (component: Component, branchLike?: BranchLike) => {
-    if (component.qualifier === ComponentQualifier.Project) {
-      getAnalysisStatus({
-        component: component.key,
-        ...getBranchLikeQuery(branchLike),
-      }).then(
-        ({ component }) => {
-          this.setState({ warnings: component.warnings });
-        },
-        () => {}
-      );
-    }
-  };
-
   fetchProjectBindingErrors = async (component: Component) => {
     if (
       component.qualifier === ComponentQualifier.Project &&
@@ -269,27 +206,18 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
     qualifier: component.breadcrumbs[component.breadcrumbs.length - 1].qualifier,
   });
 
-  getCurrentBranchLike = (branchLikes: BranchLike[]) => {
-    const { query } = this.props.location;
-    return query.pullRequest
-      ? branchLikes.find((b) => isPullRequest(b) && b.key === query.pullRequest)
-      : branchLikes.find((b) => isBranch(b) && (query.branch ? b.name === query.branch : b.isMain));
-  };
-
-  getCurrentTask = (current: Task, branchLike?: BranchLike) => {
+  getCurrentTask = (current: Task) => {
     if (!current || !this.isReportRelatedTask(current)) {
       return undefined;
     }
 
-    return current.status === TaskStatuses.Failed || this.isSameBranch(current, branchLike)
+    return current.status === TaskStatuses.Failed || this.isSameBranch(current)
       ? current
       : undefined;
   };
 
-  getPendingTasksForBranchLike = (pendingTasks: Task[], branchLike?: BranchLike) => {
-    return pendingTasks.filter(
-      (task) => this.isReportRelatedTask(task) && this.isSameBranch(task, branchLike)
-    );
+  getPendingTasksForBranchLike = (pendingTasks: Task[]) => {
+    return pendingTasks.filter((task) => this.isReportRelatedTask(task) && this.isSameBranch(task));
   };
 
   getInProgressTasks = (pendingTasks: Task[]) => {
@@ -346,31 +274,19 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
     );
   };
 
-  isSameBranch = (task: Pick<Task, 'branch' | 'pullRequest'>, branchLike?: BranchLike) => {
-    if (branchLike) {
-      if (isMainBranch(branchLike)) {
-        return (!task.pullRequest && !task.branch) || branchLike.name === task.branch;
-      }
-      if (isPullRequest(branchLike)) {
-        return branchLike.key === task.pullRequest;
-      }
-      if (isBranch(branchLike)) {
-        return branchLike.name === task.branch;
-      }
-    }
-    return !task.branch && !task.pullRequest;
-  };
+  isSameBranch = (task: Pick<Task, 'branch' | 'pullRequest'>) => {
+    const { branch, pullRequest } = this.props.location.query;
 
-  registerBranchStatuses = (branchLikes: BranchLike[], component: Component) => {
-    branchLikes.forEach((branchLike) => {
-      if (branchLike.status) {
-        this.props.updateBranchStatus(
-          branchLike,
-          component.key,
-          branchLike.status.qualityGateStatus
-        );
-      }
-    });
+    if (!pullRequest && !branch) {
+      return !task.branch && !task.pullRequest;
+    }
+    if (pullRequest) {
+      return pullRequest === task.pullRequest;
+    }
+    if (branch) {
+      return branch === task.branch;
+    }
+    return false;
   };
 
   handleComponentChange = (changes: Partial<Component>) => {
@@ -385,33 +301,6 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
     }
   };
 
-  handleBranchesChange = () => {
-    const { router, location } = this.props;
-    const { component } = this.state;
-
-    if (this.mounted && component) {
-      this.fetchBranches(component).then(
-        ({ branchLike, branchLikes }) => {
-          if (this.mounted) {
-            this.setState({ branchLike, branchLikes });
-
-            if (branchLike === undefined) {
-              router.replace({ query: { ...location.query, branch: undefined } });
-            }
-          }
-        },
-        () => {}
-      );
-    }
-  };
-
-  handleWarningDismiss = () => {
-    const { component } = this.state;
-    if (component !== undefined) {
-      this.fetchWarnings(component);
-    }
-  };
-
   render() {
     const { component, loading } = this.state;
 
@@ -423,16 +312,8 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
       return <PageUnavailableDueToIndexation component={component} />;
     }
 
-    const {
-      branchLike,
-      branchLikes,
-      currentTask,
-      isPending,
-      projectBinding,
-      projectBindingErrors,
-      tasksInProgress,
-      warnings,
-    } = this.state;
+    const { currentTask, isPending, projectBinding, projectBindingErrors, tasksInProgress } =
+      this.state;
     const isInProgress = tasksInProgress && tasksInProgress.length > 0;
 
     return (
@@ -449,17 +330,12 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
             component.qualifier
           ) && (
             <ComponentNav
-              branchLikes={branchLikes}
               component={component}
-              currentBranchLike={branchLike}
               currentTask={currentTask}
-              currentTaskOnSameBranch={currentTask && this.isSameBranch(currentTask, branchLike)}
               isInProgress={isInProgress}
               isPending={isPending}
-              onWarningDismiss={this.handleWarningDismiss}
               projectBinding={projectBinding}
               projectBindingErrors={projectBindingErrors}
-              warnings={warnings}
             />
           )}
         {loading ? (
@@ -469,12 +345,9 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
         ) : (
           <ComponentContext.Provider
             value={{
-              branchLike,
-              branchLikes,
               component,
               isInProgress,
               isPending,
-              onBranchesChange: this.handleBranchesChange,
               onComponentChange: this.handleComponentChange,
               projectBinding,
             }}
@@ -487,4 +360,4 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
   }
 }
 
-export default withRouter(withAvailableFeatures(withBranchStatusActions(ComponentContainer)));
+export default withRouter(withAvailableFeatures(ComponentContainer));
index b1ed60e0ce49543d010ddd3d5d7e033b8ed493e1..05c5c8148ca20f2085d96df0513568aefe87051b 100644 (file)
@@ -29,7 +29,6 @@ import Workspace from '../../components/workspace/Workspace';
 import GlobalFooter from './GlobalFooter';
 import StartupModal from './StartupModal';
 import SystemAnnouncement from './SystemAnnouncement';
-import BranchStatusContextProvider from './branch-status/BranchStatusContextProvider';
 import IndexationContextProvider from './indexation/IndexationContextProvider';
 import IndexationNotification from './indexation/IndexationNotification';
 import LanguagesContextProvider from './languages/LanguagesContextProvider';
@@ -68,21 +67,19 @@ export default function GlobalContainer() {
                 id="container"
               >
                 <div className="page-container">
-                  <BranchStatusContextProvider>
-                    <Workspace>
-                      <IndexationContextProvider>
-                        <LanguagesContextProvider>
-                          <MetricsContextProvider>
-                            <SystemAnnouncement />
-                            <IndexationNotification />
-                            <UpdateNotification dismissable />
-                            <GlobalNav location={location} />
-                            <Outlet />
-                          </MetricsContextProvider>
-                        </LanguagesContextProvider>
-                      </IndexationContextProvider>
-                    </Workspace>
-                  </BranchStatusContextProvider>
+                  <Workspace>
+                    <IndexationContextProvider>
+                      <LanguagesContextProvider>
+                        <MetricsContextProvider>
+                          <SystemAnnouncement />
+                          <IndexationNotification />
+                          <UpdateNotification dismissable />
+                          <GlobalNav location={location} />
+                          <Outlet />
+                        </MetricsContextProvider>
+                      </LanguagesContextProvider>
+                    </IndexationContextProvider>
+                  </Workspace>
                 </div>
                 <PromotionNotification />
               </div>
index 27c68dc4db1e05761c9c0104ae8dd97ebbdef5d1..9faf1d9486b6d1c136275a1d15ef1bd7e4a82899 100644 (file)
@@ -21,7 +21,7 @@ import { shallow } from 'enzyme';
 import * as React from 'react';
 import { getProjectAlmBinding, validateProjectAlmBinding } from '../../../api/alm-settings';
 import { getBranches, getPullRequests } from '../../../api/branches';
-import { getAnalysisStatus, getTasksForComponent } from '../../../api/ce';
+import { getTasksForComponent } from '../../../api/ce';
 import { getComponentData } from '../../../api/components';
 import { getComponentNavigation } from '../../../api/navigation';
 import { mockProjectAlmBindingConfigurationErrors } from '../../../helpers/mocks/alm-settings';
@@ -97,7 +97,6 @@ afterEach(() => {
 it('changes component', () => {
   const wrapper = shallowRender();
   wrapper.setState({
-    branchLikes: [mockMainBranch()],
     component: {
       qualifier: ComponentQualifier.Project,
       visibility: Visibility.Public,
@@ -147,42 +146,6 @@ it("doesn't load branches portfolio", async () => {
   });
 });
 
-it('updates branches on change', async () => {
-  const updateBranchStatus = jest.fn();
-  const wrapper = shallowRender({
-    hasFeature: () => true,
-    location: mockLocation({ query: { id: 'portfolioKey' } }),
-    updateBranchStatus,
-  });
-  wrapper.setState({
-    branchLikes: [mockMainBranch()],
-    component: mockComponent({
-      breadcrumbs: [{ key: 'projectKey', name: 'project', qualifier: ComponentQualifier.Project }],
-    }),
-    loading: false,
-  });
-  wrapper.instance().handleBranchesChange();
-  expect(getBranches).toHaveBeenCalledWith('projectKey');
-  expect(getPullRequests).toHaveBeenCalledWith('projectKey');
-  await waitAndUpdate(wrapper);
-  expect(updateBranchStatus).toHaveBeenCalledTimes(2);
-});
-
-it('sets main branch when current branch is not found', async () => {
-  const router = mockRouter();
-  const wrapper = shallowRender({
-    hasFeature: () => true,
-    location: mockLocation({ query: { id: 'portfolioKey', branch: 'any-branch' } }),
-    router,
-  });
-  await waitAndUpdate(wrapper);
-
-  wrapper.instance().handleBranchesChange();
-  await waitAndUpdate(wrapper);
-
-  expect(router.replace).toHaveBeenCalledWith({ query: { id: 'portfolioKey' } });
-});
-
 it('fetches status', async () => {
   (getComponentData as jest.Mock<any>).mockResolvedValueOnce({
     component: {},
@@ -198,31 +161,29 @@ it('filters correctly the pending tasks for a main branch', () => {
   const component = wrapper.instance();
   const mainBranch = mockMainBranch();
   const branch3 = mockBranch({ name: 'branch-3' });
-  const branch2 = mockBranch({ name: 'branch-2' });
   const pullRequest = mockPullRequest();
 
   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);
-  expect(component.isSameBranch({ branch: branch3.name }, branch3)).toBe(true);
-  expect(component.isSameBranch({ branch: 'feature' }, branch2)).toBe(false);
-  expect(component.isSameBranch({ branch: 'branch-6.6' }, branch2)).toBe(false);
-  expect(component.isSameBranch({ branch: branch2.name }, branch2)).toBe(true);
-  expect(component.isSameBranch({ branch: 'branch-6.7' }, pullRequest)).toBe(false);
-  expect(component.isSameBranch({ pullRequest: pullRequest.key }, pullRequest)).toBe(true);
+  wrapper.setProps({ location: mockLocation({ query: { branch: mainBranch.name } }) });
+  expect(component.isSameBranch({ branch: mainBranch.name })).toBe(true);
+  expect(component.isSameBranch({})).toBe(false);
+  wrapper.setProps({ location: mockLocation({ query: { branch: branch3.name } }) });
+  expect(component.isSameBranch({ branch: branch3.name })).toBe(true);
+  wrapper.setProps({ location: mockLocation({ query: { pullRequest: pullRequest.key } }) });
+  expect(component.isSameBranch({ pullRequest: pullRequest.key })).toBe(true);
 
   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(failedTask)).toBe(failedTask);
+  wrapper.setProps({ location: mockLocation({ query: {} }) });
   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.getPendingTasksForBranchLike(pendingTasks, mainBranch)).toMatchObject([{}]);
-  expect(component.getPendingTasksForBranchLike(pendingTasks, pullRequest)).toMatchObject([
-    currentTask,
-  ]);
+  wrapper.setProps({ location: mockLocation({ query: { pullRequest: pullRequest.key } }) });
+  expect(component.getCurrentTask(currentTask)).toMatchObject(currentTask);
+
+  expect(component.getPendingTasksForBranchLike(pendingTasks)).toMatchObject([currentTask]);
+  wrapper.setProps({ location: mockLocation({ query: {} }) });
+  expect(component.getPendingTasksForBranchLike(pendingTasks)).toMatchObject([{}]);
 });
 
 it('reload component after task progress finished', async () => {
@@ -393,22 +354,6 @@ it('should display display the unavailable page if the component needs issue syn
   expect(wrapper.find(PageUnavailableDueToIndexation).exists()).toBe(true);
 });
 
-it('should correctly reload last task warnings if anything got dismissed', async () => {
-  (getComponentData as jest.Mock<any>).mockResolvedValueOnce({
-    component: mockComponent({
-      breadcrumbs: [{ key: 'foo', name: 'Foo', qualifier: ComponentQualifier.Project }],
-    }),
-  });
-  (getComponentNavigation as jest.Mock).mockResolvedValueOnce({});
-
-  const wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-  (getAnalysisStatus as jest.Mock).mockClear();
-
-  wrapper.instance().handleWarningDismiss();
-  expect(getAnalysisStatus).toHaveBeenCalledTimes(1);
-});
-
 describe('should correctly validate the project binding depending on the context', () => {
   const COMPONENT = mockComponent({
     breadcrumbs: [{ key: 'foo', name: 'Foo', qualifier: ComponentQualifier.Project }],
@@ -461,7 +406,6 @@ function shallowRender(props: Partial<ComponentContainer['props']> = {}) {
     <ComponentContainer
       hasFeature={jest.fn().mockReturnValue(false)}
       location={mockLocation({ query: { id: 'foo' } })}
-      updateBranchStatus={jest.fn()}
       router={mockRouter()}
       {...props}
     >
diff --git a/server/sonar-web/src/main/js/app/components/branch-status/BranchStatusContext.tsx b/server/sonar-web/src/main/js/app/components/branch-status/BranchStatusContext.tsx
deleted file mode 100644 (file)
index 57c4ee1..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * 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 * as React from 'react';
-import { BranchLike, BranchStatusData } from '../../../types/branch-like';
-import { QualityGateStatusCondition } from '../../../types/quality-gates';
-import { Dict, Status } from '../../../types/types';
-
-export interface BranchStatusContextInterface {
-  branchStatusByComponent: Dict<Dict<BranchStatusData>>;
-  fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => void;
-  updateBranchStatus: (
-    branchLike: BranchLike,
-    projectKey: string,
-    status: Status,
-    conditions?: QualityGateStatusCondition[],
-    ignoredConditions?: boolean
-  ) => void;
-}
-
-export const BranchStatusContext = React.createContext<BranchStatusContextInterface>({
-  branchStatusByComponent: {},
-  fetchBranchStatus: () => {
-    throw Error('BranchStatusContext is not provided');
-  },
-  updateBranchStatus: () => {
-    throw Error('BranchStatusContext is not provided');
-  },
-});
diff --git a/server/sonar-web/src/main/js/app/components/branch-status/BranchStatusContextProvider.tsx b/server/sonar-web/src/main/js/app/components/branch-status/BranchStatusContextProvider.tsx
deleted file mode 100644 (file)
index cf2d2ba..0000000
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * 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 * as React from 'react';
-import { getQualityGateProjectStatus } from '../../../api/quality-gates';
-import { getBranchLikeKey, getBranchLikeQuery } from '../../../helpers/branch-like';
-import { extractStatusConditionsFromProjectStatus } from '../../../helpers/qualityGates';
-import { BranchLike, BranchStatusData } from '../../../types/branch-like';
-import { QualityGateStatusCondition } from '../../../types/quality-gates';
-import { Dict, Status } from '../../../types/types';
-import { BranchStatusContext } from './BranchStatusContext';
-
-interface State {
-  branchStatusByComponent: Dict<Dict<BranchStatusData>>;
-}
-
-export default class BranchStatusContextProvider extends React.PureComponent<{}, State> {
-  mounted = false;
-  state: State = {
-    branchStatusByComponent: {},
-  };
-
-  componentDidMount() {
-    this.mounted = true;
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  fetchBranchStatus = async (branchLike: BranchLike, projectKey: string) => {
-    const projectStatus = await getQualityGateProjectStatus({
-      projectKey,
-      ...getBranchLikeQuery(branchLike),
-    }).catch(() => undefined);
-
-    if (!this.mounted || projectStatus === undefined) {
-      return;
-    }
-
-    const { ignoredConditions, status } = projectStatus;
-    const conditions = extractStatusConditionsFromProjectStatus(projectStatus);
-
-    this.updateBranchStatus(branchLike, projectKey, status, conditions, ignoredConditions);
-  };
-
-  updateBranchStatus = (
-    branchLike: BranchLike,
-    projectKey: string,
-    status: Status,
-    conditions?: QualityGateStatusCondition[],
-    ignoredConditions?: boolean
-  ) => {
-    const branchLikeKey = getBranchLikeKey(branchLike);
-
-    this.setState(({ branchStatusByComponent }) => ({
-      branchStatusByComponent: {
-        ...branchStatusByComponent,
-        [projectKey]: {
-          ...(branchStatusByComponent[projectKey] || {}),
-          [branchLikeKey]: {
-            conditions,
-            ignoredConditions,
-            status,
-          },
-        },
-      },
-    }));
-  };
-
-  render() {
-    return (
-      <BranchStatusContext.Provider
-        value={{
-          branchStatusByComponent: this.state.branchStatusByComponent,
-          fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => {
-            this.fetchBranchStatus(branchLike, projectKey).catch(() => {
-              /* noop */
-            });
-          },
-          updateBranchStatus: this.updateBranchStatus,
-        }}
-      >
-        {this.props.children}
-      </BranchStatusContext.Provider>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/app/components/branch-status/__tests__/BranchStatusContextProvider-test.tsx b/server/sonar-web/src/main/js/app/components/branch-status/__tests__/BranchStatusContextProvider-test.tsx
deleted file mode 100644 (file)
index 4add5f7..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import { getQualityGateProjectStatus } from '../../../../api/quality-gates';
-import { mockBranch } from '../../../../helpers/mocks/branch-like';
-import { waitAndUpdate } from '../../../../helpers/testUtils';
-import { BranchStatusData } from '../../../../types/branch-like';
-import BranchStatusContextProvider from '../BranchStatusContextProvider';
-
-jest.mock('../../../../api/quality-gates', () => ({
-  getQualityGateProjectStatus: jest.fn().mockResolvedValue({}),
-}));
-
-describe('fetchBranchStatus', () => {
-  it('should get the branch status', async () => {
-    const projectKey = 'projectKey';
-    const branchName = 'branch-6.7';
-    const status: BranchStatusData = {
-      status: 'OK',
-      conditions: [],
-      ignoredConditions: false,
-    };
-    (getQualityGateProjectStatus as jest.Mock).mockResolvedValueOnce(status);
-    const wrapper = shallowRender();
-
-    wrapper.instance().fetchBranchStatus(mockBranch({ name: branchName }), projectKey);
-
-    expect(getQualityGateProjectStatus).toHaveBeenCalledWith({ projectKey, branch: branchName });
-
-    await waitAndUpdate(wrapper);
-
-    expect(wrapper.state().branchStatusByComponent).toEqual({
-      [projectKey]: { [`branch-${branchName}`]: status },
-    });
-  });
-
-  it('should ignore errors', async () => {
-    (getQualityGateProjectStatus as jest.Mock).mockRejectedValueOnce('error');
-    const wrapper = shallowRender();
-
-    wrapper.instance().fetchBranchStatus(mockBranch(), 'project');
-
-    await waitAndUpdate(wrapper);
-
-    expect(wrapper.state().branchStatusByComponent).toEqual({});
-  });
-});
-
-function shallowRender() {
-  return shallow<BranchStatusContextProvider>(
-    <BranchStatusContextProvider>
-      <div />
-    </BranchStatusContextProvider>
-  );
-}
diff --git a/server/sonar-web/src/main/js/app/components/branch-status/withBranchStatus.tsx b/server/sonar-web/src/main/js/app/components/branch-status/withBranchStatus.tsx
deleted file mode 100644 (file)
index 9433940..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * 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 * as React from 'react';
-import { getWrappedDisplayName } from '../../../components/hoc/utils';
-import { getBranchStatusByBranchLike } from '../../../helpers/branch-like';
-import { BranchLike, BranchStatusData } from '../../../types/branch-like';
-import { Component } from '../../../types/types';
-import { BranchStatusContext } from './BranchStatusContext';
-
-export default function withBranchStatus<
-  P extends { branchLike: BranchLike; component: Component }
->(WrappedComponent: React.ComponentType<P & BranchStatusData>) {
-  return class WithBranchStatus extends React.PureComponent<Omit<P, keyof BranchStatusData>> {
-    static displayName = getWrappedDisplayName(WrappedComponent, 'withBranchStatus');
-
-    render() {
-      const { branchLike, component } = this.props;
-
-      return (
-        <BranchStatusContext.Consumer>
-          {({ branchStatusByComponent }) => {
-            const { conditions, ignoredConditions, status } = getBranchStatusByBranchLike(
-              branchStatusByComponent,
-              component.key,
-              branchLike
-            );
-
-            return (
-              <WrappedComponent
-                conditions={conditions}
-                ignoredConditions={ignoredConditions}
-                status={status}
-                {...(this.props as P)}
-              />
-            );
-          }}
-        </BranchStatusContext.Consumer>
-      );
-    }
-  };
-}
diff --git a/server/sonar-web/src/main/js/app/components/branch-status/withBranchStatusActions.tsx b/server/sonar-web/src/main/js/app/components/branch-status/withBranchStatusActions.tsx
deleted file mode 100644 (file)
index 365c030..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * 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 * as React from 'react';
-import { getWrappedDisplayName } from '../../../components/hoc/utils';
-import { BranchStatusContext, BranchStatusContextInterface } from './BranchStatusContext';
-
-export type WithBranchStatusActionsProps =
-  | Pick<BranchStatusContextInterface, 'fetchBranchStatus'>
-  | Pick<BranchStatusContextInterface, 'updateBranchStatus'>;
-
-export default function withBranchStatusActions<P>(
-  WrappedComponent: React.ComponentType<P & WithBranchStatusActionsProps>
-) {
-  return class WithBranchStatusActions extends React.PureComponent<
-    Omit<P, keyof BranchStatusContextInterface>
-  > {
-    static displayName = getWrappedDisplayName(WrappedComponent, 'withBranchStatusActions');
-
-    render() {
-      return (
-        <BranchStatusContext.Consumer>
-          {({ fetchBranchStatus, updateBranchStatus }) => (
-            <WrappedComponent
-              fetchBranchStatus={fetchBranchStatus}
-              updateBranchStatus={updateBranchStatus}
-              {...(this.props as P)}
-            />
-          )}
-        </BranchStatusContext.Consumer>
-      );
-    }
-  };
-}
index 4f19ef2eda39e2ee8fb1381bb0bf995cbc4824dd..8fd8ae528eaa4202326e29d903966973d9b83d7f 100644 (file)
@@ -22,7 +22,5 @@ import * as React from 'react';
 import { ComponentContextShape } from '../../../types/component';
 
 export const ComponentContext = React.createContext<ComponentContextShape>({
-  branchLikes: [],
-  onBranchesChange: noop,
   onComponentChange: noop,
 });
index 7849c5e95546b456bc16c5fec3c63e638de5dcc8..9f0e85ba96dc0f73968a48b4a4bc21e5ab81bf6d 100644 (file)
@@ -25,7 +25,7 @@ export default function withCurrentUserContext<P>(
   WrappedComponent: React.ComponentType<P & Pick<CurrentUserContextInterface, 'currentUser'>>
 ) {
   return class WithCurrentUserContext extends React.PureComponent<
-    Omit<P, keyof CurrentUserContextInterface>
+    Omit<P, 'currentUser' | 'updateCurrentUserHomepage' | 'updateDismissedNotices'>
   > {
     static displayName = getWrappedDisplayName(WrappedComponent, 'withCurrentUserContext');
 
index d3535c539beff59e4f553a00515833927372441d..179d690caa94cd9a430ec12446d75a46cb6defdb 100644 (file)
@@ -18,6 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { withTheme } from '@emotion/react';
+import { QueryClient } from '@tanstack/react-query';
 import { Theme } from 'design-system';
 import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
@@ -28,6 +29,7 @@ import { addGlobalErrorMessage } from '../../../helpers/globalMessages';
 import { translate } from '../../../helpers/l10n';
 import { getCurrentL10nBundle } from '../../../helpers/l10nBundle';
 import { getBaseUrl } from '../../../helpers/system';
+import { withQueryClient } from '../../../queries/withQueryClientHoc';
 import { AppState } from '../../../types/appstate';
 import { ExtensionStartMethod } from '../../../types/extension';
 import { Dict, Extension as TypeExtension } from '../../../types/types';
@@ -44,6 +46,7 @@ export interface ExtensionProps extends WrappedComponentProps {
   location: Location;
   options?: Dict<any>;
   router: Router;
+  queryClient: QueryClient;
   updateCurrentUserHomepage: (homepage: HomePage) => void;
 }
 
@@ -74,7 +77,7 @@ class Extension extends React.PureComponent<ExtensionProps, State> {
   }
 
   handleStart = (start: ExtensionStartMethod) => {
-    const { theme: dsTheme } = this.props;
+    const { theme: dsTheme, queryClient } = this.props;
     const result = start({
       appState: this.props.appState,
       el: this.container,
@@ -90,6 +93,7 @@ class Extension extends React.PureComponent<ExtensionProps, State> {
       // See SONAR-16207 and core-extension-enterprise-server/src/main/js/portfolios/components/Header.tsx
       // for more information on why we're passing this as a prop to an extension.
       updateCurrentUserHomepage: this.props.updateCurrentUserHomepage,
+      queryClient,
       ...this.props.options,
     });
 
@@ -134,5 +138,5 @@ class Extension extends React.PureComponent<ExtensionProps, State> {
 }
 
 export default injectIntl(
-  withRouter(withTheme(withAppStateContext(withCurrentUserContext(Extension))))
+  withRouter(withTheme(withAppStateContext(withCurrentUserContext(withQueryClient(Extension)))))
 );
index 83046f78c3a2c6b0391abe80e92afc5ae2dfeb63..26ed70a56e9fd32bec1c5117bbc4375fcaf8016d 100644 (file)
  */
 import * as React from 'react';
 import { useParams } from 'react-router-dom';
+import { useRefreshBranches } from '../../../queries/branch';
 import NotFound from '../NotFound';
 import { ComponentContext } from '../componentContext/ComponentContext';
 import Extension from './Extension';
 
 export default function ProjectAdminPageExtension() {
   const { extensionKey, pluginKey } = useParams();
-  const { component, onBranchesChange, onComponentChange } = React.useContext(ComponentContext);
+  const { component, onComponentChange } = React.useContext(ComponentContext);
+
+  // We keep that for compatibility but ideally should advocate to use tanstack query
+  const onBranchesChange = useRefreshBranches();
 
   const extension =
     component &&
@@ -35,7 +39,7 @@ export default function ProjectAdminPageExtension() {
     );
 
   return extension ? (
-    <Extension extension={extension} options={{ component, onBranchesChange, onComponentChange }} />
+    <Extension extension={extension} options={{ component, onComponentChange, onBranchesChange }} />
   ) : (
     <NotFound withContainer={false} />
   );
index 747504f02a8bd7797e6ffe509f38eba3b6ae7c29..33b78f118445650766bda5639caa51c6564e7ecb 100644 (file)
@@ -19,6 +19,7 @@
  */
 import * as React from 'react';
 import { useParams } from 'react-router-dom';
+import { useBranchesQuery } from '../../../queries/branch';
 import NotFound from '../NotFound';
 import { ComponentContext } from '../componentContext/ComponentContext';
 import Extension from './Extension';
@@ -32,12 +33,14 @@ export interface ProjectPageExtensionProps {
 
 export default function ProjectPageExtension({ params }: ProjectPageExtensionProps) {
   const { extensionKey, pluginKey } = useParams();
-  const { branchLike, component } = React.useContext(ComponentContext);
+  const { component } = React.useContext(ComponentContext);
+  const { data } = useBranchesQuery(component);
 
-  if (component === undefined) {
+  if (component === undefined || data === undefined) {
     return null;
   }
 
+  const { branchLike } = data;
   const fullKey =
     params !== undefined
       ? `${params.pluginKey}/${params.extensionKey}`
index e0d6a4c327dc103ffb05b82861fb7e24f2f4bfbc..5d7ed345ae88392f803ab2a0d2bfcfaae5e7ec36 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
 import { render, screen } from '@testing-library/react';
 import * as React from 'react';
 import { HelmetProvider } from 'react-helmet-async';
@@ -59,17 +60,20 @@ function renderProjectAdminPageExtension(
   }
 ) {
   const { pluginKey, extensionKey } = params;
+  const queryClient = new QueryClient();
   return render(
-    <HelmetProvider context={{}}>
-      <IntlProvider defaultLocale="en" locale="en">
-        <ComponentContext.Provider value={{ component } as ComponentContextShape}>
-          <MemoryRouter initialEntries={[`/${pluginKey}/${extensionKey}`]}>
-            <Routes>
-              <Route path="/:pluginKey/:extensionKey" element={<ProjectAdminPageExtension />} />
-            </Routes>
-          </MemoryRouter>
-        </ComponentContext.Provider>
-      </IntlProvider>
-    </HelmetProvider>
+    <QueryClientProvider client={queryClient}>
+      <HelmetProvider context={{}}>
+        <IntlProvider defaultLocale="en" locale="en">
+          <ComponentContext.Provider value={{ component } as ComponentContextShape}>
+            <MemoryRouter initialEntries={[`/${pluginKey}/${extensionKey}`]}>
+              <Routes>
+                <Route path="/:pluginKey/:extensionKey" element={<ProjectAdminPageExtension />} />
+              </Routes>
+            </MemoryRouter>
+          </ComponentContext.Provider>
+        </IntlProvider>
+      </HelmetProvider>
+    </QueryClientProvider>
   );
 }
index c2f8e87184608f5efb0deaabc4122b617a7d1a3a..d61aeee9739ed7cea54f20e58d929533e44c3be7 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 { render, screen } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { render, screen, waitFor } from '@testing-library/react';
 import * as React from 'react';
 import { HelmetProvider } from 'react-helmet-async';
 import { IntlProvider } from 'react-intl';
 import { MemoryRouter, Route, Routes } from 'react-router-dom';
+import BranchesServiceMock from '../../../../api/mocks/BranchesServiceMock';
 import { getExtensionStart } from '../../../../helpers/extensions';
 import { mockComponent } from '../../../../helpers/mocks/component';
 import { ComponentContextShape } from '../../../../types/component';
@@ -33,51 +35,60 @@ jest.mock('../../../../helpers/extensions', () => ({
   getExtensionStart: jest.fn().mockResolvedValue(jest.fn()),
 }));
 
+const handler = new BranchesServiceMock();
+
+beforeEach(() => {
+  handler.reset();
+});
+
 it('should not render when no component is passed', () => {
   renderProjectPageExtension();
   expect(screen.queryByText('page_not_found')).not.toBeInTheDocument();
   expect(getExtensionStart).not.toHaveBeenCalledWith('pluginId/extensionId');
 });
 
-it('should render correctly when the extension is found', () => {
+it('should render correctly when the extension is found', async () => {
   renderProjectPageExtension(
     mockComponent({ extensions: [{ key: 'pluginId/extensionId', name: 'name' }] }),
     { params: { pluginKey: 'pluginId', extensionKey: 'extensionId' } }
   );
-  expect(getExtensionStart).toHaveBeenCalledWith('pluginId/extensionId');
+  await waitFor(() => expect(getExtensionStart).toHaveBeenCalledWith('pluginId/extensionId'));
 });
 
-it('should render correctly when the extension is not found', () => {
+it('should render correctly when the extension is not found', async () => {
   renderProjectPageExtension(
     mockComponent({ extensions: [{ key: 'pluginId/extensionId', name: 'name' }] }),
     { params: { pluginKey: 'not-found-plugin', extensionKey: 'not-found-extension' } }
   );
-  expect(screen.getByText('page_not_found')).toBeInTheDocument();
+  expect(await screen.findByText('page_not_found')).toBeInTheDocument();
 });
 
 function renderProjectPageExtension(
   component?: Component,
   props?: Partial<ProjectPageExtensionProps>
 ) {
+  const queryClient = new QueryClient();
   return render(
-    <HelmetProvider context={{}}>
-      <IntlProvider defaultLocale="en" locale="en">
-        <ComponentContext.Provider value={{ component } as ComponentContextShape}>
-          <MemoryRouter>
-            <Routes>
-              <Route
-                path="*"
-                element={
-                  <ProjectPageExtension
-                    params={{ extensionKey: 'extensionId', pluginKey: 'pluginId' }}
-                    {...props}
-                  />
-                }
-              />
-            </Routes>
-          </MemoryRouter>
-        </ComponentContext.Provider>
-      </IntlProvider>
-    </HelmetProvider>
+    <QueryClientProvider client={queryClient}>
+      <HelmetProvider context={{}}>
+        <IntlProvider defaultLocale="en" locale="en">
+          <ComponentContext.Provider value={{ component } as ComponentContextShape}>
+            <MemoryRouter initialEntries={[`/?id=${component?.key}`]}>
+              <Routes>
+                <Route
+                  path="*"
+                  element={
+                    <ProjectPageExtension
+                      params={{ extensionKey: 'extensionId', pluginKey: 'pluginId' }}
+                      {...props}
+                    />
+                  }
+                />
+              </Routes>
+            </MemoryRouter>
+          </ComponentContext.Provider>
+        </IntlProvider>
+      </HelmetProvider>
+    </QueryClientProvider>
   );
 }
index 4fbc1c4b1dbfcec3fe153e68a56b699290f33a8b..15ca34de7c31dcb59654ecd07a1251dc95352764 100644 (file)
@@ -21,7 +21,6 @@ import { sortBy } from 'lodash';
 import { ComponentQualifier } from '../../../../js/types/component';
 
 const ORDER = [
-  ComponentQualifier.Developper,
   ComponentQualifier.Portfolio,
   ComponentQualifier.SubPortfolio,
   ComponentQualifier.Application,
index 00b8bda2c62d0a55354449ea369172ec51ca14d5..0ec4af077df7f641f20852b667779f96856fecef 100644 (file)
@@ -22,20 +22,39 @@ import { Link } from 'design-system';
 import React from 'react';
 import { FormattedMessage } from 'react-intl';
 import { useLocation } from 'react-router-dom';
+import { isBranch, isMainBranch, isPullRequest } from '../../../../helpers/branch-like';
 import { hasMessage, translate } from '../../../../helpers/l10n';
 import { getComponentBackgroundTaskUrl } from '../../../../helpers/urls';
+import { useBranchesQuery } from '../../../../queries/branch';
+import { BranchLike } from '../../../../types/branch-like';
 import { Task } from '../../../../types/tasks';
 import { Component } from '../../../../types/types';
 
 interface Props {
   component: Component;
   currentTask: Task;
-  currentTaskOnSameBranch?: boolean;
   onLeave: () => void;
 }
 
+function isSameBranch(task: Task, branchLike?: BranchLike) {
+  if (branchLike) {
+    if (isMainBranch(branchLike)) {
+      return (!task.pullRequest && !task.branch) || branchLike.name === task.branch;
+    }
+    if (isPullRequest(branchLike)) {
+      return branchLike.key === task.pullRequest;
+    }
+    if (isBranch(branchLike)) {
+      return branchLike.name === task.branch;
+    }
+  }
+  return !task.branch && !task.pullRequest;
+}
+
 export function AnalysisErrorMessage(props: Props) {
-  const { component, currentTask, currentTaskOnSameBranch } = props;
+  const { component, currentTask } = props;
+  const { data: { branchLike } = {} } = useBranchesQuery(component);
+  const currentTaskOnSameBranch = isSameBranch(currentTask, branchLike);
 
   const location = useLocation();
 
index bd3687844217816d15873d612c36c61688a13b9a..ea77cdbc3432d0c567267323ead28b61aaa0020e 100644 (file)
@@ -29,12 +29,11 @@ import { AnalysisLicenseError } from './AnalysisLicenseError';
 interface Props {
   component: Component;
   currentTask: Task;
-  currentTaskOnSameBranch?: boolean;
   onClose: () => void;
 }
 
 export function AnalysisErrorModal(props: Props) {
-  const { component, currentTask, currentTaskOnSameBranch } = props;
+  const { component, currentTask } = props;
 
   const header = translate('error');
 
@@ -55,7 +54,6 @@ export function AnalysisErrorModal(props: Props) {
           <AnalysisErrorMessage
             component={component}
             currentTask={currentTask}
-            currentTaskOnSameBranch={currentTaskOnSameBranch}
             onLeave={props.onClose}
           />
         )}
index 4d8953398fadef1f7bcabb58b8da4a704a551a51..a082485aa6578e7a7a370fb45e9bf69e6a0f76aa 100644 (file)
  */
 import { DeferredSpinner, FlagMessage, Link } from 'design-system';
 import * as React from 'react';
-import AnalysisWarningsModal from '../../../../components/common/AnalysisWarningsModal';
 import { translate } from '../../../../helpers/l10n';
-import { Task, TaskStatuses, TaskWarning } from '../../../../types/tasks';
+import { useBranchWarrningQuery } from '../../../../queries/branch';
+import { Task, TaskStatuses } from '../../../../types/tasks';
 import { Component } from '../../../../types/types';
 import { AnalysisErrorModal } from './AnalysisErrorModal';
+import AnalysisWarningsModal from './AnalysisWarningsModal';
 
 export interface HeaderMetaProps {
   currentTask?: Task;
-  currentTaskOnSameBranch?: boolean;
   component: Component;
   isInProgress?: boolean;
   isPending?: boolean;
-  onWarningDismiss: () => void;
-  warnings: TaskWarning[];
 }
 
 export function AnalysisStatus(props: HeaderMetaProps) {
-  const { component, currentTask, currentTaskOnSameBranch, isInProgress, isPending, warnings } =
-    props;
+  const { component, currentTask, isInProgress, isPending } = props;
+  const { data: warnings, isLoading } = useBranchWarrningQuery(component);
 
   const [modalIsVisible, setDisplayModal] = React.useState(false);
   const openModal = React.useCallback(() => {
@@ -73,7 +71,6 @@ export function AnalysisStatus(props: HeaderMetaProps) {
           <AnalysisErrorModal
             component={component}
             currentTask={currentTask}
-            currentTaskOnSameBranch={currentTaskOnSameBranch}
             onClose={closeModal}
           />
         )}
@@ -81,7 +78,7 @@ export function AnalysisStatus(props: HeaderMetaProps) {
     );
   }
 
-  if (warnings.length > 0) {
+  if (!isLoading && warnings && warnings.length > 0) {
     return (
       <>
         <FlagMessage variant="warning">
@@ -91,13 +88,7 @@ export function AnalysisStatus(props: HeaderMetaProps) {
           </Link>
         </FlagMessage>
         {modalIsVisible && (
-          <AnalysisWarningsModal
-            componentKey={component.key}
-            onClose={closeModal}
-            taskId={currentTask?.id}
-            onWarningDismiss={props.onWarningDismiss}
-            warnings={warnings}
-          />
+          <AnalysisWarningsModal component={component} onClose={closeModal} warnings={warnings} />
         )}
       </>
     );
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisWarningsModal.tsx b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisWarningsModal.tsx
new file mode 100644 (file)
index 0000000..90f7da9
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * 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 {
+  DangerButtonSecondary,
+  DeferredSpinner,
+  FlagMessage,
+  HtmlFormatter,
+  Modal,
+} from 'design-system';
+import * as React from 'react';
+import { translate } from '../../../../helpers/l10n';
+import { sanitizeStringRestricted } from '../../../../helpers/sanitize';
+import { useDismissBranchWarningMutation } from '../../../../queries/branch';
+import { TaskWarning } from '../../../../types/tasks';
+import { Component } from '../../../../types/types';
+import { CurrentUser } from '../../../../types/users';
+import withCurrentUserContext from '../../current-user/withCurrentUserContext';
+
+interface Props {
+  component: Component;
+  currentUser: CurrentUser;
+  onClose: () => void;
+  warnings: TaskWarning[];
+}
+
+export function AnalysisWarningsModal(props: Props) {
+  const { component, currentUser, warnings } = props;
+
+  const { mutate, isLoading, variables } = useDismissBranchWarningMutation();
+
+  const handleDismissMessage = (messageKey: string) => {
+    mutate({ component, key: messageKey });
+  };
+
+  const body = (
+    <>
+      {warnings.map(({ dismissable, key, message }) => (
+        <React.Fragment key={key}>
+          <div className="sw-flex sw-items-center sw-mt-2">
+            <FlagMessage variant="warning">
+              <HtmlFormatter>
+                <span
+                  // eslint-disable-next-line react/no-danger
+                  dangerouslySetInnerHTML={{
+                    __html: sanitizeStringRestricted(message.trim().replace(/\n/g, '<br />')),
+                  }}
+                />
+              </HtmlFormatter>
+            </FlagMessage>
+          </div>
+          <div>
+            {dismissable && currentUser.isLoggedIn && (
+              <div className="sw-mt-4">
+                <DangerButtonSecondary
+                  disabled={Boolean(isLoading)}
+                  onClick={() => {
+                    handleDismissMessage(key);
+                  }}
+                >
+                  {translate('dismiss_permanently')}
+                </DangerButtonSecondary>
+
+                <DeferredSpinner
+                  className="sw-ml-2"
+                  loading={isLoading && variables?.key === key}
+                />
+              </div>
+            )}
+          </div>
+        </React.Fragment>
+      ))}
+    </>
+  );
+
+  return (
+    <Modal
+      headerTitle={translate('warnings')}
+      onClose={props.onClose}
+      body={body}
+      primaryButton={null}
+      secondaryButtonLabel={translate('close')}
+    />
+  );
+}
+
+export default withCurrentUserContext(AnalysisWarningsModal);
index 3f1db236c3b214a13e1f8c97cddbf948d3b0d783..f81c5a66e042f573d10634df501914d98630a3dc 100644 (file)
@@ -24,9 +24,8 @@ import {
   ProjectAlmBindingConfigurationErrors,
   ProjectAlmBindingResponse,
 } from '../../../../types/alm-settings';
-import { BranchLike } from '../../../../types/branch-like';
 import { ComponentQualifier } from '../../../../types/component';
-import { Task, TaskWarning } from '../../../../types/tasks';
+import { Task } from '../../../../types/tasks';
 import { Component } from '../../../../types/types';
 import RecentHistory from '../../RecentHistory';
 import ComponentNavProjectBindingErrorNotif from './ComponentNavProjectBindingErrorNotif';
@@ -35,34 +34,17 @@ import HeaderMeta from './HeaderMeta';
 import Menu from './Menu';
 
 export interface ComponentNavProps {
-  branchLikes: BranchLike[];
-  currentBranchLike: BranchLike | undefined;
   component: Component;
   currentTask?: Task;
-  currentTaskOnSameBranch?: boolean;
   isInProgress?: boolean;
   isPending?: boolean;
-  onWarningDismiss: () => void;
   projectBinding?: ProjectAlmBindingResponse;
   projectBindingErrors?: ProjectAlmBindingConfigurationErrors;
-  warnings: TaskWarning[];
 }
 
 export default function ComponentNav(props: ComponentNavProps) {
-  const {
-    branchLikes,
-    component,
-    currentBranchLike,
-    currentTask,
-    currentTaskOnSameBranch,
-    isInProgress,
-    isPending,
-    projectBinding,
-    projectBindingErrors,
-    warnings,
-  } = props;
-
-  const [displayProjectInfo, setDisplayProjectInfo] = React.useState(false);
+  const { component, currentTask, isInProgress, isPending, projectBinding, projectBindingErrors } =
+    props;
 
   React.useEffect(() => {
     const { breadcrumbs, key, name } = component;
@@ -72,7 +54,6 @@ export default function ComponentNav(props: ComponentNavProps) {
         ComponentQualifier.Project,
         ComponentQualifier.Portfolio,
         ComponentQualifier.Application,
-        ComponentQualifier.Developper,
       ].includes(qualifier as ComponentQualifier)
     ) {
       RecentHistory.add(key, name, qualifier.toLowerCase());
@@ -88,34 +69,15 @@ export default function ComponentNav(props: ComponentNavProps) {
     <>
       <TopBar id="context-navigation" aria-label={translate('qualifier', component.qualifier)}>
         <div className="sw-min-h-10 sw-flex sw-justify-between">
-          <Header
-            branchLikes={branchLikes}
-            component={component}
-            currentBranchLike={currentBranchLike}
-            projectBinding={projectBinding}
-          />
+          <Header component={component} projectBinding={projectBinding} />
           <HeaderMeta
-            branchLike={currentBranchLike}
             component={component}
             currentTask={currentTask}
-            currentTaskOnSameBranch={currentTaskOnSameBranch}
             isInProgress={isInProgress}
             isPending={isPending}
-            onWarningDismiss={props.onWarningDismiss}
-            warnings={warnings}
           />
         </div>
-        <Menu
-          branchLike={currentBranchLike}
-          branchLikes={branchLikes}
-          component={component}
-          isInProgress={isInProgress}
-          isPending={isPending}
-          onToggleProjectInfo={() => {
-            setDisplayProjectInfo(!displayProjectInfo);
-          }}
-          projectInfoDisplayed={displayProjectInfo}
-        />
+        <Menu component={component} isInProgress={isInProgress} isPending={isPending} />
       </TopBar>
       {prDecoNotifComponent}
     </>
index d52c2dc672803f4de94abe874d38c3a33ed4433c..92e82ebb4dc344de19dcb57de21f950e680ceb96 100644 (file)
  */
 import * as React from 'react';
 import { ProjectAlmBindingResponse } from '../../../../types/alm-settings';
-import { BranchLike } from '../../../../types/branch-like';
 import { Component } from '../../../../types/types';
 import { CurrentUser } from '../../../../types/users';
 import withCurrentUserContext from '../../current-user/withCurrentUserContext';
-import BranchLikeNavigation from './branch-like/BranchLikeNavigation';
 import { Breadcrumb } from './Breadcrumb';
+import BranchLikeNavigation from './branch-like/BranchLikeNavigation';
 
 export interface HeaderProps {
-  branchLikes: BranchLike[];
   component: Component;
-  currentBranchLike: BranchLike | undefined;
   currentUser: CurrentUser;
   projectBinding?: ProjectAlmBindingResponse;
 }
 
 export function Header(props: HeaderProps) {
-  const { branchLikes, component, currentBranchLike, currentUser, projectBinding } = props;
+  const { component, currentUser, projectBinding } = props;
 
   return (
     <div className="sw-flex sw-flex-shrink sw-items-center">
       <Breadcrumb component={component} currentUser={currentUser} />
-      {currentBranchLike && (
-        <>
-          <span className="slash-separator sw-mx-2" />
-          <BranchLikeNavigation
-            branchLikes={branchLikes}
-            component={component}
-            currentBranchLike={currentBranchLike}
-            projectBinding={projectBinding}
-          />
-        </>
-      )}
+
+      <BranchLikeNavigation component={component} projectBinding={projectBinding} />
     </div>
   );
 }
index 56d22719501e96acbea4f3d1a964263cd49302cd..5e1043531c25b26757c8dfcbb4a41557c0afcaf0 100644 (file)
@@ -22,8 +22,8 @@ import * as React from 'react';
 import HomePageSelect from '../../../../components/controls/HomePageSelect';
 import { isBranch, isPullRequest } from '../../../../helpers/branch-like';
 import { translateWithParameters } from '../../../../helpers/l10n';
-import { BranchLike } from '../../../../types/branch-like';
-import { Task, TaskWarning } from '../../../../types/tasks';
+import { useBranchesQuery } from '../../../../queries/branch';
+import { Task } from '../../../../types/tasks';
 import { Component } from '../../../../types/types';
 import { CurrentUser, isLoggedIn } from '../../../../types/users';
 import withCurrentUserContext from '../../current-user/withCurrentUserContext';
@@ -32,28 +32,17 @@ import CurrentBranchLikeMergeInformation from './branch-like/CurrentBranchLikeMe
 import { getCurrentPage } from './utils';
 
 export interface HeaderMetaProps {
-  branchLike?: BranchLike;
   component: Component;
   currentUser: CurrentUser;
   currentTask?: Task;
-  currentTaskOnSameBranch?: boolean;
   isInProgress?: boolean;
   isPending?: boolean;
-  onWarningDismiss: () => void;
-  warnings: TaskWarning[];
 }
 
 export function HeaderMeta(props: HeaderMetaProps) {
-  const {
-    branchLike,
-    component,
-    currentUser,
-    currentTask,
-    currentTaskOnSameBranch,
-    isInProgress,
-    isPending,
-    warnings,
-  } = props;
+  const { component, currentUser, currentTask, isInProgress, isPending } = props;
+
+  const { data: { branchLike } = {} } = useBranchesQuery(component);
 
   const isABranch = isBranch(branchLike);
 
@@ -64,11 +53,8 @@ export function HeaderMeta(props: HeaderMetaProps) {
       <AnalysisStatus
         component={component}
         currentTask={currentTask}
-        currentTaskOnSameBranch={currentTaskOnSameBranch}
         isInProgress={isInProgress}
         isPending={isPending}
-        onWarningDismiss={props.onWarningDismiss}
-        warnings={warnings}
       />
       {branchLike && <CurrentBranchLikeMergeInformation currentBranchLike={branchLike} />}
       {component.version !== undefined && isABranch && (
index a3a7111bc8f4941bf3b910e00ebadde095b347c0..d011f9d949e6ddec1f607ca80513cee251510bab 100644 (file)
@@ -30,8 +30,14 @@ import Tooltip from '../../../../components/controls/Tooltip';
 import { getBranchLikeQuery, isPullRequest } from '../../../../helpers/branch-like';
 import { hasMessage, translate, translateWithParameters } from '../../../../helpers/l10n';
 import { getPortfolioUrl, getProjectQueryUrl } from '../../../../helpers/urls';
-import { BranchLike, BranchParameters } from '../../../../types/branch-like';
-import { ComponentQualifier, isPortfolioLike } from '../../../../types/component';
+import { useBranchesQuery } from '../../../../queries/branch';
+import { BranchParameters } from '../../../../types/branch-like';
+import {
+  ComponentQualifier,
+  isApplication,
+  isPortfolioLike,
+  isProject,
+} from '../../../../types/component';
 import { Feature } from '../../../../types/features';
 import { Component, Dict, Extension } from '../../../../types/types';
 import withAvailableFeatures, {
@@ -55,84 +61,39 @@ const SETTINGS_URLS = [
 ];
 
 interface Props extends WithAvailableFeaturesProps {
-  branchLike: BranchLike | undefined;
-  branchLikes: BranchLike[] | undefined;
   component: Component;
   isInProgress?: boolean;
   isPending?: boolean;
-  onToggleProjectInfo: () => void;
-  projectInfoDisplayed: boolean;
 }
 
 type Query = BranchParameters & { id: string };
 
-export class Menu extends React.PureComponent<Props> {
-  projectInfoLink: HTMLElement | null = null;
-
-  componentDidUpdate(prevProps: Props) {
-    if (
-      prevProps.projectInfoDisplayed &&
-      !this.props.projectInfoDisplayed &&
-      this.projectInfoLink
-    ) {
-      this.projectInfoLink.focus();
-    }
-  }
+export function Menu(props: Props) {
+  const { component, isInProgress, isPending } = props;
+  const { extensions = [], canBrowseAllChildProjects, qualifier, configuration = {} } = component;
+  const { data: { branchLikes, branchLike } = { branchLikes: [] } } = useBranchesQuery(component);
+  const isApplicationChildInaccessble = isApplication(qualifier) && !canBrowseAllChildProjects;
 
-  hasAnalysis = () => {
-    const { branchLikes = [], component, isInProgress, isPending } = this.props;
+  const hasAnalysis = () => {
     const hasBranches = branchLikes.length > 1;
     return hasBranches || isInProgress || isPending || component.analysisDate !== undefined;
   };
 
-  isProject = () => {
-    return this.props.component.qualifier === ComponentQualifier.Project;
-  };
-
-  isDeveloper = () => {
-    return this.props.component.qualifier === ComponentQualifier.Developper;
-  };
-
-  isPortfolio = () => {
-    const { qualifier } = this.props.component;
-    return isPortfolioLike(qualifier);
-  };
-
-  isApplication = () => {
-    return this.props.component.qualifier === ComponentQualifier.Application;
-  };
-
-  isAllChildProjectAccessible = () => {
-    return Boolean(this.props.component.canBrowseAllChildProjects);
-  };
+  const isGovernanceEnabled = extensions.some((extension) =>
+    extension.key.startsWith('governance/')
+  );
 
-  isApplicationChildInaccessble = () => {
-    return this.isApplication() && !this.isAllChildProjectAccessible();
+  const getQuery = (): Query => {
+    return { id: component.key, ...getBranchLikeQuery(branchLike) };
   };
 
-  isGovernanceEnabled = () => {
-    const {
-      component: { extensions },
-    } = this.props;
-
-    return extensions && extensions.some((extension) => extension.key.startsWith('governance/'));
-  };
-
-  getConfiguration = () => {
-    return this.props.component.configuration || {};
-  };
-
-  getQuery = (): Query => {
-    return { id: this.props.component.key, ...getBranchLikeQuery(this.props.branchLike) };
-  };
-
-  renderLinkWhenInaccessibleChild(label: React.ReactNode) {
+  const renderLinkWhenInaccessibleChild = (label: React.ReactNode) => {
     return (
       <li>
         <Tooltip
           overlay={translateWithParameters(
             'layout.all_project_must_be_accessible',
-            translate('qualifier', this.props.component.qualifier)
+            translate('qualifier', qualifier)
           )}
         >
           <a aria-disabled="true" className="disabled-link">
@@ -141,9 +102,9 @@ export class Menu extends React.PureComponent<Props> {
         </Tooltip>
       </li>
     );
-  }
+  };
 
-  renderMenuLink = ({
+  const renderMenuLink = ({
     label,
     pathname,
     additionalQueryParams = {},
@@ -152,13 +113,11 @@ export class Menu extends React.PureComponent<Props> {
     pathname: string;
     additionalQueryParams?: Dict<string>;
   }) => {
-    const hasAnalysis = this.hasAnalysis();
-    const isApplicationChildInaccessble = this.isApplicationChildInaccessble();
-    const query = this.getQuery();
+    const query = getQuery();
     if (isApplicationChildInaccessble) {
-      return this.renderLinkWhenInaccessibleChild(label);
+      return renderLinkWhenInaccessibleChild(label);
     }
-    return hasAnalysis ? (
+    return hasAnalysis() ? (
       <NavBarTabLink
         to={{
           pathname,
@@ -171,86 +130,82 @@ export class Menu extends React.PureComponent<Props> {
     );
   };
 
-  renderDashboardLink = () => {
-    const { id, ...branchLike } = this.getQuery();
+  const renderDashboardLink = () => {
+    const { id, ...branchLike } = getQuery();
 
-    if (this.isPortfolio()) {
-      return this.isGovernanceEnabled() ? (
+    if (isPortfolioLike(qualifier)) {
+      return isGovernanceEnabled ? (
         <NavBarTabLink to={getPortfolioUrl(id)} text={translate('overview.page')} />
       ) : null;
     }
 
-    const isApplicationChildInaccessble = this.isApplicationChildInaccessble();
     if (isApplicationChildInaccessble) {
-      return this.renderLinkWhenInaccessibleChild(translate('overview.page'));
+      return renderLinkWhenInaccessibleChild(translate('overview.page'));
     }
     return (
       <NavBarTabLink to={getProjectQueryUrl(id, branchLike)} text={translate('overview.page')} />
     );
   };
 
-  renderBreakdownLink = () => {
-    return this.isPortfolio() && this.isGovernanceEnabled()
-      ? this.renderMenuLink({
+  const renderBreakdownLink = () => {
+    return isPortfolioLike(qualifier) && isGovernanceEnabled
+      ? renderMenuLink({
           label: translate('portfolio_breakdown.page'),
           pathname: '/code',
         })
       : null;
   };
 
-  renderCodeLink = () => {
-    if (this.isPortfolio() || this.isDeveloper()) {
+  const renderCodeLink = () => {
+    if (isPortfolioLike(qualifier)) {
       return null;
     }
 
-    const label = this.isApplication() ? translate('view_projects.page') : translate('code.page');
+    const label = isApplication(qualifier)
+      ? translate('view_projects.page')
+      : translate('code.page');
 
-    return this.renderMenuLink({ label, pathname: '/code' });
+    return renderMenuLink({ label, pathname: '/code' });
   };
 
-  renderActivityLink = () => {
-    const { branchLike } = this.props;
-
+  const renderActivityLink = () => {
     if (isPullRequest(branchLike)) {
       return null;
     }
 
-    return this.renderMenuLink({
+    return renderMenuLink({
       label: translate('project_activity.page'),
       pathname: '/project/activity',
     });
   };
 
-  renderIssuesLink = () => {
-    return this.renderMenuLink({
+  const renderIssuesLink = () => {
+    return renderMenuLink({
       label: translate('issues.page'),
       pathname: '/project/issues',
       additionalQueryParams: { resolved: 'false' },
     });
   };
 
-  renderComponentMeasuresLink = () => {
-    return this.renderMenuLink({
+  const renderComponentMeasuresLink = () => {
+    return renderMenuLink({
       label: translate('layout.measures'),
       pathname: '/component_measures',
     });
   };
 
-  renderSecurityHotspotsLink = () => {
-    const isPortfolio = this.isPortfolio();
+  const renderSecurityHotspotsLink = () => {
+    const isPortfolio = isPortfolioLike(qualifier);
     return (
       !isPortfolio &&
-      this.renderMenuLink({
+      renderMenuLink({
         label: translate('layout.security_hotspots'),
         pathname: '/security_hotspots',
       })
     );
   };
 
-  renderSecurityReports = () => {
-    const { branchLike, component } = this.props;
-    const { extensions = [] } = component;
-
+  const renderSecurityReports = () => {
     if (isPullRequest(branchLike)) {
       return null;
     }
@@ -263,26 +218,27 @@ export class Menu extends React.PureComponent<Props> {
       return null;
     }
 
-    return this.renderMenuLink({
+    return renderMenuLink({
       label: translate('layout.security_reports'),
       pathname: '/project/extension/securityreport/securityreport',
     });
   };
 
-  renderAdministration = () => {
-    const { branchLike, component } = this.props;
-    const isProject = this.isProject();
-    const isPortfolio = this.isPortfolio();
-    const isApplication = this.isApplication();
-    const query = this.getQuery();
+  const renderAdministration = () => {
+    const query = getQuery();
 
-    if (!this.getConfiguration().showSettings || isPullRequest(branchLike)) {
+    if (!configuration.showSettings || isPullRequest(branchLike)) {
       return null;
     }
 
     const isSettingsActive = SETTINGS_URLS.some((url) => window.location.href.includes(url));
 
-    const adminLinks = this.renderAdministrationLinks(query, isProject, isApplication, isPortfolio);
+    const adminLinks = renderAdministrationLinks(
+      query,
+      isProject(qualifier),
+      isApplication(qualifier),
+      isPortfolioLike(qualifier)
+    );
     if (!adminLinks.some((link) => link != null)) {
       return null;
     }
@@ -313,46 +269,43 @@ export class Menu extends React.PureComponent<Props> {
     );
   };
 
-  renderAdministrationLinks = (
+  const renderAdministrationLinks = (
     query: Query,
     isProject: boolean,
     isApplication: boolean,
     isPortfolio: boolean
   ) => {
     return [
-      this.renderSettingsLink(query, isApplication, isPortfolio),
-      this.renderBranchesLink(query, isProject),
-      this.renderBaselineLink(query, isApplication, isPortfolio),
-      ...this.renderAdminExtensions(query, isApplication),
-      this.renderImportExportLink(query, isProject),
-      this.renderProfilesLink(query),
-      this.renderQualityGateLink(query),
-      this.renderLinksLink(query),
-      this.renderPermissionsLink(query),
-      this.renderBackgroundTasksLink(query),
-      this.renderUpdateKeyLink(query),
-      this.renderWebhooksLink(query, isProject),
-      this.renderDeletionLink(query),
+      renderSettingsLink(query, isApplication, isPortfolio),
+      renderBranchesLink(query, isProject),
+      renderBaselineLink(query, isApplication, isPortfolio),
+      ...renderAdminExtensions(query, isApplication),
+      renderImportExportLink(query, isProject),
+      renderProfilesLink(query),
+      renderQualityGateLink(query),
+      renderLinksLink(query),
+      renderPermissionsLink(query),
+      renderBackgroundTasksLink(query),
+      renderUpdateKeyLink(query),
+      renderWebhooksLink(query, isProject),
+      renderDeletionLink(query),
     ];
   };
 
-  renderProjectInformationButton = () => {
-    const isProject = this.isProject();
-    const isApplication = this.isApplication();
-    const label = translate(isProject ? 'project' : 'application', 'info.title');
-    const isApplicationChildInaccessble = this.isApplicationChildInaccessble();
-    const query = this.getQuery();
+  const renderProjectInformationButton = () => {
+    const label = translate(isProject(qualifier) ? 'project' : 'application', 'info.title');
+    const query = getQuery();
 
-    if (isPullRequest(this.props.branchLike)) {
+    if (isPullRequest(branchLike)) {
       return null;
     }
 
     if (isApplicationChildInaccessble) {
-      return this.renderLinkWhenInaccessibleChild(label);
+      return renderLinkWhenInaccessibleChild(label);
     }
 
     return (
-      (isProject || isApplication) && (
+      (isProject(qualifier) || isApplication(qualifier)) && (
         <NavBarTabLink
           to={{ pathname: '/project/information', search: new URLSearchParams(query).toString() }}
           text={label}
@@ -361,8 +314,8 @@ export class Menu extends React.PureComponent<Props> {
     );
   };
 
-  renderSettingsLink = (query: Query, isApplication: boolean, isPortfolio: boolean) => {
-    if (!this.getConfiguration().showSettings || isApplication || isPortfolio) {
+  const renderSettingsLink = (query: Query, isApplication: boolean, isPortfolio: boolean) => {
+    if (!configuration.showSettings || isApplication || isPortfolio) {
       return null;
     }
     return (
@@ -375,12 +328,8 @@ export class Menu extends React.PureComponent<Props> {
     );
   };
 
-  renderBranchesLink = (query: Query, isProject: boolean) => {
-    if (
-      !this.props.hasFeature(Feature.BranchSupport) ||
-      !isProject ||
-      !this.getConfiguration().showSettings
-    ) {
+  const renderBranchesLink = (query: Query, isProject: boolean) => {
+    if (!props.hasFeature(Feature.BranchSupport) || !isProject || !configuration.showSettings) {
       return null;
     }
 
@@ -394,8 +343,8 @@ export class Menu extends React.PureComponent<Props> {
     );
   };
 
-  renderBaselineLink = (query: Query, isApplication: boolean, isPortfolio: boolean) => {
-    if (!this.getConfiguration().showSettings || isApplication || isPortfolio) {
+  const renderBaselineLink = (query: Query, isApplication: boolean, isPortfolio: boolean) => {
+    if (!configuration.showSettings || isApplication || isPortfolio) {
       return null;
     }
     return (
@@ -408,7 +357,7 @@ export class Menu extends React.PureComponent<Props> {
     );
   };
 
-  renderImportExportLink = (query: Query, isProject: boolean) => {
+  const renderImportExportLink = (query: Query, isProject: boolean) => {
     if (!isProject) {
       return null;
     }
@@ -425,8 +374,8 @@ export class Menu extends React.PureComponent<Props> {
     );
   };
 
-  renderProfilesLink = (query: Query) => {
-    if (!this.getConfiguration().showQualityProfiles) {
+  const renderProfilesLink = (query: Query) => {
+    if (!configuration.showQualityProfiles) {
       return null;
     }
     return (
@@ -442,8 +391,8 @@ export class Menu extends React.PureComponent<Props> {
     );
   };
 
-  renderQualityGateLink = (query: Query) => {
-    if (!this.getConfiguration().showQualityGates) {
+  const renderQualityGateLink = (query: Query) => {
+    if (!configuration.showQualityGates) {
       return null;
     }
     return (
@@ -456,8 +405,8 @@ export class Menu extends React.PureComponent<Props> {
     );
   };
 
-  renderLinksLink = (query: Query) => {
-    if (!this.getConfiguration().showLinks) {
+  const renderLinksLink = (query: Query) => {
+    if (!configuration.showLinks) {
       return null;
     }
     return (
@@ -470,8 +419,8 @@ export class Menu extends React.PureComponent<Props> {
     );
   };
 
-  renderPermissionsLink = (query: Query) => {
-    if (!this.getConfiguration().showPermissions) {
+  const renderPermissionsLink = (query: Query) => {
+    if (!configuration.showPermissions) {
       return null;
     }
     return (
@@ -484,8 +433,8 @@ export class Menu extends React.PureComponent<Props> {
     );
   };
 
-  renderBackgroundTasksLink = (query: Query) => {
-    if (!this.getConfiguration().showBackgroundTasks) {
+  const renderBackgroundTasksLink = (query: Query) => {
+    if (!configuration.showBackgroundTasks) {
       return null;
     }
     return (
@@ -501,8 +450,8 @@ export class Menu extends React.PureComponent<Props> {
     );
   };
 
-  renderUpdateKeyLink = (query: Query) => {
-    if (!this.getConfiguration().showUpdateKey) {
+  const renderUpdateKeyLink = (query: Query) => {
+    if (!configuration.showUpdateKey) {
       return null;
     }
     return (
@@ -515,8 +464,8 @@ export class Menu extends React.PureComponent<Props> {
     );
   };
 
-  renderWebhooksLink = (query: Query, isProject: boolean) => {
-    if (!this.getConfiguration().showSettings || !isProject) {
+  const renderWebhooksLink = (query: Query, isProject: boolean) => {
+    if (!configuration.showSettings || !isProject) {
       return null;
     }
     return (
@@ -529,10 +478,8 @@ export class Menu extends React.PureComponent<Props> {
     );
   };
 
-  renderDeletionLink = (query: Query) => {
-    const { qualifier } = this.props.component;
-
-    if (!this.getConfiguration().showSettings) {
+  const renderDeletionLink = (query: Query) => {
+    if (!configuration.showSettings) {
       return null;
     }
 
@@ -556,9 +503,9 @@ export class Menu extends React.PureComponent<Props> {
     );
   };
 
-  renderExtension = ({ key, name }: Extension, isAdmin: boolean, baseQuery: Query) => {
+  const renderExtension = ({ key, name }: Extension, isAdmin: boolean, baseQuery: Query) => {
     const pathname = isAdmin ? `/project/admin/extension/${key}` : `/project/extension/${key}`;
-    const query = { ...baseQuery, qualifier: this.props.component.qualifier };
+    const query = { ...baseQuery, qualifier };
     return (
       <ItemNavLink key={key} to={{ pathname, search: new URLSearchParams(query).toString() }}>
         {name}
@@ -566,16 +513,15 @@ export class Menu extends React.PureComponent<Props> {
     );
   };
 
-  renderAdminExtensions = (query: Query, isApplication: boolean) => {
-    const extensions = this.getConfiguration().extensions || [];
+  const renderAdminExtensions = (query: Query, isApplication: boolean) => {
+    const extensions = component.configuration?.extensions ?? [];
     return extensions
       .filter((e) => !isApplication || e.key !== 'governance/console')
-      .map((e) => this.renderExtension(e, true, query));
+      .map((e) => renderExtension(e, true, query));
   };
 
-  renderExtensions = () => {
-    const query = this.getQuery();
-    const extensions = this.props.component.extensions ?? [];
+  const renderExtensions = () => {
+    const query = getQuery();
     const withoutSecurityExtension = extensions.filter(
       (extension) =>
         !extension.key.startsWith('securityreport/') && !extension.key.startsWith('governance/')
@@ -591,7 +537,7 @@ export class Menu extends React.PureComponent<Props> {
         id="component-navigation-more"
         size="auto"
         zLevel={PopupZLevel.Global}
-        overlay={withoutSecurityExtension.map((e) => this.renderExtension(e, false, query))}
+        overlay={withoutSecurityExtension.map((e) => renderExtension(e, false, query))}
       >
         {({ onToggleClick, open, a11yAttrs }) => (
           <NavBarTabLink
@@ -608,27 +554,25 @@ export class Menu extends React.PureComponent<Props> {
     );
   };
 
-  render() {
-    return (
-      <div className="sw-flex sw-justify-between sw-pt-4 it__navbar-tabs">
-        <NavBarTabs>
-          {this.renderDashboardLink()}
-          {this.renderBreakdownLink()}
-          {this.renderIssuesLink()}
-          {this.renderSecurityHotspotsLink()}
-          {this.renderSecurityReports()}
-          {this.renderComponentMeasuresLink()}
-          {this.renderCodeLink()}
-          {this.renderActivityLink()}
-          {this.renderExtensions()}
-        </NavBarTabs>
-        <NavBarTabs>
-          {this.renderAdministration()}
-          {this.renderProjectInformationButton()}
-        </NavBarTabs>
-      </div>
-    );
-  }
+  return (
+    <div className="sw-flex sw-justify-between sw-pt-4 it__navbar-tabs">
+      <NavBarTabs>
+        {renderDashboardLink()}
+        {renderBreakdownLink()}
+        {renderIssuesLink()}
+        {renderSecurityHotspotsLink()}
+        {renderSecurityReports()}
+        {renderComponentMeasuresLink()}
+        {renderCodeLink()}
+        {renderActivityLink()}
+        {renderExtensions()}
+      </NavBarTabs>
+      <NavBarTabs>
+        {renderAdministration()}
+        {renderProjectInformationButton()}
+      </NavBarTabs>
+    </div>
+  );
 }
 
 export default withAvailableFeatures(Menu);
index 77b79601c60d6db1207f19af695724194028e6dd..c7adb1be8435542e3d00c12767fc495ddd274184 100644 (file)
  */
 import { screen } from '@testing-library/react';
 import * as React from 'react';
+import BranchesServiceMock from '../../../../../api/mocks/BranchesServiceMock';
 import { mockComponent } from '../../../../../helpers/mocks/component';
 import { mockTask } from '../../../../../helpers/mocks/tasks';
 import { renderApp } from '../../../../../helpers/testReactTestingUtils';
+import { Feature } from '../../../../../types/features';
 import { AnalysisErrorMessage } from '../AnalysisErrorMessage';
 
+const handler = new BranchesServiceMock();
+
+beforeEach(() => {
+  handler.reset();
+});
+
 it('should work when error is on a different branch', () => {
   renderAnalysisErrorMessage({
     currentTask: mockTask({ branch: 'branch-1.2' }),
-    currentTaskOnSameBranch: false,
   });
 
   expect(screen.getByText(/component_navigation.status.failed_branch_X/)).toBeInTheDocument();
   expect(screen.getByText(/branch-1\.2/)).toBeInTheDocument();
 });
 
-it('should work for errors on Pull Requests', () => {
-  renderAnalysisErrorMessage({
-    currentTask: mockTask({ pullRequest: '2342', pullRequestTitle: 'Fix stuff' }),
-    currentTaskOnSameBranch: true,
-  });
+it('should work for errors on Pull Requests', async () => {
+  renderAnalysisErrorMessage(
+    {
+      currentTask: mockTask({ pullRequest: '01', pullRequestTitle: 'Fix stuff' }),
+    },
+    undefined,
+    'pullRequest=01&id=my-project'
+  );
 
-  expect(screen.getByText(/component_navigation.status.failed_X/)).toBeInTheDocument();
-  expect(screen.getByText(/2342 - Fix stuff/)).toBeInTheDocument();
+  expect(await screen.findByText(/component_navigation.status.failed_X/)).toBeInTheDocument();
+  expect(screen.getByText(/01 - Fix stuff/)).toBeInTheDocument();
 });
 
 it('should provide a link to admins', () => {
@@ -67,7 +77,8 @@ it('should explain to admins how to get the staktrace', () => {
 
 function renderAnalysisErrorMessage(
   overrides: Partial<Parameters<typeof AnalysisErrorMessage>[0]> = {},
-  location = '/'
+  location = '/',
+  params?: string
 ) {
   return renderApp(
     location,
@@ -75,8 +86,8 @@ function renderAnalysisErrorMessage(
       component={mockComponent()}
       currentTask={mockTask()}
       onLeave={jest.fn()}
-      currentTaskOnSameBranch
       {...overrides}
-    />
+    />,
+    { navigateTo: params ? `/?${params}` : undefined, featureList: [Feature.BranchSupport] }
   );
 }
index 57bd0ba3d699d09c5fa20f1914d992ff8baadb36..a3b5a039a84b6b3c1bb66c19f6d58584bca1fa03 100644 (file)
@@ -21,19 +21,12 @@ import { screen } from '@testing-library/react';
 import React from 'react';
 import { mockProjectAlmBindingConfigurationErrors } from '../../../../../helpers/mocks/alm-settings';
 import { mockComponent } from '../../../../../helpers/mocks/component';
-import { mockTask, mockTaskWarning } from '../../../../../helpers/mocks/tasks';
+import { mockTask } from '../../../../../helpers/mocks/tasks';
 import { renderApp } from '../../../../../helpers/testReactTestingUtils';
 import { ComponentQualifier } from '../../../../../types/component';
 import { TaskStatuses } from '../../../../../types/tasks';
 import ComponentNav, { ComponentNavProps } from '../ComponentNav';
 
-it('renders correctly when there are warnings', () => {
-  renderComponentNav({ warnings: [mockTaskWarning()] });
-  expect(
-    screen.getByText('project_navigation.analysis_status.warnings', { exact: false })
-  ).toBeInTheDocument();
-});
-
 it('renders correctly when there is a background task in progress', () => {
   renderComponentNav({ isInProgress: true });
   expect(
@@ -74,15 +67,11 @@ function renderComponentNav(props: Partial<ComponentNavProps> = {}) {
   return renderApp(
     '/',
     <ComponentNav
-      branchLikes={[]}
       component={mockComponent({
         breadcrumbs: [{ key: 'foo', name: 'Foo', qualifier: ComponentQualifier.Project }],
       })}
-      currentBranchLike={undefined}
       isInProgress={false}
       isPending={false}
-      onWarningDismiss={jest.fn()}
-      warnings={[]}
       {...props}
     />
   );
index 4dbfe4e9a128de947c99f1fe53b94e925b1ebbe3..4b5157618867001c444799942a3038aef236e8e7 100644 (file)
 import { screen } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import * as React from 'react';
+import BranchesServiceMock from '../../../../../api/mocks/BranchesServiceMock';
 import { mockProjectAlmBindingResponse } from '../../../../../helpers/mocks/alm-settings';
-import {
-  mockMainBranch,
-  mockPullRequest,
-  mockSetOfBranchAndPullRequestForBranchSelector,
-} from '../../../../../helpers/mocks/branch-like';
+import { mockMainBranch, mockPullRequest } from '../../../../../helpers/mocks/branch-like';
 import { mockComponent } from '../../../../../helpers/mocks/component';
 import { mockCurrentUser, mockLoggedInUser } from '../../../../../helpers/testMocks';
 import { renderApp } from '../../../../../helpers/testReactTestingUtils';
 import { AlmKeys } from '../../../../../types/alm-settings';
 import { ComponentQualifier } from '../../../../../types/component';
 import { Feature } from '../../../../../types/features';
-import { BranchStatusContext } from '../../../branch-status/BranchStatusContext';
 import { Header, HeaderProps } from '../Header';
 
 jest.mock('../../../../../api/favorites', () => ({
@@ -40,34 +36,51 @@ jest.mock('../../../../../api/favorites', () => ({
   removeFavorite: jest.fn().mockResolvedValue({}),
 }));
 
-it('should render correctly when there is only 1 branch', () => {
-  renderHeader({ branchLikes: [mockMainBranch()] });
+const handler = new BranchesServiceMock();
+
+beforeEach(() => handler.reset());
+
+it('should render correctly when there is only 1 branch', async () => {
+  handler.emptyBranchesAndPullRequest();
+  handler.addBranch(mockMainBranch({ status: { qualityGateStatus: 'OK' } }));
+  renderHeader();
+  expect(await screen.findByLabelText('help-tooltip')).toBeInTheDocument();
   expect(screen.getByText('project')).toBeInTheDocument();
-  expect(screen.getByLabelText('help-tooltip')).toBeInTheDocument();
   expect(
-    screen.getByRole('button', { name: 'branch-1 overview.quality_gate_x.OK' })
+    await screen.findByRole('button', { name: 'master overview.quality_gate_x.OK' })
   ).toBeDisabled();
 });
 
 it('should render correctly when there are multiple branch', async () => {
   const user = userEvent.setup();
   renderHeader();
-  expect(screen.getByRole('button', { name: 'branch-1 overview.quality_gate_x.OK' })).toBeEnabled();
+
+  expect(
+    await screen.findByRole('button', { name: 'main overview.quality_gate_x.OK' })
+  ).toBeEnabled();
+
   expect(screen.queryByLabelText('help-tooltip')).not.toBeInTheDocument();
 
-  await user.click(screen.getByRole('button', { name: 'branch-1 overview.quality_gate_x.OK' }));
+  await user.click(screen.getByRole('button', { name: 'main overview.quality_gate_x.OK' }));
   expect(screen.getByText('branches.main_branch')).toBeInTheDocument();
   expect(
-    screen.getByRole('menuitem', { name: 'branch-2 overview.quality_gate_x.ERROR ERROR' })
+    screen.getByRole('menuitem', {
+      name: '03 â€“ TEST-193 dumb commit overview.quality_gate_x.ERROR ERROR',
+    })
+  ).toBeInTheDocument();
+  expect(
+    screen.getByRole('menuitem', {
+      name: '01 â€“ TEST-191 update master overview.quality_gate_x.OK OK',
+    })
+  ).toBeInTheDocument();
+  expect(
+    screen.getByRole('menuitem', { name: 'normal-branch overview.quality_gate_x.ERROR ERROR' })
   ).toBeInTheDocument();
-  expect(screen.getByRole('menuitem', { name: 'branch-3' })).toBeInTheDocument();
-  expect(screen.getByRole('menuitem', { name: '1 â€“ PR-1' })).toBeInTheDocument();
-  expect(screen.getByRole('menuitem', { name: '2 â€“ PR-2' })).toBeInTheDocument();
 
   await user.click(
-    screen.getByRole('menuitem', { name: 'branch-2 overview.quality_gate_x.ERROR ERROR' })
+    screen.getByRole('menuitem', { name: 'normal-branch overview.quality_gate_x.ERROR ERROR' })
   );
-  expect(screen.getByText('/dashboard?branch=branch-2&id=my-project')).toBeInTheDocument();
+  expect(screen.getByText('/dashboard?branch=normal-branch&id=header-project')).toBeInTheDocument();
 });
 
 it('should show manage branch and pull request button for admin', async () => {
@@ -75,16 +88,17 @@ it('should show manage branch and pull request button for admin', async () => {
   renderHeader({
     currentUser: mockLoggedInUser(),
     component: mockComponent({
+      key: 'header-project',
       configuration: { showSettings: true },
       breadcrumbs: [{ name: 'project', key: 'project', qualifier: ComponentQualifier.Project }],
     }),
   });
-  await user.click(screen.getByRole('button', { name: 'branch-1 overview.quality_gate_x.OK' }));
+  await user.click(await screen.findByRole('button', { name: 'main overview.quality_gate_x.OK' }));
 
   expect(screen.getByRole('link', { name: 'branch_like_navigation.manage' })).toBeInTheDocument();
   expect(screen.getByRole('link', { name: 'branch_like_navigation.manage' })).toHaveAttribute(
     'href',
-    '/project/branches?id=my-project'
+    '/project/branches?id=header-project'
   );
 });
 
@@ -104,45 +118,41 @@ it('should render favorite button if the user is logged in', async () => {
 
 it.each([['github'], ['gitlab'], ['bitbucket'], ['azure']])(
   'should show correct %s links for a PR',
-  (alm: string) => {
-    renderHeader({
-      currentUser: mockLoggedInUser(),
-      currentBranchLike: mockPullRequest({
-        key: '1',
-        title: 'PR-1',
-        status: { qualityGateStatus: 'OK' },
-        url: alm,
-      }),
-      branchLikes: [
-        mockPullRequest({
-          key: '1',
-          title: 'PR-1',
-          status: { qualityGateStatus: 'OK' },
-          url: alm,
-        }),
-      ],
-    });
-    const image = screen.getByAltText(alm);
+  async (alm: string) => {
+    handler.emptyBranchesAndPullRequest();
+    handler.addPullRequest(mockPullRequest({ url: alm }));
+    renderHeader(
+      {
+        currentUser: mockLoggedInUser(),
+      },
+      undefined,
+      'pullRequest=1001&id=compa'
+    );
+    const image = await screen.findByAltText(alm);
     expect(image).toBeInTheDocument();
     expect(image).toHaveAttribute('src', `/images/alm/${alm}.svg`);
   }
 );
 
-it('should show the correct help tooltip for applications', () => {
+it('should show the correct help tooltip for applications', async () => {
+  handler.emptyBranchesAndPullRequest();
+  handler.addBranch(mockMainBranch());
   renderHeader({
     currentUser: mockLoggedInUser(),
     component: mockComponent({
+      key: 'header-project',
       configuration: { showSettings: true },
       breadcrumbs: [{ name: 'project', key: 'project', qualifier: ComponentQualifier.Application }],
       qualifier: 'APP',
     }),
-    branchLikes: [mockMainBranch()],
   });
-  expect(screen.getByText('application.branches.help')).toBeInTheDocument();
+  expect(await screen.findByText('application.branches.help')).toBeInTheDocument();
   expect(screen.getByText('application.branches.link')).toBeInTheDocument();
 });
 
-it('should show the correct help tooltip when branch support is not enabled', () => {
+it('should show the correct help tooltip when branch support is not enabled', async () => {
+  handler.emptyBranchesAndPullRequest();
+  handler.addBranch(mockMainBranch());
   renderHeader(
     {
       currentUser: mockLoggedInUser(),
@@ -154,47 +164,29 @@ it('should show the correct help tooltip when branch support is not enabled', ()
     },
     []
   );
-  expect(screen.getByText('branch_like_navigation.no_branch_support.title.mr')).toBeInTheDocument();
+  expect(
+    await screen.findByText('branch_like_navigation.no_branch_support.title.mr')
+  ).toBeInTheDocument();
   expect(
     screen.getByText('branch_like_navigation.no_branch_support.content_x.mr.alm.gitlab')
   ).toBeInTheDocument();
 });
 
-function renderHeader(props?: Partial<HeaderProps>, featureList = [Feature.BranchSupport]) {
-  const branchLikes = mockSetOfBranchAndPullRequestForBranchSelector();
-
+function renderHeader(
+  props?: Partial<HeaderProps>,
+  featureList = [Feature.BranchSupport],
+  params?: string
+) {
   return renderApp(
     '/',
-    <BranchStatusContext.Provider
-      value={{
-        branchStatusByComponent: {
-          'my-project': {
-            'branch-branch-1': {
-              status: 'OK',
-            },
-            'branch-branch-2': {
-              status: 'ERROR',
-            },
-          },
-        },
-        fetchBranchStatus: () => {
-          /*noop*/
-        },
-        updateBranchStatus: () => {
-          /*noop*/
-        },
-      }}
-    >
-      <Header
-        branchLikes={branchLikes}
-        component={mockComponent({
-          breadcrumbs: [{ name: 'project', key: 'project', qualifier: ComponentQualifier.Project }],
-        })}
-        currentBranchLike={branchLikes[0]}
-        currentUser={mockCurrentUser()}
-        {...props}
-      />
-    </BranchStatusContext.Provider>,
-    { featureList }
+    <Header
+      component={mockComponent({
+        key: 'header-project',
+        breadcrumbs: [{ name: 'project', key: 'project', qualifier: ComponentQualifier.Project }],
+      })}
+      currentUser={mockCurrentUser()}
+      {...props}
+    />,
+    { featureList, navigateTo: params ? `/?id=header-project&${params}` : '/?id=header-project' }
   );
 }
index cb92dda3fa194a193dbb122cbdda89a7354fddfa..3530de943b627f81abadc49ec103cb442ed16069 100644 (file)
 import { screen } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import * as React from 'react';
-import { mockBranch, mockPullRequest } from '../../../../../helpers/mocks/branch-like';
+import { getAnalysisStatus } from '../../../../../api/ce';
+import BranchesServiceMock from '../../../../../api/mocks/BranchesServiceMock';
 import { mockComponent } from '../../../../../helpers/mocks/component';
-import { mockTask, mockTaskWarning } from '../../../../../helpers/mocks/tasks';
+import { mockTask } from '../../../../../helpers/mocks/tasks';
 import { mockCurrentUser, mockLoggedInUser } from '../../../../../helpers/testMocks';
 import { renderApp } from '../../../../../helpers/testReactTestingUtils';
+import { Feature } from '../../../../../types/features';
 import { TaskStatuses } from '../../../../../types/tasks';
 import { CurrentUser } from '../../../../../types/users';
 import HeaderMeta, { HeaderMetaProps } from '../HeaderMeta';
 
+jest.mock('../../../../../api/ce');
+
+const handler = new BranchesServiceMock();
+
+beforeEach(() => handler.reset());
+
 it('should render correctly for a branch with warnings', async () => {
   const user = userEvent.setup();
+  jest.mocked(getAnalysisStatus).mockResolvedValue({
+    component: {
+      warnings: [{ dismissable: false, key: 'key', message: 'bar' }],
+      key: 'compkey',
+      name: 'me',
+    },
+  });
+  renderHeaderMeta({}, undefined, 'branch=normal-branch&id=my-project');
 
-  renderHeaderMeta();
-
-  expect(screen.getByText('version_x.0.0.1')).toBeInTheDocument();
+  expect(await screen.findByText('version_x.0.0.1')).toBeInTheDocument();
 
-  expect(screen.getByText('project_navigation.analysis_status.warnings')).toBeInTheDocument();
+  expect(
+    await screen.findByText('project_navigation.analysis_status.warnings')
+  ).toBeInTheDocument();
 
   await user.click(screen.getByText('project_navigation.analysis_status.details_link'));
 
@@ -44,7 +60,14 @@ it('should render correctly for a branch with warnings', async () => {
 });
 
 it('should handle a branch with missing version and no warnings', () => {
-  renderHeaderMeta({ component: mockComponent({ version: undefined }), warnings: [] });
+  jest.mocked(getAnalysisStatus).mockResolvedValue({
+    component: {
+      warnings: [],
+      key: 'compkey',
+      name: 'me',
+    },
+  });
+  renderHeaderMeta({ component: mockComponent({ version: undefined }) });
 
   expect(screen.queryByText('version_x.0.0.1')).not.toBeInTheDocument();
   expect(screen.queryByText('project_navigation.analysis_status.warnings')).not.toBeInTheDocument();
@@ -60,22 +83,20 @@ it('should render correctly with a failed analysis', async () => {
     }),
   });
 
-  expect(screen.getByText('project_navigation.analysis_status.failed')).toBeInTheDocument();
+  expect(await screen.findByText('project_navigation.analysis_status.failed')).toBeInTheDocument();
 
   await user.click(screen.getByText('project_navigation.analysis_status.details_link'));
 
   expect(screen.getByRole('heading', { name: 'error' })).toBeInTheDocument();
 });
 
-it('should render correctly for a pull request', () => {
-  renderHeaderMeta({
-    branchLike: mockPullRequest({
-      url: 'https://example.com/pull/1234',
-    }),
-  });
+it('should render correctly for a pull request', async () => {
+  renderHeaderMeta({}, undefined, 'pullRequest=01&id=my-project');
 
+  expect(
+    await screen.findByText('branch_like_navigation.for_merge_into_x_from_y')
+  ).toBeInTheDocument();
   expect(screen.queryByText('version_x.0.0.1')).not.toBeInTheDocument();
-  expect(screen.getByText('branch_like_navigation.for_merge_into_x_from_y')).toBeInTheDocument();
 });
 
 it('should render correctly when the user is not logged in', () => {
@@ -87,20 +108,12 @@ it('should render correctly when the user is not logged in', () => {
 
 function renderHeaderMeta(
   props: Partial<HeaderMetaProps> = {},
-  currentUser: CurrentUser = mockLoggedInUser()
+  currentUser: CurrentUser = mockLoggedInUser(),
+  params?: string
 ) {
-  return renderApp(
-    '/',
-    <HeaderMeta
-      branchLike={mockBranch()}
-      component={mockComponent({ version: '0.0.1' })}
-      onWarningDismiss={jest.fn()}
-      warnings={[
-        mockTaskWarning({ key: '1', message: 'ERROR_1' }),
-        mockTaskWarning({ key: '2', message: 'ERROR_2' }),
-      ]}
-      {...props}
-    />,
-    { currentUser }
-  );
+  return renderApp('/', <HeaderMeta component={mockComponent({ version: '0.0.1' })} {...props} />, {
+    currentUser,
+    navigateTo: params ? `/?id=my-project&${params}` : '/?id=my-project',
+    featureList: [Feature.BranchSupport],
+  });
 }
index 599299e7e7bb6a13b2f1efa2403b4926b28c237e..2add79f18e5024ac71189534e24d617d8a716eba 100644 (file)
 import { screen } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import * as React from 'react';
-import {
-  mockBranch,
-  mockMainBranch,
-  mockPullRequest,
-} from '../../../../../helpers/mocks/branch-like';
+import BranchesServiceMock from '../../../../../api/mocks/BranchesServiceMock';
 import { mockComponent } from '../../../../../helpers/mocks/component';
 import { renderComponent } from '../../../../../helpers/testReactTestingUtils';
+import { ComponentPropsType } from '../../../../../helpers/testUtils';
 import { ComponentQualifier } from '../../../../../types/component';
+import { Feature } from '../../../../../types/features';
 import { Menu } from '../Menu';
 
+const handler = new BranchesServiceMock();
+
 const BASE_COMPONENT = mockComponent({
   analysisDate: '2019-12-01',
   key: 'foo',
   name: 'foo',
 });
 
+beforeEach(() => handler.reset());
+
 it('should render correctly', async () => {
   const user = userEvent.setup();
   const component = {
@@ -90,33 +92,37 @@ it('should render correctly when on a Portofolio', () => {
   expect(screen.getByRole('link', { name: 'portfolio_breakdown.page' })).toBeInTheDocument();
 });
 
-it('should render correctly when on a branch', () => {
-  renderMenu({
-    branchLike: mockBranch(),
-    component: {
-      ...BASE_COMPONENT,
-      configuration: { showSettings: true },
-      extensions: [{ key: 'component-foo', name: 'ComponentFoo' }],
+it('should render correctly when on a branch', async () => {
+  renderMenu(
+    {
+      component: {
+        ...BASE_COMPONENT,
+        configuration: { showSettings: true },
+        extensions: [{ key: 'component-foo', name: 'ComponentFoo' }],
+      },
     },
-  });
+    'branch=normal-branch'
+  );
 
-  expect(screen.getByRole('link', { name: 'overview.page' })).toBeInTheDocument();
+  expect(await screen.findByRole('link', { name: 'overview.page' })).toBeInTheDocument();
   expect(screen.getByRole('link', { name: 'issues.page' })).toBeInTheDocument();
   expect(screen.getByRole('link', { name: 'layout.measures' })).toBeInTheDocument();
   expect(screen.getByRole('link', { name: 'project.info.title' })).toBeInTheDocument();
 });
 
-it('should render correctly when on a pull request', () => {
-  renderMenu({
-    branchLike: mockPullRequest(),
-    component: {
-      ...BASE_COMPONENT,
-      configuration: { showSettings: true },
-      extensions: [{ key: 'component-foo', name: 'ComponentFoo' }],
+it('should render correctly when on a pull request', async () => {
+  renderMenu(
+    {
+      component: {
+        ...BASE_COMPONENT,
+        configuration: { showSettings: true },
+        extensions: [{ key: 'component-foo', name: 'ComponentFoo' }],
+      },
     },
-  });
+    'pullRequest=01'
+  );
 
-  expect(screen.getByRole('link', { name: 'overview.page' })).toBeInTheDocument();
+  expect(await screen.findByRole('link', { name: 'overview.page' })).toBeInTheDocument();
   expect(screen.getByRole('link', { name: 'issues.page' })).toBeInTheDocument();
   expect(screen.getByRole('link', { name: 'layout.measures' })).toBeInTheDocument();
 
@@ -153,19 +159,16 @@ it('should disable links if application has inaccessible projects', () => {
   expect(screen.queryByRole('button', { name: 'application.info.title' })).not.toBeInTheDocument();
 });
 
-function renderMenu(props: Partial<Menu['props']> = {}) {
-  const mainBranch = mockMainBranch();
+function renderMenu(props: Partial<ComponentPropsType<typeof Menu>> = {}, params?: string) {
   return renderComponent(
     <Menu
       hasFeature={jest.fn().mockReturnValue(false)}
-      branchLike={mainBranch}
-      branchLikes={[mainBranch]}
       component={BASE_COMPONENT}
       isInProgress={false}
       isPending={false}
-      onToggleProjectInfo={jest.fn()}
-      projectInfoDisplayed={false}
       {...props}
-    />
+    />,
+    params ? `/?${params}` : '/',
+    { featureList: [Feature.BranchSupport] }
   );
 }
index 4c6d2c54525192331a162f7579d735a2d20ec3ab..3b20d3b6f891f8df8cfb73f0d22ba32ad8f7850d 100644 (file)
@@ -22,8 +22,8 @@ import * as React from 'react';
 import EscKeydownHandler from '../../../../../components/controls/EscKeydownHandler';
 import FocusOutHandler from '../../../../../components/controls/FocusOutHandler';
 import OutsideClickHandler from '../../../../../components/controls/OutsideClickHandler';
+import { useBranchesQuery } from '../../../../../queries/branch';
 import { AlmKeys, ProjectAlmBindingResponse } from '../../../../../types/alm-settings';
-import { BranchLike } from '../../../../../types/branch-like';
 import { ComponentQualifier } from '../../../../../types/component';
 import { Feature } from '../../../../../types/features';
 import { Component } from '../../../../../types/types';
@@ -36,91 +36,95 @@ import Menu from './Menu';
 import PRLink from './PRLink';
 
 export interface BranchLikeNavigationProps extends WithAvailableFeaturesProps {
-  branchLikes: BranchLike[];
   component: Component;
-  currentBranchLike: BranchLike;
   projectBinding?: ProjectAlmBindingResponse;
 }
 
 export function BranchLikeNavigation(props: BranchLikeNavigationProps) {
   const {
-    branchLikes,
     component,
     component: { configuration },
-    currentBranchLike,
     projectBinding,
   } = props;
 
+  const { data: { branchLikes, branchLike: currentBranchLike } = { branchLikes: [] } } =
+    useBranchesQuery(component);
+  const [isMenuOpen, setIsMenuOpen] = React.useState(false);
+
+  if (currentBranchLike === undefined) {
+    return null;
+  }
+
   const isApplication = component.qualifier === ComponentQualifier.Application;
   const isGitLab = projectBinding !== undefined && projectBinding.alm === AlmKeys.GitLab;
 
-  const [isMenuOpen, setIsMenuOpen] = React.useState(false);
   const branchSupportEnabled = props.hasFeature(Feature.BranchSupport);
   const canAdminComponent = configuration?.showSettings;
   const hasManyBranches = branchLikes.length >= 2;
   const isMenuEnabled = branchSupportEnabled && hasManyBranches;
 
-  const currentBranchLikeElement = (
-    <CurrentBranchLike component={component} currentBranchLike={currentBranchLike} />
-  );
+  const currentBranchLikeElement = <CurrentBranchLike currentBranchLike={currentBranchLike} />;
 
   const handleOutsideClick = () => {
     setIsMenuOpen(false);
   };
 
   return (
-    <div className="sw-flex sw-items-center it__branch-like-navigation-toggler-container">
-      <Popup
-        allowResizing
-        overlay={
-          isMenuOpen && (
-            <FocusOutHandler onFocusOut={handleOutsideClick}>
-              <EscKeydownHandler onKeydown={handleOutsideClick}>
-                <OutsideClickHandler onClickOutside={handleOutsideClick}>
-                  <Menu
-                    branchLikes={branchLikes}
-                    canAdminComponent={canAdminComponent}
-                    component={component}
-                    currentBranchLike={currentBranchLike}
-                    onClose={() => {
-                      setIsMenuOpen(false);
-                    }}
-                  />
-                </OutsideClickHandler>
-              </EscKeydownHandler>
-            </FocusOutHandler>
-          )
-        }
-        placement={PopupPlacement.BottomLeft}
-        zLevel={PopupZLevel.Global}
-      >
-        <ButtonSecondary
-          className="sw-max-w-abs-350 sw-px-3"
-          onClick={() => {
-            setIsMenuOpen(!isMenuOpen);
-          }}
-          disabled={!isMenuEnabled}
-          aria-expanded={isMenuOpen}
-          aria-haspopup="menu"
+    <>
+      <span className="slash-separator sw-mx-2" />
+      <div className="sw-flex sw-items-center it__branch-like-navigation-toggler-container">
+        <Popup
+          allowResizing
+          overlay={
+            isMenuOpen && (
+              <FocusOutHandler onFocusOut={handleOutsideClick}>
+                <EscKeydownHandler onKeydown={handleOutsideClick}>
+                  <OutsideClickHandler onClickOutside={handleOutsideClick}>
+                    <Menu
+                      branchLikes={branchLikes}
+                      canAdminComponent={canAdminComponent}
+                      component={component}
+                      currentBranchLike={currentBranchLike}
+                      onClose={() => {
+                        setIsMenuOpen(false);
+                      }}
+                    />
+                  </OutsideClickHandler>
+                </EscKeydownHandler>
+              </FocusOutHandler>
+            )
+          }
+          placement={PopupPlacement.BottomLeft}
+          zLevel={PopupZLevel.Global}
         >
-          {currentBranchLikeElement}
-        </ButtonSecondary>
-      </Popup>
+          <ButtonSecondary
+            className="sw-max-w-abs-350 sw-px-3"
+            onClick={() => {
+              setIsMenuOpen(!isMenuOpen);
+            }}
+            disabled={!isMenuEnabled}
+            aria-expanded={isMenuOpen}
+            aria-haspopup="menu"
+          >
+            {currentBranchLikeElement}
+          </ButtonSecondary>
+        </Popup>
 
-      <div className="sw-ml-2">
-        <BranchHelpTooltip
-          component={component}
-          isApplication={isApplication}
-          projectBinding={projectBinding}
-          hasManyBranches={hasManyBranches}
-          canAdminComponent={canAdminComponent}
-          branchSupportEnabled={branchSupportEnabled}
-          isGitLab={isGitLab}
-        />
-      </div>
+        <div className="sw-ml-2">
+          <BranchHelpTooltip
+            component={component}
+            isApplication={isApplication}
+            projectBinding={projectBinding}
+            hasManyBranches={hasManyBranches}
+            canAdminComponent={canAdminComponent}
+            branchSupportEnabled={branchSupportEnabled}
+            isGitLab={isGitLab}
+          />
+        </div>
 
-      <PRLink currentBranchLike={currentBranchLike} component={component} />
-    </div>
+        <PRLink currentBranchLike={currentBranchLike} component={component} />
+      </div>
+    </>
   );
 }
 
index 2de026da87e5e84d047aafe1b54effb9871881ab..e3bc2c89a414096c0534dac51180aeaf7aa7f4c6 100644 (file)
@@ -22,16 +22,14 @@ import * as React from 'react';
 import BranchLikeIcon from '../../../../../components/icons/BranchLikeIcon';
 import { getBranchLikeDisplayName } from '../../../../../helpers/branch-like';
 import { BranchLike, BranchStatusData } from '../../../../../types/branch-like';
-import { Component } from '../../../../../types/types';
 import QualityGateStatus from './QualityGateStatus';
 
 export interface CurrentBranchLikeProps extends Pick<BranchStatusData, 'status'> {
-  component: Component;
   currentBranchLike: BranchLike;
 }
 
 export function CurrentBranchLike(props: CurrentBranchLikeProps) {
-  const { component, currentBranchLike } = props;
+  const { currentBranchLike } = props;
 
   const displayName = getBranchLikeDisplayName(currentBranchLike);
 
@@ -39,7 +37,7 @@ export function CurrentBranchLike(props: CurrentBranchLikeProps) {
     <div className="sw-flex sw-items-center text-ellipsis">
       <BranchLikeIcon branchLike={currentBranchLike} />
       <TextMuted text={displayName} className="sw-ml-3" />
-      <QualityGateStatus branchLike={currentBranchLike} component={component} className="sw-ml-4" />
+      <QualityGateStatus branchLike={currentBranchLike} className="sw-ml-4" />
       <ChevronDownIcon className="sw-ml-1" />
     </div>
   );
index ea1bfbc48f6edcabd80ba3fe4f116389a5c00b61..777f2d999fd8648cdff79f6a3e22e366be2c3f0a 100644 (file)
@@ -176,7 +176,6 @@ export class Menu extends React.PureComponent<Props, State> {
         />
         <MenuItemList
           branchLikeTree={branchLikesToDisplayTree}
-          component={component}
           hasResults={hasResults}
           onSelect={this.handleOnSelect}
           selectedBranchLike={selectedBranchLike}
index cc9b8644d527401c3928b7c6c3c2f330446d5ac1..dff3e81a1cd42f7f7464d293c1929e58667df92a 100644 (file)
@@ -24,12 +24,10 @@ import BranchLikeIcon from '../../../../../components/icons/BranchLikeIcon';
 import { getBranchLikeDisplayName, isMainBranch } from '../../../../../helpers/branch-like';
 import { translate } from '../../../../../helpers/l10n';
 import { BranchLike } from '../../../../../types/branch-like';
-import { Component } from '../../../../../types/types';
 import QualityGateStatus from './QualityGateStatus';
 
 export interface MenuItemProps {
   branchLike: BranchLike;
-  component: Component;
   onSelect: (branchLike: BranchLike) => void;
   selected: boolean;
   indent: boolean;
@@ -37,7 +35,7 @@ export interface MenuItemProps {
 }
 
 export function MenuItem(props: MenuItemProps) {
-  const { branchLike, component, setSelectedNode, onSelect, selected, indent } = props;
+  const { branchLike, setSelectedNode, onSelect, selected, indent } = props;
   const displayName = getBranchLikeDisplayName(branchLike);
 
   return (
@@ -64,7 +62,6 @@ export function MenuItem(props: MenuItemProps) {
         </div>
         <QualityGateStatus
           branchLike={branchLike}
-          component={component}
           className="sw-flex sw-items-center sw-w-24"
           showStatusText
         />
index 93ea2516d1f729bcd3afbe0ab5c84bc073ad6b3e..c91f6b744b7a77d7ed4bf89a1318f2b1ddfde7fc 100644 (file)
@@ -24,12 +24,10 @@ import { getBranchLikeKey, isSameBranchLike } from '../../../../../helpers/branc
 import { translate } from '../../../../../helpers/l10n';
 import { isDefined } from '../../../../../helpers/types';
 import { BranchLike, BranchLikeTree } from '../../../../../types/branch-like';
-import { Component } from '../../../../../types/types';
 import MenuItem from './MenuItem';
 
 export interface MenuItemListProps {
   branchLikeTree: BranchLikeTree;
-  component: Component;
   hasResults: boolean;
   onSelect: (branchLike: BranchLike) => void;
   selectedBranchLike: BranchLike | undefined;
@@ -45,12 +43,11 @@ export function MenuItemList(props: MenuItemListProps) {
     }
   });
 
-  const { branchLikeTree, component, hasResults, onSelect, selectedBranchLike } = props;
+  const { branchLikeTree, hasResults, onSelect, selectedBranchLike } = props;
 
   const renderItem = (branchLike: BranchLike, indent = false) => (
     <MenuItem
       branchLike={branchLike}
-      component={component}
       key={getBranchLikeKey(branchLike)}
       onSelect={onSelect}
       selected={isSameBranchLike(branchLike, selectedBranchLike)}
index 861f7c84ea3ce3917c6b1176ad195d25df567964..8ba0149febfbe319081dfe717d891654663aa611 100644 (file)
  */
 import classNames from 'classnames';
 import { QualityGateIndicator } from 'design-system';
-import React, { useContext } from 'react';
-import { getBranchStatusByBranchLike } from '../../../../../helpers/branch-like';
+import React from 'react';
 import { translateWithParameters } from '../../../../../helpers/l10n';
 import { formatMeasure } from '../../../../../helpers/measures';
 import { BranchLike } from '../../../../../types/branch-like';
 import { MetricType } from '../../../../../types/metrics';
-import { Component } from '../../../../../types/types';
-import { BranchStatusContext } from '../../../branch-status/BranchStatusContext';
 
 interface Props {
-  component: Component;
   branchLike: BranchLike;
   className: string;
   showStatusText?: boolean;
 }
 
-export default function QualityGateStatus({
-  component,
-  branchLike,
-  className,
-  showStatusText,
-}: Props) {
-  const { branchStatusByComponent } = useContext(BranchStatusContext);
-  const branchStatus = getBranchStatusByBranchLike(
-    branchStatusByComponent,
-    component.key,
-    branchLike
-  );
-
+export default function QualityGateStatus({ className, showStatusText, branchLike }: Props) {
   // eslint-disable-next-line @typescript-eslint/prefer-optional-chain, @typescript-eslint/no-unnecessary-condition
-  if (!branchStatus || !branchStatus.status) {
+  if (!branchLike.status?.qualityGateStatus) {
     return null;
   }
-  const { status } = branchStatus;
-  const formatted = formatMeasure(status, MetricType.Level);
+
+  const formatted = formatMeasure(branchLike.status?.qualityGateStatus, MetricType.Level);
   const ariaLabel = translateWithParameters('overview.quality_gate_x', formatted);
   return (
-    <div className={classNames(`it__level-${status}`, className)}>
-      <QualityGateIndicator status={status} className="sw-mr-2" ariaLabel={ariaLabel} size="sm" />
+    <div className={classNames(`it__level-${branchLike.status.qualityGateStatus}`, className)}>
+      <QualityGateIndicator
+        status={branchLike.status?.qualityGateStatus}
+        className="sw-mr-2"
+        ariaLabel={ariaLabel}
+        size="sm"
+      />
       {showStatusText && <span>{formatted}</span>}
     </div>
   );
index 003dd7c052525c07373e8493b708af85bfc01553..f71537e379b3cb6f8439e835bb18c5a3b2cfb13a 100644 (file)
@@ -27,9 +27,7 @@ import { CurrentUserContextInterface } from '../current-user/CurrentUserContext'
 import withCurrentUserContext from '../current-user/withCurrentUserContext';
 import './PromotionNotification.css';
 
-export interface PromotionNotificationProps extends CurrentUserContextInterface {}
-
-export function PromotionNotification(props: PromotionNotificationProps) {
+export function PromotionNotification(props: CurrentUserContextInterface) {
   const { currentUser } = props;
 
   if (!isLoggedIn(currentUser) || currentUser.dismissedNotices[NoticeType.SONARLINT_AD]) {
index cd11a4f17d2a1b8a7b6362fadacd280e0154cd4b..4f30edf3a9c61620db748b3b22a98703b185ce91 100644 (file)
@@ -23,7 +23,8 @@ import { dismissNotice } from '../../../../api/users';
 import { mockCurrentUser, mockLoggedInUser } from '../../../../helpers/testMocks';
 import { waitAndUpdate } from '../../../../helpers/testUtils';
 import { NoticeType } from '../../../../types/users';
-import { PromotionNotification, PromotionNotificationProps } from '../PromotionNotification';
+import { CurrentUserContextInterface } from '../../current-user/CurrentUserContext';
+import { PromotionNotification } from '../PromotionNotification';
 
 jest.mock('../../../../api/users', () => ({
   dismissNotice: jest.fn().mockResolvedValue({}),
@@ -67,7 +68,7 @@ it('should remove the toaster and navigate to sonarlint when click on learn more
   expect(updateDismissedNotices).toHaveBeenCalled();
 });
 
-function shallowRender(props: Partial<PromotionNotificationProps> = {}) {
+function shallowRender(props: Partial<CurrentUserContextInterface> = {}) {
   return shallow(
     <PromotionNotification
       currentUser={mockCurrentUser()}
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/AnalysisWarningsModal.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/AnalysisWarningsModal.tsx
new file mode 100644 (file)
index 0000000..c585b9a
--- /dev/null
@@ -0,0 +1,167 @@
+/*
+ * 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 {
+  DangerButtonSecondary,
+  DeferredSpinner,
+  FlagMessage,
+  HtmlFormatter,
+  Modal,
+} from 'design-system';
+import * as React from 'react';
+import { dismissAnalysisWarning, getTask } from '../../../api/ce';
+import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
+import { translate } from '../../../helpers/l10n';
+import { sanitizeStringRestricted } from '../../../helpers/sanitize';
+import { TaskWarning } from '../../../types/tasks';
+import { CurrentUser } from '../../../types/users';
+
+interface Props {
+  componentKey?: string;
+  currentUser: CurrentUser;
+  onClose: () => void;
+  taskId: string;
+}
+
+interface State {
+  loading: boolean;
+  dismissedWarning?: string;
+  warnings: TaskWarning[];
+}
+
+export class AnalysisWarningsModal extends React.PureComponent<Props, State> {
+  mounted = false;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      loading: false,
+      warnings: [],
+    };
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+    this.loadWarnings(this.props.taskId);
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    const { taskId } = this.props;
+    if (prevProps.taskId !== taskId) {
+      this.loadWarnings(taskId);
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  handleDismissMessage = async (messageKey: string) => {
+    const { componentKey } = this.props;
+
+    if (componentKey === undefined) {
+      return;
+    }
+
+    this.setState({ dismissedWarning: messageKey });
+    try {
+      await dismissAnalysisWarning(componentKey, messageKey);
+    } catch (e) {
+      // Noop
+    }
+
+    if (this.mounted) {
+      this.setState({ dismissedWarning: undefined });
+    }
+  };
+
+  loadWarnings = async (taskId: string) => {
+    this.setState({ loading: true });
+    try {
+      const { warnings = [] } = await getTask(taskId, ['warnings']);
+
+      if (this.mounted) {
+        this.setState({
+          loading: false,
+          warnings: warnings.map((w) => ({ key: w, message: w, dismissable: false })),
+        });
+      }
+    } catch (e) {
+      if (this.mounted) {
+        this.setState({ loading: false });
+      }
+    }
+  };
+
+  render() {
+    const { currentUser } = this.props;
+    const { loading, dismissedWarning, warnings } = this.state;
+
+    const header = translate('warnings');
+
+    const body = (
+      <DeferredSpinner loading={loading}>
+        {warnings.map(({ dismissable, key, message }) => (
+          <React.Fragment key={key}>
+            <div className="sw-flex sw-items-center sw-mt-2">
+              <FlagMessage variant="warning">
+                <HtmlFormatter>
+                  <span
+                    // eslint-disable-next-line react/no-danger
+                    dangerouslySetInnerHTML={{
+                      __html: sanitizeStringRestricted(message.trim().replace(/\n/g, '<br />')),
+                    }}
+                  />
+                </HtmlFormatter>
+              </FlagMessage>
+            </div>
+            <div>
+              {dismissable && currentUser.isLoggedIn && (
+                <div className="sw-mt-4">
+                  <DangerButtonSecondary
+                    disabled={Boolean(dismissedWarning)}
+                    onClick={() => {
+                      this.handleDismissMessage(key);
+                    }}
+                  >
+                    {translate('dismiss_permanently')}
+                  </DangerButtonSecondary>
+
+                  <DeferredSpinner className="sw-ml-2" loading={dismissedWarning === key} />
+                </div>
+              )}
+            </div>
+          </React.Fragment>
+        ))}
+      </DeferredSpinner>
+    );
+
+    return (
+      <Modal
+        headerTitle={header}
+        onClose={this.props.onClose}
+        body={body}
+        primaryButton={null}
+        secondaryButtonLabel={translate('close')}
+      />
+    );
+  }
+}
+
+export default withCurrentUserContext(AnalysisWarningsModal);
index 4234161e1b67fb98baf8985b939f582f3ef12c35..e3533e0b1975ec9336c8787be300875b12506bfc 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import AnalysisWarningsModal from '../../../components/common/AnalysisWarningsModal';
 import ActionsDropdown, { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown';
 import ConfirmModal from '../../../components/controls/ConfirmModal';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { Task, TaskStatuses } from '../../../types/tasks';
+import AnalysisWarningsModal from './AnalysisWarningsModal';
 import ScannerContext from './ScannerContext';
 import Stacktrace from './Stacktrace';
 
index af5d7b5255fdc8d11e6eee6b4ec5a3436bcc29ac..25845bd1ccb95d487c89ede6a9196f062370f856 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 { debounce, noop } from 'lodash';
 import * as React from 'react';
-import withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions';
 import withComponentContext from '../../../app/components/componentContext/withComponentContext';
 import withMetricsContext from '../../../app/components/metrics/withMetricsContext';
 import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
-import { isPullRequest } from '../../../helpers/branch-like';
 import { CodeScope, getCodeUrl, getProjectUrl } from '../../../helpers/urls';
+import { withBranchLikes } from '../../../queries/branch';
 import { BranchLike } from '../../../types/branch-like';
 import { ComponentQualifier } from '../../../types/component';
-import { Breadcrumb, Component, ComponentMeasure, Dict, Issue, Metric } from '../../../types/types';
+import { Breadcrumb, Component, ComponentMeasure, Dict, Metric } from '../../../types/types';
 import { addComponent, addComponentBreadcrumbs, clearBucket } from '../bucket';
 import '../code.css';
 import { loadMoreChildren, retrieveComponent, retrieveComponentChildren } from '../utils';
@@ -35,8 +33,8 @@ import CodeAppRenderer from './CodeAppRenderer';
 
 interface Props {
   branchLike?: BranchLike;
+  branchLikes: BranchLike[];
   component: Component;
-  fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => Promise<void>;
   location: Location;
   router: Router;
   metrics: Dict<Metric>;
@@ -68,7 +66,6 @@ class CodeApp extends React.Component<Props, State> {
       total: 0,
       newCodeSelected: true,
     };
-    this.refreshBranchStatus = debounce(this.refreshBranchStatus, 1000);
   }
 
   componentDidMount() {
@@ -184,10 +181,6 @@ class CodeApp extends React.Component<Props, State> {
     this.setState({ highlighted });
   };
 
-  handleIssueChange = (_: Issue) => {
-    this.refreshBranchStatus();
-  };
-
   handleSearchClear = () => {
     this.setState({ searchResults: undefined });
   };
@@ -223,13 +216,6 @@ class CodeApp extends React.Component<Props, State> {
     this.loadComponent(finalKey);
   };
 
-  refreshBranchStatus = () => {
-    const { branchLike, component } = this.props;
-    if (branchLike && component && isPullRequest(branchLike)) {
-      this.props.fetchBranchStatus(branchLike, component.key).catch(noop);
-    }
-  };
-
   render() {
     return (
       <CodeAppRenderer
@@ -237,7 +223,6 @@ class CodeApp extends React.Component<Props, State> {
         {...this.state}
         handleGoToParent={this.handleGoToParent}
         handleHighlight={this.handleHighlight}
-        handleIssueChange={this.handleIssueChange}
         handleLoadMore={this.handleLoadMore}
         handleSearchClear={this.handleSearchClear}
         handleSearchResults={this.handleSearchResults}
@@ -248,6 +233,4 @@ class CodeApp extends React.Component<Props, State> {
   }
 }
 
-export default withRouter(
-  withComponentContext(withBranchStatusActions(withMetricsContext(CodeApp)))
-);
+export default withRouter(withComponentContext(withMetricsContext(withBranchLikes(CodeApp))));
index 67fc4ee4697e0d5c758398f56f7907e003117060..d7ea12754eb261cb60df8a925d477ec66c1b19c2 100644 (file)
@@ -38,7 +38,7 @@ import { KeyboardKeys } from '../../../helpers/keycodes';
 import { translate } from '../../../helpers/l10n';
 import { BranchLike } from '../../../types/branch-like';
 import { isApplication, isPortfolioLike } from '../../../types/component';
-import { Breadcrumb, Component, ComponentMeasure, Dict, Issue, Metric } from '../../../types/types';
+import { Breadcrumb, Component, ComponentMeasure, Dict, Metric } from '../../../types/types';
 import '../code.css';
 import { getCodeMetrics } from '../utils';
 import CodeBreadcrumbs from './CodeBreadcrumbs';
@@ -63,7 +63,6 @@ interface Props {
 
   handleGoToParent: () => void;
   handleHighlight: (highlighted: ComponentMeasure) => void;
-  handleIssueChange: (issue: Issue) => void;
   handleLoadMore: () => void;
   handleSearchClear: () => void;
   handleSearchResults: (searchResults: ComponentMeasure[]) => void;
@@ -230,7 +229,6 @@ export default function CodeAppRenderer(props: Props) {
             isFile
             location={location}
             onGoToParent={props.handleGoToParent}
-            onIssueChange={props.handleIssueChange}
           />
         </div>
       )}
index e1f557e7825a2e09a1bf95978c96761fbd6e4136..652a41f68372c1e618ae9d9a4a4852055d3ebd05 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
+import SourceViewer from '../../../components/SourceViewer/SourceViewer';
 import withKeyboardNavigation from '../../../components/hoc/withKeyboardNavigation';
 import { Location } from '../../../components/hoc/withRouter';
-import SourceViewer from '../../../components/SourceViewer/SourceViewer';
 import { BranchLike } from '../../../types/branch-like';
-import { Issue, Measure } from '../../../types/types';
+import { Measure } from '../../../types/types';
 
 export interface SourceViewerWrapperProps {
   branchLike?: BranchLike;
   component: string;
   componentMeasures: Measure[] | undefined;
   location: Location;
-  onIssueChange?: (issue: Issue) => void;
 }
 
 function SourceViewerWrapper(props: SourceViewerWrapperProps) {
@@ -53,7 +52,6 @@ function SourceViewerWrapper(props: SourceViewerWrapperProps) {
       component={component}
       componentMeasures={componentMeasures}
       highlightedLine={finalLine}
-      onIssueChange={props.onIssueChange}
       onLoaded={handleLoaded}
       showMeasures
     />
index 05d8be68ed6c2a426a7c36317614a2f9b5cfbcd6..c235053b9b96e9197e76fddfc7b5a21a2b58a5c2 100644 (file)
@@ -21,15 +21,16 @@ import { act, screen, waitFor, within } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { times } from 'lodash';
 import selectEvent from 'react-select-event';
+import BranchesServiceMock from '../../../api/mocks/BranchesServiceMock';
 import ComponentsServiceMock from '../../../api/mocks/ComponentsServiceMock';
 import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock';
 import { MeasuresServiceMock } from '../../../api/mocks/MeasuresServiceMock';
-import { mockPullRequest } from '../../../helpers/mocks/branch-like';
 import { mockComponent } from '../../../helpers/mocks/component';
 import { mockMeasure, mockMetric } from '../../../helpers/testMocks';
 import { renderAppWithComponentContext } from '../../../helpers/testReactTestingUtils';
 import { byLabelText, byRole, byTestId, byText } from '../../../helpers/testSelector';
 import { ComponentContextShape, ComponentQualifier } from '../../../types/component';
+import { Feature } from '../../../types/features';
 import { MetricKey } from '../../../types/metrics';
 import routes from '../routes';
 
@@ -46,11 +47,13 @@ jest.mock('../../../api/metrics', () => {
 const componentsHandler = new ComponentsServiceMock();
 const measuresHandler = new MeasuresServiceMock();
 const issuesHandler = new IssuesServiceMock();
+const branchHandler = new BranchesServiceMock();
 
 afterEach(() => {
   componentsHandler.reset();
   measuresHandler.reset();
   issuesHandler.reset();
+  branchHandler.reset();
 });
 
 describe('rendering', () => {
@@ -144,12 +147,10 @@ describe('rendering', () => {
 
   it('should render correctly if on a pull request and viewing coverage', async () => {
     const { ui } = getPageObject();
-    renderMeasuresApp('component_measures?id=foo&metric=coverage&pullRequest=1', {
-      branchLike: mockPullRequest({ key: '1' }),
-    });
+    renderMeasuresApp('component_measures?id=foo&metric=coverage&pullRequest=01');
     await ui.appLoaded();
 
-    expect(ui.detailsUnavailableText.get()).toBeInTheDocument();
+    expect(await ui.detailsUnavailableText.find()).toBeInTheDocument();
   });
 
   it('should render a warning message if the user does not have access to all components', async () => {
@@ -538,7 +539,7 @@ function renderMeasuresApp(navigateTo?: string, componentContext?: Partial<Compo
   return renderAppWithComponentContext(
     'component_measures',
     routes,
-    { navigateTo },
+    { navigateTo, featureList: [Feature.BranchSupport] },
     { component: mockComponent({ key: 'foo' }), ...componentContext }
   );
 }
index 13aaf746887aeed9d9615a28d1f578ea95b9be51..33a9263ee3cc3e85b05ca61cbaefd86518135e7d 100644 (file)
@@ -27,12 +27,11 @@ import {
   themeBorder,
   themeColor,
 } from 'design-system';
-import { debounce, keyBy } from 'lodash';
+import { keyBy } from 'lodash';
 import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
 import { getMeasuresWithPeriod } from '../../../api/measures';
 import { getAllMetrics } from '../../../api/metrics';
-import withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions';
 import { ComponentContext } from '../../../app/components/componentContext/ComponentContext';
 import Suggestions from '../../../components/embed-docs-modal/Suggestions';
 import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
@@ -40,18 +39,12 @@ import { enhanceMeasure } from '../../../components/measure/utils';
 import '../../../components/search-navigator.css';
 import { getBranchLikeQuery, isPullRequest, isSameBranchLike } from '../../../helpers/branch-like';
 import { translate } from '../../../helpers/l10n';
+import { useBranchesQuery } from '../../../queries/branch';
 import { BranchLike } from '../../../types/branch-like';
 import { ComponentQualifier } from '../../../types/component';
 import { MeasurePageView } from '../../../types/measures';
 import { MetricKey } from '../../../types/metrics';
-import {
-  ComponentMeasure,
-  Dict,
-  Issue,
-  MeasureEnhanced,
-  Metric,
-  Period,
-} from '../../../types/types';
+import { ComponentMeasure, Dict, MeasureEnhanced, Metric, Period } from '../../../types/types';
 import Sidebar from '../sidebar/Sidebar';
 import '../style.css';
 import {
@@ -74,7 +67,6 @@ import MeasuresEmpty from './MeasuresEmpty';
 interface Props {
   branchLike?: BranchLike;
   component: ComponentMeasure;
-  fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => Promise<void>;
   location: Location;
   router: Router;
 }
@@ -97,7 +89,6 @@ class ComponentMeasuresApp extends React.PureComponent<Props, State> {
       measures: [],
       metrics: {},
     };
-    this.refreshBranchStatus = debounce(this.refreshBranchStatus, 1000);
   }
 
   componentDidMount() {
@@ -180,10 +171,6 @@ class ComponentMeasuresApp extends React.PureComponent<Props, State> {
     return metric;
   };
 
-  handleIssueChange = (_: Issue) => {
-    this.refreshBranchStatus();
-  };
-
   updateQuery = (newQuery: Partial<Query>) => {
     const query: Query = { ...parseQuery(this.props.location.query), ...newQuery };
 
@@ -206,13 +193,6 @@ class ComponentMeasuresApp extends React.PureComponent<Props, State> {
     });
   };
 
-  refreshBranchStatus = () => {
-    const { branchLike, component } = this.props;
-    if (branchLike && component && isPullRequest(branchLike)) {
-      this.props.fetchBranchStatus(branchLike, component.key);
-    }
-  };
-
   renderContent = (displayOverview: boolean, query: Query, metric?: Metric) => {
     const { branchLike, component } = this.props;
     const { leakPeriod } = this.state;
@@ -225,7 +205,6 @@ class ComponentMeasuresApp extends React.PureComponent<Props, State> {
             domain={query.metric}
             leakPeriod={leakPeriod}
             metrics={this.state.metrics}
-            onIssueChange={this.handleIssueChange}
             rootComponent={component}
             router={this.props.router}
             selected={query.selected}
@@ -261,7 +240,6 @@ class ComponentMeasuresApp extends React.PureComponent<Props, State> {
           branchLike={branchLike}
           leakPeriod={leakPeriod}
           metrics={this.state.metrics}
-          onIssueChange={this.handleIssueChange}
           requestedMetric={metric}
           rootComponent={component}
           router={this.props.router}
@@ -323,10 +301,11 @@ class ComponentMeasuresApp extends React.PureComponent<Props, State> {
  * is that we can't use the usual withComponentContext HOC, because the type
  * of `component` isn't the same. It probably used to work because of the lazy loading
  */
-const WrappedApp = withRouter(withBranchStatusActions(ComponentMeasuresApp));
+const WrappedApp = withRouter(ComponentMeasuresApp);
 
 function AppWithComponentContext() {
-  const { branchLike, component } = React.useContext(ComponentContext);
+  const { component } = React.useContext(ComponentContext);
+  const { data: { branchLike } = {} } = useBranchesQuery(component);
 
   return <WrappedApp branchLike={branchLike} component={component as ComponentMeasure} />;
 }
index 19e3a95ca0c72da175c9298931e445154a529cb4..f055182097dc8155ffff2341dc29607c1d41ab66 100644 (file)
@@ -42,7 +42,6 @@ import {
   ComponentMeasureEnhanced,
   ComponentMeasureIntern,
   Dict,
-  Issue,
   Measure,
   Metric,
   Paging,
@@ -62,7 +61,6 @@ interface Props {
   leakPeriod?: Period;
   requestedMetric: Pick<Metric, 'key' | 'direction'>;
   metrics: Dict<Metric>;
-  onIssueChange?: (issue: Issue) => void;
   rootComponent: ComponentMeasure;
   router: Router;
   selected?: string;
@@ -438,7 +436,6 @@ export default class MeasureContent extends React.PureComponent<Props, State> {
                 branchLike={branchLike}
                 component={baseComponent.key}
                 metricKey={this.state.metric?.key}
-                onIssueChange={this.props.onIssueChange}
               />
             </div>
           ) : (
index 8fb3ba81a25fc686ad6a9ee0f4b4d931fda42445..95a8382f216b7de899e4e5ef79c4f5f3fea36da3 100644 (file)
@@ -30,7 +30,6 @@ import {
   ComponentMeasureEnhanced,
   ComponentMeasureIntern,
   Dict,
-  Issue,
   Metric,
   Paging,
   Period,
@@ -49,7 +48,6 @@ interface Props {
   leakPeriod?: Period;
   loading: boolean;
   metrics: Dict<Metric>;
-  onIssueChange?: (issue: Issue) => void;
   rootComponent: ComponentMeasure;
   updateLoading: (param: Dict<boolean>) => void;
   updateSelected: (component: ComponentMeasureIntern) => void;
@@ -127,12 +125,7 @@ export default class MeasureOverview extends React.PureComponent<Props, State> {
     if (isFile) {
       return (
         <div className="measure-details-viewer">
-          <SourceViewer
-            hideHeader
-            branchLike={branchLike}
-            component={component.key}
-            onIssueChange={this.props.onIssueChange}
-          />
+          <SourceViewer hideHeader branchLike={branchLike} component={component.key} />
         </div>
       );
     }
index 51667902957d1f73cbeebf7d64bed3eef2b44576..f97567175bf14086ebdad940b33e53f30ff977dd 100644 (file)
@@ -28,7 +28,6 @@ import {
   ComponentMeasure,
   ComponentMeasureIntern,
   Dict,
-  Issue,
   Metric,
   Period,
 } from '../../../types/types';
@@ -41,7 +40,6 @@ interface Props {
   domain: string;
   leakPeriod?: Period;
   metrics: Dict<Metric>;
-  onIssueChange?: (issue: Issue) => void;
   rootComponent: ComponentMeasure;
   router: Router;
   selected?: string;
@@ -135,7 +133,6 @@ export default class MeasureOverviewContainer extends React.PureComponent<Props,
         leakPeriod={this.props.leakPeriod}
         loading={this.state.loading.component || this.state.loading.bubbles}
         metrics={this.props.metrics}
-        onIssueChange={this.props.onIssueChange}
         rootComponent={this.props.rootComponent}
         updateLoading={this.updateLoading}
         updateSelected={this.updateSelected}
index e3e6645d3d8700d805ab29da20937ae105ade349..8206deb02b665030fceb6d584ad2cc15547f38bf 100644 (file)
@@ -42,6 +42,7 @@ import IssueTypeIcon from '../../../components/icon-mappers/IssueTypeIcon';
 import { SEVERITIES } from '../../../helpers/constants';
 import { throwGlobalError } from '../../../helpers/error';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { withBranchStatusRefresh } from '../../../queries/branch';
 import { IssueSeverity } from '../../../types/issues';
 import { Dict, Issue, IssueType, Paging } from '../../../types/types';
 import AssigneeSelect from './AssigneeSelect';
@@ -51,6 +52,7 @@ interface Props {
   fetchIssues: (x: {}) => Promise<{ issues: Issue[]; paging: Paging }>;
   onClose: () => void;
   onDone: () => void;
+  refreshBranchStatus: () => void;
 }
 
 interface FormFields {
@@ -84,7 +86,7 @@ enum InputField {
 
 export const MAX_PAGE_SIZE = 500;
 
-export default class BulkChangeModal extends React.PureComponent<Props, State> {
+export class BulkChangeModal extends React.PureComponent<Props, State> {
   mounted = false;
 
   constructor(props: Props) {
@@ -185,6 +187,7 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
     bulkChangeIssues(issueKeys, query).then(
       () => {
         this.setState({ submitting: false });
+        this.props.refreshBranchStatus();
         this.props.onDone();
       },
       (error) => {
@@ -499,3 +502,5 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
 function hasAction(action: string) {
   return (issue: Issue) => issue.actions && issue.actions.includes(action);
 }
+
+export default withBranchStatusRefresh(BulkChangeModal);
index 9cb91a60ee32c17adfdf0d30d0a20387b73384b5..3d38005558cca28970f152c8ba8e5f04ee61c9e7 100644 (file)
@@ -31,13 +31,12 @@ import {
   themeBorder,
   themeColor,
 } from 'design-system';
-import { debounce, keyBy, omit, without } from 'lodash';
+import { keyBy, omit, without } from 'lodash';
 import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
 import { FormattedMessage } from 'react-intl';
 import { searchIssues } from '../../../api/issues';
 import { getRuleDetails } from '../../../api/rules';
-import withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions';
 import withComponentContext from '../../../app/components/componentContext/withComponentContext';
 import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
 import { PageContext } from '../../../app/components/indexation/PageUnavailableDueToIndexation';
@@ -51,12 +50,7 @@ import { Location, Router, withRouter } from '../../../components/hoc/withRouter
 import IssueTabViewer from '../../../components/rules/IssueTabViewer';
 import '../../../components/search-navigator.css';
 import DeferredSpinner from '../../../components/ui/DeferredSpinner';
-import {
-  fillBranchLike,
-  getBranchLikeQuery,
-  isPullRequest,
-  isSameBranchLike,
-} from '../../../helpers/branch-like';
+import { fillBranchLike, getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like';
 import handleRequiredAuthentication from '../../../helpers/handleRequiredAuthentication';
 import { parseIssueFromResponse } from '../../../helpers/issues';
 import { isDatePicker, isInput, isShortcut } from '../../../helpers/keyboardEventHelpers';
@@ -69,6 +63,7 @@ import {
   removeWhitePageClass,
 } from '../../../helpers/pages';
 import { serializeDate } from '../../../helpers/query';
+import { withBranchLikes } from '../../../queries/branch';
 import { BranchLike } from '../../../types/branch-like';
 import { ComponentQualifier, isPortfolioLike, isProject } from '../../../types/component';
 import {
@@ -115,11 +110,9 @@ interface Props {
   branchLike?: BranchLike;
   component?: Component;
   currentUser: CurrentUser;
-  fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => void;
   location: Location;
   router: Router;
 }
-
 export interface State {
   bulkChangeModal: boolean;
   cannotShowOpenIssue?: boolean;
@@ -153,7 +146,6 @@ export interface State {
 
 const DEFAULT_QUERY = { resolved: 'false' };
 const MAX_INITAL_FETCH = 1000;
-const BRANCH_STATUS_REFRESH_INTERVAL = 1000;
 const VARIANTS_FACET = 'codeVariants';
 
 export class App extends React.PureComponent<Props, State> {
@@ -197,8 +189,6 @@ export class App extends React.PureComponent<Props, State> {
       referencedUsers: {},
       selected: getOpen(props.location.query),
     };
-
-    this.refreshBranchStatus = debounce(this.refreshBranchStatus, BRANCH_STATUS_REFRESH_INTERVAL);
   }
 
   static getDerivedStateFromProps(props: Props, state: State) {
@@ -835,8 +825,6 @@ export class App extends React.PureComponent<Props, State> {
   };
 
   handleIssueChange = (issue: Issue) => {
-    this.refreshBranchStatus();
-
     this.setState((state) => ({
       issues: state.issues.map((candidate) => (candidate.key === issue.key ? issue : candidate)),
     }));
@@ -856,7 +844,6 @@ export class App extends React.PureComponent<Props, State> {
 
   handleBulkChangeDone = () => {
     this.setState({ checkAll: false });
-    this.refreshBranchStatus();
     this.fetchFirstIssues(false).catch(() => undefined);
     this.handleCloseBulkChange();
   };
@@ -910,14 +897,6 @@ export class App extends React.PureComponent<Props, State> {
     this.setState(actions.selectPreviousFlow);
   };
 
-  refreshBranchStatus = () => {
-    const { branchLike, component } = this.props;
-
-    if (branchLike && component && isPullRequest(branchLike)) {
-      this.props.fetchBranchStatus(branchLike, component.key);
-    }
-  };
-
   renderBulkChange() {
     const { currentUser } = this.props;
     const { checkAll, bulkChangeModal, checked, issues, paging } = this.state;
@@ -1324,7 +1303,7 @@ export class App extends React.PureComponent<Props, State> {
 }
 
 export default withIndexationGuard(
-  withRouter(withCurrentUserContext(withBranchStatusActions(withComponentContext(App)))),
+  withRouter(withComponentContext(withCurrentUserContext(withBranchLikes(App)))),
   PageContext.Issues
 );
 
index b964c4f117243f7b02ba6bfeacf0cae3ec64641f..aec43a577d0a4d0d1859fe9681345914f4115765 100644 (file)
@@ -26,6 +26,7 @@ import CurrentUserContextProvider from '../../../../app/components/current-user/
 import { SEVERITIES } from '../../../../helpers/constants';
 import { mockIssue, mockLoggedInUser } from '../../../../helpers/testMocks';
 import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { ComponentPropsType } from '../../../../helpers/testUtils';
 import { IssueType } from '../../../../types/issues';
 import { Issue } from '../../../../types/types';
 import { CurrentUser } from '../../../../types/users';
@@ -187,7 +188,7 @@ it('should properly submit', async () => {
 
 function renderBulkChangeModal(
   issues: Issue[],
-  props: Partial<BulkChangeModal['props']> = {},
+  props: Partial<ComponentPropsType<typeof BulkChangeModal>> = {},
   currentUser: CurrentUser = mockLoggedInUser()
 ) {
   return renderComponent(
index 3962f6ba9819a34049b7d8fa6b7f472045b1e29e..04b3969f7dd16003bf9fcda4cd59667cb2e91a3e 100644 (file)
@@ -23,7 +23,7 @@ import * as React from 'react';
 import { mockFlowLocation, mockIssue, mockPaging } from '../../../../helpers/testMocks';
 import { renderComponent } from '../../../../helpers/testReactTestingUtils';
 import { byRole, byText } from '../../../../helpers/testSelector';
-import { FCProps } from '../../../../helpers/testUtils';
+import { ComponentPropsType } from '../../../../helpers/testUtils';
 import { FlowType, Issue } from '../../../../types/types';
 import { VISIBLE_LOCATIONS_COLLAPSE } from '../IssueLocationsCrossFile';
 import SubnavigationIssuesList from '../SubnavigationIssuesList';
@@ -245,7 +245,7 @@ function getPageObject() {
 
 function renderConciseIssues(
   issues: Issue[],
-  listProps: Partial<FCProps<typeof SubnavigationIssuesList>> = {}
+  listProps: Partial<ComponentPropsType<typeof SubnavigationIssuesList>> = {}
 ) {
   const wrapper = renderComponent(
     <SubnavigationIssuesList
@@ -266,7 +266,7 @@ function renderConciseIssues(
 
   function override(
     issues: Issue[],
-    listProps: Partial<FCProps<typeof SubnavigationIssuesList>> = {}
+    listProps: Partial<ComponentPropsType<typeof SubnavigationIssuesList>> = {}
   ) {
     wrapper.rerender(
       <SubnavigationIssuesList
index 005897e31d2afa6b438503cd2e34aded71dbfaa5..8e06b68f8d90f3734e99e5fdda9e794a40a8113b 100644 (file)
@@ -166,7 +166,6 @@ export class SidebarClass extends React.PureComponent<Props> {
 
     const displayPeriodFilter = component !== undefined && !isPortfolioLike(component.qualifier);
     const displayProjectsFacet = !component || isView(component.qualifier);
-    const displayAuthorFacet = !component || component.qualifier !== ComponentQualifier.Developper;
 
     return (
       <>
@@ -356,7 +355,7 @@ export class SidebarClass extends React.PureComponent<Props> {
           </>
         )}
 
-        {displayAuthorFacet && !disableDeveloperAggregatedInfo && (
+        {!disableDeveloperAggregatedInfo && (
           <>
             <BasicSeparator className="sw-my-4" />
 
index 020ae38d128e7b538e3f34fae129b7bb4ce622bc..cef93cc97d22c703da1b79e4e804cf9b62f29beb 100644 (file)
@@ -97,6 +97,13 @@ export default class BranchOverview extends React.PureComponent<Props, State> {
     this.loadHistory();
   }
 
+  componentDidUpdate(prevProps: Props) {
+    if (prevProps.branch !== this.props.branch) {
+      this.loadStatus();
+      this.loadHistory();
+    }
+  }
+
   componentWillUnmount() {
     this.mounted = false;
   }
index b3af0f38b155ca45284739c03f2bc27de7f6ba99..96a4bcf287bf25c7e1d1980b9601591a62da4f5a 100644 (file)
@@ -26,8 +26,8 @@ import withComponentContext from '../../../app/components/componentContext/withC
 import Suggestions from '../../../components/embed-docs-modal/Suggestions';
 import { isPullRequest } from '../../../helpers/branch-like';
 import { translate } from '../../../helpers/l10n';
+import { useBranchesQuery } from '../../../queries/branch';
 import { ProjectAlmBindingResponse } from '../../../types/alm-settings';
-import { BranchLike } from '../../../types/branch-like';
 import { isPortfolioLike } from '../../../types/component';
 import { Feature } from '../../../types/features';
 import { Component } from '../../../types/types';
@@ -36,8 +36,6 @@ import PullRequestOverview from '../pullRequests/PullRequestOverview';
 import EmptyOverview from './EmptyOverview';
 
 interface AppProps extends WithAvailableFeaturesProps {
-  branchLike?: BranchLike;
-  branchLikes: BranchLike[];
   component: Component;
   isInProgress?: boolean;
   isPending?: boolean;
@@ -45,13 +43,16 @@ interface AppProps extends WithAvailableFeaturesProps {
 }
 
 export function App(props: AppProps) {
-  const { branchLike, branchLikes, component, projectBinding, isPending, isInProgress } = props;
+  const { component, projectBinding, isPending, isInProgress } = props;
   const branchSupportEnabled = props.hasFeature(Feature.BranchSupport);
+  const { data } = useBranchesQuery(component);
 
-  if (isPortfolioLike(component.qualifier)) {
+  if (isPortfolioLike(component.qualifier) || !data) {
     return null;
   }
 
+  const { branchLike, branchLikes } = data;
+
   return (
     <>
       <Helmet defer={false} title={translate('overview.page')} />
index 22395f7a1138c2a0f22df91f1bd59872fac76898..eeaeaf0c1c5cdaf16a01768728c9ef2e42f7d0db 100644 (file)
  */
 import { screen } from '@testing-library/react';
 import * as React from 'react';
+import BranchesServiceMock from '../../../../api/mocks/BranchesServiceMock';
 import CurrentUserContextProvider from '../../../../app/components/current-user/CurrentUserContextProvider';
-import { mockBranch, mockMainBranch } from '../../../../helpers/mocks/branch-like';
+import { mockBranch } from '../../../../helpers/mocks/branch-like';
 import { mockComponent } from '../../../../helpers/mocks/component';
 import { mockCurrentUser } from '../../../../helpers/testMocks';
 import { renderComponent } from '../../../../helpers/testReactTestingUtils';
 import { ComponentQualifier } from '../../../../types/component';
 import { App } from '../App';
 
+const handler = new BranchesServiceMock();
+
+beforeEach(() => {
+  handler.reset();
+});
+
 it('should render Empty Overview for Application with no analysis', async () => {
   renderApp({ component: mockComponent({ qualifier: ComponentQualifier.Application }) });
 
@@ -37,7 +44,7 @@ it('should render Empty Overview on main branch with no analysis', async () => {
   renderApp({}, mockCurrentUser());
 
   expect(
-    await screen.findByText('provisioning.no_analysis_on_main_branch.master')
+    await screen.findByText('provisioning.no_analysis_on_main_branch.main')
   ).toBeInTheDocument();
 });
 
@@ -46,7 +53,7 @@ it('should render Empty Overview on main branch with multiple branches with bad
 
   expect(
     await screen.findByText(
-      'provisioning.no_analysis_on_main_branch.bad_configuration.master.branches.main_branch'
+      'provisioning.no_analysis_on_main_branch.bad_configuration.main.branches.main_branch'
     )
   ).toBeInTheDocument();
 });
@@ -68,13 +75,8 @@ it('should not render for portfolios and subportfolios', () => {
 function renderApp(props = {}, userProps = {}) {
   return renderComponent(
     <CurrentUserContextProvider currentUser={mockCurrentUser({ isLoggedIn: true, ...userProps })}>
-      <App
-        hasFeature={jest.fn().mockReturnValue(false)}
-        branchLikes={[]}
-        branchLike={mockMainBranch()}
-        component={mockComponent()}
-        {...props}
-      />
-    </CurrentUserContextProvider>
+      <App hasFeature={jest.fn().mockReturnValue(false)} component={mockComponent()} {...props} />
+    </CurrentUserContextProvider>,
+    '/?id=my-project'
   );
 }
index 7bb2f167366a81e3f4b1f3b3c2ce0d055a5b2192..d5be3ce02918a64ac6e344a7b4d67675c2c2f219 100644 (file)
@@ -29,21 +29,20 @@ import {
   PageTitle,
   TextMuted,
 } from 'design-system';
-import { differenceBy, uniq } from 'lodash';
+import { uniq } from 'lodash';
 import * as React from 'react';
+import { useEffect, useState } from 'react';
 import { FormattedMessage } from 'react-intl';
 import { getMeasuresWithMetrics } from '../../../api/measures';
-import { BranchStatusContextInterface } from '../../../app/components/branch-status/BranchStatusContext';
-import withBranchStatus from '../../../app/components/branch-status/withBranchStatus';
-import withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions';
 import HelpTooltip from '../../../components/controls/HelpTooltip';
 import { duplicationRatingConverter } from '../../../components/measure/utils';
 import { getBranchLikeQuery } from '../../../helpers/branch-like';
 import { translate } from '../../../helpers/l10n';
 import { enhanceConditionWithMeasure, enhanceMeasuresWithMetrics } from '../../../helpers/measures';
 import { isDefined } from '../../../helpers/types';
-import { getQualityGatesUrl, getQualityGateUrl } from '../../../helpers/urls';
-import { BranchStatusData, PullRequest } from '../../../types/branch-like';
+import { getQualityGateUrl, getQualityGatesUrl } from '../../../helpers/urls';
+import { useBranchStatusQuery } from '../../../queries/branch';
+import { PullRequest } from '../../../types/branch-like';
 import { IssueType } from '../../../types/issues';
 import { Component, MeasureEnhanced } from '../../../types/types';
 import MeasuresPanelIssueMeasure from '../branches/MeasuresPanelIssueMeasure';
@@ -57,73 +56,21 @@ import SonarLintPromotion from '../components/SonarLintPromotion';
 import '../styles.css';
 import { MeasurementType, PR_METRICS } from '../utils';
 
-interface Props extends BranchStatusData, Pick<BranchStatusContextInterface, 'fetchBranchStatus'> {
+interface Props {
   branchLike: PullRequest;
   component: Component;
 }
 
-interface State {
-  loading: boolean;
-  measures: MeasureEnhanced[];
-}
-
-export class PullRequestOverview extends React.PureComponent<Props, State> {
-  mounted = false;
-
-  state: State = {
-    loading: false,
-    measures: [],
-  };
+export default function PullRequestOverview(props: Props) {
+  const { component, branchLike } = props;
+  const [loadingMeasure, setLoadingMeasure] = useState(false);
+  const [measures, setMeasures] = useState<MeasureEnhanced[]>([]);
+  const { data: { conditions, ignoredConditions, status } = {}, isLoading } =
+    useBranchStatusQuery(component);
+  const loading = isLoading || loadingMeasure;
 
-  componentDidMount() {
-    this.mounted = true;
-    if (this.props.conditions === undefined) {
-      this.fetchBranchStatusData();
-    } else {
-      this.fetchBranchData();
-    }
-  }
-
-  componentDidUpdate(prevProps: Props) {
-    if (this.conditionsHaveChanged(prevProps)) {
-      this.fetchBranchData();
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  conditionsHaveChanged = (prevProps: Props) => {
-    const prevConditions = prevProps.conditions ?? [];
-    const newConditions = this.props.conditions ?? [];
-    const diff = differenceBy(
-      prevConditions.filter((c) => c.level === 'ERROR'),
-      newConditions.filter((c) => c.level === 'ERROR'),
-      (c) => c.metric
-    );
-
-    return (
-      (prevProps.conditions === undefined && this.props.conditions !== undefined) || diff.length > 0
-    );
-  };
-
-  fetchBranchStatusData = () => {
-    const {
-      branchLike,
-      component: { key },
-    } = this.props;
-    this.props.fetchBranchStatus(branchLike, key);
-  };
-
-  fetchBranchData = () => {
-    const {
-      branchLike,
-      component: { key },
-      conditions,
-    } = this.props;
-
-    this.setState({ loading: true });
+  useEffect(() => {
+    setLoadingMeasure(true);
 
     const metricKeys =
       conditions !== undefined
@@ -131,153 +78,140 @@ export class PullRequestOverview extends React.PureComponent<Props, State> {
           uniq([...PR_METRICS, ...conditions.filter((c) => c.level !== 'OK').map((c) => c.metric)])
         : PR_METRICS;
 
-    getMeasuresWithMetrics(key, metricKeys, getBranchLikeQuery(branchLike)).then(
+    getMeasuresWithMetrics(component.key, metricKeys, getBranchLikeQuery(branchLike)).then(
       ({ component, metrics }) => {
-        if (this.mounted && component.measures) {
-          this.setState({
-            loading: false,
-            measures: enhanceMeasuresWithMetrics(component.measures || [], metrics),
-          });
+        if (component.measures) {
+          setLoadingMeasure(false);
+          setMeasures(enhanceMeasuresWithMetrics(component.measures || [], metrics));
         }
       },
       () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
+        setLoadingMeasure(false);
       }
     );
-  };
+  }, [branchLike, component.key, conditions]);
+
+  if (loading) {
+    return (
+      <LargeCenteredLayout>
+        <div className="sw-p-6">
+          <DeferredSpinner loading />
+        </div>
+      </LargeCenteredLayout>
+    );
+  }
 
-  render() {
-    const { branchLike, component, conditions, ignoredConditions, status } = this.props;
-    const { loading, measures } = this.state;
+  if (conditions === undefined) {
+    return null;
+  }
 
-    if (loading) {
-      return (
-        <LargeCenteredLayout>
-          <div className="sw-p-6">
-            <DeferredSpinner loading />
-          </div>
-        </LargeCenteredLayout>
-      );
-    }
+  const path =
+    component.qualityGate === undefined
+      ? getQualityGatesUrl()
+      : getQualityGateUrl(component.qualityGate.name);
+
+  const failedConditions = conditions
+    .filter((condition) => condition.level === 'ERROR')
+    .map((c) => enhanceConditionWithMeasure(c, measures))
+    .filter(isDefined);
+
+  return (
+    <LargeCenteredLayout>
+      <div className="it__pr-overview sw-mt-12">
+        <div className="sw-flex">
+          <div className="sw-flex sw-flex-col sw-mr-12 width-30">
+            <QualityGateStatusTitle />
+            <Card>
+              {status && (
+                <QualityGateStatusHeader
+                  status={status}
+                  failedConditionCount={failedConditions.length}
+                />
+              )}
+
+              <div className="sw-flex sw-items-center sw-mb-4">
+                <TextMuted text={translate('overview.on_new_code_long')} />
+                <HelpTooltip
+                  className="sw-ml-2"
+                  overlay={
+                    <FormattedMessage
+                      defaultMessage={translate('overview.quality_gate.conditions_on_new_code')}
+                      id="overview.quality_gate.conditions_on_new_code"
+                      values={{
+                        link: <Link to={path}>{translate('overview.quality_gate')}</Link>,
+                      }}
+                    />
+                  }
+                >
+                  <HelperHintIcon aria-label="help-tooltip" />
+                </HelpTooltip>
+              </div>
 
-    if (conditions === undefined) {
-      return null;
-    }
+              {ignoredConditions && <IgnoredConditionWarning />}
 
-    const path =
-      component.qualityGate === undefined
-        ? getQualityGatesUrl()
-        : getQualityGateUrl(component.qualityGate.name);
+              {status === 'OK' && failedConditions.length === 0 && <QualityGateStatusPassedView />}
 
-    const failedConditions = conditions
-      .filter((condition) => condition.level === 'ERROR')
-      .map((c) => enhanceConditionWithMeasure(c, measures))
-      .filter(isDefined);
+              {status !== 'OK' && <BasicSeparator />}
 
-    return (
-      <LargeCenteredLayout>
-        <div className="it__pr-overview sw-mt-12">
-          <div className="sw-flex">
-            <div className="sw-flex sw-flex-col sw-mr-12 width-30">
-              <QualityGateStatusTitle />
-              <Card>
-                {status && (
-                  <QualityGateStatusHeader
-                    status={status}
-                    failedConditionCount={failedConditions.length}
+              {failedConditions.length > 0 && (
+                <div>
+                  <QualityGateConditions
+                    branchLike={branchLike}
+                    collapsible
+                    component={component}
+                    failedConditions={failedConditions}
                   />
-                )}
-
-                <div className="sw-flex sw-items-center sw-mb-4">
-                  <TextMuted text={translate('overview.on_new_code_long')} />
-                  <HelpTooltip
-                    className="sw-ml-2"
-                    overlay={
-                      <FormattedMessage
-                        defaultMessage={translate('overview.quality_gate.conditions_on_new_code')}
-                        id="overview.quality_gate.conditions_on_new_code"
-                        values={{
-                          link: <Link to={path}>{translate('overview.quality_gate')}</Link>,
-                        }}
-                      />
-                    }
-                  >
-                    <HelperHintIcon aria-label="help-tooltip" />
-                  </HelpTooltip>
                 </div>
+              )}
+            </Card>
+            <SonarLintPromotion qgConditions={conditions} />
+          </div>
 
-                {ignoredConditions && <IgnoredConditionWarning />}
-
-                {status === 'OK' && failedConditions.length === 0 && (
-                  <QualityGateStatusPassedView />
-                )}
-
-                {status !== 'OK' && <BasicSeparator />}
-
-                {failedConditions.length > 0 && (
-                  <div>
-                    <QualityGateConditions
-                      branchLike={branchLike}
-                      collapsible
-                      component={component}
-                      failedConditions={failedConditions}
-                    />
-                  </div>
-                )}
-              </Card>
-              <SonarLintPromotion qgConditions={conditions} />
+          <div className="sw-flex-1">
+            <div className="sw-body-md-highlight">
+              <PageTitle as="h2" text={translate('overview.measures')} />
             </div>
 
-            <div className="sw-flex-1">
-              <div className="sw-body-md-highlight">
-                <PageTitle as="h2" text={translate('overview.measures')} />
-              </div>
+            <div className="sw-grid sw-grid-cols-2 sw-gap-4 sw-mt-4">
+              {[
+                IssueType.Bug,
+                IssueType.Vulnerability,
+                IssueType.SecurityHotspot,
+                IssueType.CodeSmell,
+              ].map((type: IssueType) => (
+                <Card key={type} className="sw-p-8">
+                  <MeasuresPanelIssueMeasure
+                    branchLike={branchLike}
+                    component={component}
+                    isNewCodeTab
+                    measures={measures}
+                    type={type}
+                  />
+                </Card>
+              ))}
 
-              <div className="sw-grid sw-grid-cols-2 sw-gap-4 sw-mt-4">
-                {[
-                  IssueType.Bug,
-                  IssueType.Vulnerability,
-                  IssueType.SecurityHotspot,
-                  IssueType.CodeSmell,
-                ].map((type: IssueType) => (
+              {[MeasurementType.Coverage, MeasurementType.Duplication].map(
+                (type: MeasurementType) => (
                   <Card key={type} className="sw-p-8">
-                    <MeasuresPanelIssueMeasure
+                    <MeasuresPanelPercentMeasure
                       branchLike={branchLike}
                       component={component}
-                      isNewCodeTab
                       measures={measures}
+                      ratingIcon={renderMeasureIcon(type)}
                       type={type}
+                      useDiffMetric
                     />
                   </Card>
-                ))}
-
-                {[MeasurementType.Coverage, MeasurementType.Duplication].map(
-                  (type: MeasurementType) => (
-                    <Card key={type} className="sw-p-8">
-                      <MeasuresPanelPercentMeasure
-                        branchLike={branchLike}
-                        component={component}
-                        measures={measures}
-                        ratingIcon={renderMeasureIcon(type)}
-                        type={type}
-                        useDiffMetric
-                      />
-                    </Card>
-                  )
-                )}
-              </div>
+                )
+              )}
             </div>
           </div>
         </div>
-      </LargeCenteredLayout>
-    );
-  }
+      </div>
+    </LargeCenteredLayout>
+  );
 }
 
-export default withBranchStatus(withBranchStatusActions(PullRequestOverview));
-
 function renderMeasureIcon(type: MeasurementType) {
   if (type === MeasurementType.Coverage) {
     return function CoverageIndicatorRenderer(value?: string) {
index aba05081a7fffc3e559053c9577d242e5429e590..53473aeb27c7d89b47e62e4756a343eb9d0eecbf 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 { screen } from '@testing-library/react';
+import { screen, waitFor } from '@testing-library/react';
 import * as React from 'react';
+import { getQualityGateProjectStatus } from '../../../../api/quality-gates';
 import CurrentUserContextProvider from '../../../../app/components/current-user/CurrentUserContextProvider';
 import { mockPullRequest } from '../../../../helpers/mocks/branch-like';
 import { mockComponent } from '../../../../helpers/mocks/component';
-import { mockQualityGateStatusCondition } from '../../../../helpers/mocks/quality-gates';
+import {
+  mockQualityGateProjectCondition,
+  mockQualityGateStatusCondition,
+} from '../../../../helpers/mocks/quality-gates';
 import { mockLoggedInUser, mockMetric, mockPeriod } from '../../../../helpers/testMocks';
 import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { ComponentPropsType } from '../../../../helpers/testUtils';
 import { ComponentQualifier } from '../../../../types/component';
 import { MetricKey } from '../../../../types/metrics';
-import { PullRequestOverview } from '../PullRequestOverview';
+import { CaycStatus } from '../../../../types/types';
+import PullRequestOverview from '../PullRequestOverview';
 
 jest.mock('../../../../api/measures', () => {
   return {
@@ -112,40 +118,59 @@ jest.mock('../../../../api/quality-gates', () => {
 });
 
 it('should render correctly for a passed QG', async () => {
-  renderPullRequestOverview({ status: 'OK', conditions: [] });
+  jest.mocked(getQualityGateProjectStatus).mockResolvedValueOnce({
+    status: 'OK',
+    conditions: [],
+    caycStatus: CaycStatus.Compliant,
+    ignoredConditions: false,
+  });
+  renderPullRequestOverview();
 
-  expect(await screen.findByText('metric.level.OK')).toBeInTheDocument();
+  await waitFor(async () => expect(await screen.findByText('metric.level.OK')).toBeInTheDocument());
 });
 
 it('should render correctly if conditions are ignored', async () => {
-  renderPullRequestOverview({ conditions: [], ignoredConditions: true });
+  jest.mocked(getQualityGateProjectStatus).mockResolvedValueOnce({
+    status: 'OK',
+    conditions: [],
+    caycStatus: CaycStatus.Compliant,
+    ignoredConditions: true,
+  });
+  renderPullRequestOverview();
 
-  expect(await screen.findByText('overview.quality_gate.ignored_conditions')).toBeInTheDocument();
+  await waitFor(async () =>
+    expect(await screen.findByText('overview.quality_gate.ignored_conditions')).toBeInTheDocument()
+  );
 });
 
 it('should render correctly for a failed QG', async () => {
-  renderPullRequestOverview({
+  jest.mocked(getQualityGateProjectStatus).mockResolvedValueOnce({
     status: 'ERROR',
     conditions: [
-      mockQualityGateStatusCondition({
-        error: '2.0',
-        metric: MetricKey.new_coverage,
-        period: 1,
+      mockQualityGateProjectCondition({
+        errorThreshold: '2.0',
+        metricKey: MetricKey.new_coverage,
+        periodIndex: 1,
       }),
-      mockQualityGateStatusCondition({
-        error: '1.0',
-        metric: MetricKey.duplicated_lines,
-        period: 1,
+      mockQualityGateProjectCondition({
+        errorThreshold: '1.0',
+        metricKey: MetricKey.duplicated_lines,
+        periodIndex: 1,
       }),
-      mockQualityGateStatusCondition({
-        error: '3',
-        metric: MetricKey.new_bugs,
-        period: 1,
+      mockQualityGateProjectCondition({
+        errorThreshold: '3',
+        metricKey: MetricKey.new_bugs,
+        periodIndex: 1,
       }),
     ],
+    caycStatus: CaycStatus.Compliant,
+    ignoredConditions: true,
   });
+  renderPullRequestOverview();
 
-  expect(await screen.findByText('metric.level.ERROR')).toBeInTheDocument();
+  await waitFor(async () =>
+    expect(await screen.findByText('metric.level.ERROR')).toBeInTheDocument()
+  );
 
   expect(await screen.findByText('metric.new_coverage.name')).toBeInTheDocument();
   expect(await screen.findByText('quality_gates.operator.GT 2.0%')).toBeInTheDocument();
@@ -158,11 +183,12 @@ it('should render correctly for a failed QG', async () => {
   expect(screen.getByText('quality_gates.operator.GT 3')).toBeInTheDocument();
 });
 
-function renderPullRequestOverview(props: Partial<PullRequestOverview['props']> = {}) {
+function renderPullRequestOverview(
+  props: Partial<ComponentPropsType<typeof PullRequestOverview>> = {}
+) {
   renderComponent(
     <CurrentUserContextProvider currentUser={mockLoggedInUser()}>
       <PullRequestOverview
-        fetchBranchStatus={jest.fn()}
         branchLike={mockPullRequest()}
         component={mockComponent({
           breadcrumbs: [mockComponent({ key: 'foo' })],
index f8e5ab4c6ac50b70119f33eb69fd1d6ca054d6d2..857895cb8dd47cf2500b8214746a26c92aee6c0c 100644 (file)
@@ -54,6 +54,12 @@ export default class BranchList extends React.PureComponent<Props, State> {
     this.fetchBranches();
   }
 
+  componentDidUpdate(prevProps: Props) {
+    if (prevProps.branchList !== this.props.branchList) {
+      this.fetchBranches();
+    }
+  }
+
   componentWillUnmount() {
     this.mounted = false;
   }
index f28d2d251bd80b7b897d3e892a2626b140d53bf1..3d73558be4dc99990ad4c2e867e133378dd6c70f 100644 (file)
@@ -36,6 +36,7 @@ import {
   DEFAULT_NEW_CODE_DEFINITION_TYPE,
   getNumberOfDaysDefaultValue,
 } from '../../../helpers/new-code-definition';
+import { withBranchLikes } from '../../../queries/branch';
 import { AppState } from '../../../types/appstate';
 import { Branch, BranchLike } from '../../../types/branch-like';
 import { Feature } from '../../../types/features';
@@ -130,7 +131,7 @@ class ProjectBaselineApp extends React.PureComponent<Props, State> {
 
   sortAndFilterBranches(branchLikes: BranchLike[] = []) {
     const branchList = sortBranches(branchLikes.filter(isBranch));
-    this.setState({ branchList, referenceBranch: branchList[0].name });
+    this.setState({ branchList, referenceBranch: branchList[0]?.name });
   }
 
   fetchLeakPeriodSetting() {
@@ -141,7 +142,7 @@ class ProjectBaselineApp extends React.PureComponent<Props, State> {
     Promise.all([
       getNewCodePeriod(),
       getNewCodePeriod({
-        branch: this.props.hasFeature(Feature.BranchSupport) ? undefined : branchLike.name,
+        branch: this.props.hasFeature(Feature.BranchSupport) ? undefined : branchLike?.name,
         project: component.key,
       }),
     ]).then(
@@ -344,4 +345,6 @@ class ProjectBaselineApp extends React.PureComponent<Props, State> {
   }
 }
 
-export default withComponentContext(withAvailableFeatures(withAppStateContext(ProjectBaselineApp)));
+export default withComponentContext(
+  withAvailableFeatures(withAppStateContext(withBranchLikes(ProjectBaselineApp)))
+);
index e16920aae9c18e3ef77446f60ee8863f3bd5f284..deb5da7ea24626e794218aeba9f140b5e6d43e8f 100644 (file)
@@ -40,7 +40,7 @@ import BranchAnalysisList from './BranchAnalysisList';
 
 export interface ProjectBaselineSelectorProps {
   analysis?: string;
-  branch: Branch;
+  branch?: Branch;
   branchList: Branch[];
   branchesEnabled?: boolean;
   canAdmin: boolean | undefined;
@@ -94,6 +94,10 @@ export default function ProjectBaselineSelector(props: ProjectBaselineSelectorPr
     selected,
   });
 
+  if (branch === undefined) {
+    return null;
+  }
+
   return (
     <form className="project-baseline-selector" onSubmit={props.onSubmit}>
       <div className="big-spacer-top spacer-bottom" role="radiogroup">
index d1fc4e010ebb2cfc09a3f398995d577c83fd62ec..6a425ddc54c204f73b5803d88e07c966570efc50 100644 (file)
@@ -21,9 +21,9 @@ import { within } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { first, last } from 'lodash';
 import selectEvent from 'react-select-event';
+import BranchesServiceMock from '../../../../api/mocks/BranchesServiceMock';
 import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock';
 import { ProjectActivityServiceMock } from '../../../../api/mocks/ProjectActivityServiceMock';
-import { mockBranch } from '../../../../helpers/mocks/branch-like';
 import { mockComponent } from '../../../../helpers/mocks/component';
 import { mockNewCodePeriodBranch } from '../../../../helpers/mocks/new-code-definition';
 import { mockAppState } from '../../../../helpers/testMocks';
@@ -38,11 +38,14 @@ import routes from '../../routes';
 
 jest.mock('../../../../api/newCodePeriod');
 jest.mock('../../../../api/projectActivity');
+jest.mock('../../../../api/branches');
 
 const codePeriodsMock = new NewCodePeriodsServiceMock();
 const projectActivityMock = new ProjectActivityServiceMock();
+const branchHandler = new BranchesServiceMock();
 
 afterEach(() => {
+  branchHandler.reset();
   codePeriodsMock.reset();
   projectActivityMock.reset();
 });
@@ -52,7 +55,7 @@ it('renders correctly without branch support feature', async () => {
   renderProjectBaselineApp();
   await ui.appIsLoaded();
 
-  expect(ui.generalSettingRadio.get()).toBeChecked();
+  expect(await ui.generalSettingRadio.find()).toBeChecked();
   expect(ui.specificAnalysisRadio.query()).not.toBeInTheDocument();
 
   // User is not admin
@@ -74,7 +77,7 @@ it('prevents selection of global setting if it is not compliant and warns non-ad
   renderProjectBaselineApp();
   await ui.appIsLoaded();
 
-  expect(ui.generalSettingRadio.get()).toBeChecked();
+  expect(await ui.generalSettingRadio.find()).toBeChecked();
   expect(ui.generalSettingRadio.get()).toBeDisabled();
   expect(ui.complianceWarning.get()).toBeVisible();
 });
@@ -90,7 +93,7 @@ it('prevents selection of global setting if it is not compliant and warns admin
   renderProjectBaselineApp({ appState: mockAppState({ canAdmin: true }) });
   await ui.appIsLoaded();
 
-  expect(ui.generalSettingRadio.get()).toBeChecked();
+  expect(await ui.generalSettingRadio.find()).toBeChecked();
   expect(ui.generalSettingRadio.get()).toBeDisabled();
   expect(ui.complianceWarningAdmin.get()).toBeVisible();
   expect(ui.complianceWarning.query()).not.toBeInTheDocument();
@@ -104,7 +107,7 @@ it('renders correctly with branch support feature', async () => {
   });
   await ui.appIsLoaded();
 
-  expect(ui.generalSettingRadio.get()).toBeChecked();
+  expect(await ui.generalSettingRadio.find()).toBeChecked();
   expect(ui.specificAnalysisRadio.query()).not.toBeInTheDocument();
 
   // User is admin
@@ -120,7 +123,7 @@ it('can set previous version specific setting', async () => {
   renderProjectBaselineApp();
   await ui.appIsLoaded();
 
-  expect(ui.previousVersionRadio.get()).toHaveClass('disabled');
+  expect(await ui.previousVersionRadio.find()).toHaveClass('disabled');
   await ui.setPreviousVersionSetting();
   expect(ui.previousVersionRadio.get()).toBeChecked();
 
@@ -141,7 +144,7 @@ it('can set number of days specific setting', async () => {
   renderProjectBaselineApp();
   await ui.appIsLoaded();
 
-  expect(ui.numberDaysRadio.get()).toHaveClass('disabled');
+  expect(await ui.numberDaysRadio.find()).toHaveClass('disabled');
   await ui.setNumberDaysSetting('10');
   expect(ui.numberDaysRadio.get()).toBeChecked();
 
@@ -164,7 +167,7 @@ it('can set reference branch specific setting', async () => {
   });
   await ui.appIsLoaded();
 
-  expect(ui.referenceBranchRadio.get()).toHaveClass('disabled');
+  expect(await ui.referenceBranchRadio.find()).toHaveClass('disabled');
   await ui.setReferenceBranchSetting('main');
   expect(ui.referenceBranchRadio.get()).toBeChecked();
 
@@ -183,7 +186,7 @@ it('cannot set specific analysis setting', async () => {
   renderProjectBaselineApp();
   await ui.appIsLoaded();
 
-  expect(ui.specificAnalysisRadio.get()).toBeChecked();
+  expect(await ui.specificAnalysisRadio.find()).toBeChecked();
   expect(ui.specificAnalysisRadio.get()).toHaveClass('disabled');
   expect(ui.specificAnalysisWarning.get()).toBeInTheDocument();
 
@@ -274,18 +277,25 @@ it('can set a reference branch setting for branch', async () => {
   });
   await ui.appIsLoaded();
 
-  await ui.setBranchReferenceToBranchSetting('main', 'feature');
+  await ui.setBranchReferenceToBranchSetting('main', 'normal-branch');
 
-  expect(byRole('table').byText('baseline.reference_branch: feature').get()).toBeInTheDocument();
+  expect(
+    byRole('table').byText('baseline.reference_branch: normal-branch').get()
+  ).toBeInTheDocument();
 });
 
-function renderProjectBaselineApp(context: RenderContext = {}) {
-  const branch = mockBranch({ name: 'main', isMain: true });
-  return renderAppWithComponentContext('baseline', routes, context, {
-    component: mockComponent(),
-    branchLike: branch,
-    branchLikes: [branch, mockBranch({ name: 'feature' })],
-  });
+function renderProjectBaselineApp(context: RenderContext = {}, params?: string) {
+  return renderAppWithComponentContext(
+    'baseline',
+    routes,
+    {
+      ...context,
+      navigateTo: params ? `baseline?id=my-project&${params}` : 'baseline?id=my-project',
+    },
+    {
+      component: mockComponent(),
+    }
+  );
 }
 
 function getPageObjects() {
@@ -293,6 +303,7 @@ function getPageObjects() {
 
   const ui = {
     pageHeading: byRole('heading', { name: 'project_baseline.page' }),
+    branchTableHeading: byText('branch_list.branch'),
     branchListHeading: byRole('heading', { name: 'project_baseline.default_setting' }),
     generalSettingsLink: byRole('link', { name: 'project_baseline.page.description2.link' }),
     generalSettingRadio: byRole('radio', { name: 'project_baseline.global_setting' }),
index 48407906d2fa241129cdb33a9bf345a5c6d815c2..c52f677190ef0c8d9e20378a22e55c4401713b5e 100644 (file)
@@ -21,19 +21,16 @@ import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
 import withComponentContext from '../../app/components/componentContext/withComponentContext';
 import { translate } from '../../helpers/l10n';
-import { BranchLike } from '../../types/branch-like';
 import { Component } from '../../types/types';
 import BranchLikeTabs from './components/BranchLikeTabs';
 import LifetimeInformation from './components/LifetimeInformation';
 
 export interface ProjectBranchesAppProps {
-  branchLikes: BranchLike[];
   component: Component;
-  onBranchesChange: () => void;
 }
 
 function ProjectBranchesApp(props: ProjectBranchesAppProps) {
-  const { branchLikes, component } = props;
+  const { component } = props;
 
   return (
     <div className="page page-limited" id="project-branch-like">
@@ -43,11 +40,7 @@ function ProjectBranchesApp(props: ProjectBranchesAppProps) {
         <LifetimeInformation />
       </header>
 
-      <BranchLikeTabs
-        branchLikes={branchLikes}
-        component={component}
-        onBranchesChange={props.onBranchesChange}
-      />
+      <BranchLikeTabs component={component} />
     </div>
   );
 }
index 5b3a1650df19a5f2991bf40b9d0fdfaab7d030be..08a5718589399449cf8d10679588a7557bbf6d44 100644 (file)
 
 import { act, within } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
-import React, { useEffect, useState } from 'react';
+import React from 'react';
 import BranchesServiceMock from '../../../api/mocks/BranchesServiceMock';
 import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock';
-import BranchStatusContextProvider from '../../../app/components/branch-status/BranchStatusContextProvider';
-import withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions';
 import { ComponentContext } from '../../../app/components/componentContext/ComponentContext';
 import { mockComponent } from '../../../helpers/mocks/component';
 import { mockAppState } from '../../../helpers/testMocks';
 import { renderComponent } from '../../../helpers/testReactTestingUtils';
 import { byRole } from '../../../helpers/testSelector';
 import { AppState } from '../../../types/appstate';
-import { BranchLike } from '../../../types/branch-like';
+import { Feature } from '../../../types/features';
 import { SettingsKey } from '../../../types/settings';
 import ProjectBranchesApp from '../ProjectBranchesApp';
 
 const handler = new BranchesServiceMock();
 const settingsHandler = new SettingsServiceMock();
 
-const ui = {
-  branchTabContent: byRole('tabpanel', { name: 'project_branch_pull_request.tabs.branches' }),
-  branchTabBtn: byRole('tab', { name: 'project_branch_pull_request.tabs.branches' }),
-  linkForAdmin: byRole('link', { name: 'settings.page' }),
-  renameBranchBtn: byRole('button', { name: 'project_branch_pull_request.branch.rename' }),
-  deleteBranchBtn: byRole('button', { name: 'project_branch_pull_request.branch.delete' }),
-  deletePullRequestBtn: byRole('button', {
+const ui = new (class UI {
+  branchTabContent = byRole('tabpanel', { name: 'project_branch_pull_request.tabs.branches' });
+  branchTabBtn = byRole('tab', { name: 'project_branch_pull_request.tabs.branches' });
+  linkForAdmin = byRole('link', { name: 'settings.page' });
+  renameBranchBtn = byRole('button', { name: 'project_branch_pull_request.branch.rename' });
+  deleteBranchBtn = byRole('button', { name: 'project_branch_pull_request.branch.delete' });
+  deletePullRequestBtn = byRole('button', {
     name: 'project_branch_pull_request.pull_request.delete',
-  }),
-  pullRequestTabContent: byRole('tabpanel', {
+  });
+
+  pullRequestTabContent = byRole('tabpanel', {
     name: 'project_branch_pull_request.tabs.pull_requests',
-  }),
-  pullRequestTabBtn: byRole('tab', {
+  });
+
+  pullRequestTabBtn = byRole('tab', {
     name: 'project_branch_pull_request.tabs.pull_requests',
-  }),
-  renameBranchDialog: byRole('dialog', { name: 'project_branch_pull_request.branch.rename' }),
-  deleteBranchDialog: byRole('dialog', { name: 'project_branch_pull_request.branch.delete' }),
-  deletePullRequestDialog: byRole('dialog', {
+  });
+
+  renameBranchDialog = byRole('dialog', { name: 'project_branch_pull_request.branch.rename' });
+  deleteBranchDialog = byRole('dialog', { name: 'project_branch_pull_request.branch.delete' });
+  deletePullRequestDialog = byRole('dialog', {
     name: 'project_branch_pull_request.pull_request.delete',
-  }),
-  updateMasterBtn: byRole('button', {
-    name: 'project_branch_pull_request.branch.actions_label.master',
-  }),
-  updateSecondBranchBtn: byRole('button', {
+  });
+
+  updateMasterBtn = byRole('button', {
+    name: 'project_branch_pull_request.branch.actions_label.main',
+  });
+
+  updateSecondBranchBtn = byRole('button', {
     name: 'project_branch_pull_request.branch.actions_label.delete-branch',
-  }),
-  updateFirstPRBtn: byRole('button', {
+  });
+
+  updateFirstPRBtn = byRole('button', {
     name: 'project_branch_pull_request.branch.actions_label.01 â€“ TEST-191 update master',
-  }),
-  getBranchRow: () => within(ui.branchTabContent.get()).getAllByRole('row'),
-  getPullRequestRow: () => within(ui.pullRequestTabContent.get()).getAllByRole('row'),
-};
+  });
+
+  branchRow = this.branchTabContent.byRole('row');
+  pullRequestRow = this.pullRequestTabContent.byRole('row');
+})();
 
 beforeEach(() => {
   jest.useFakeTimers({
@@ -90,13 +95,13 @@ it('should show all branches', async () => {
   expect(await ui.branchTabContent.find()).toBeInTheDocument();
   expect(ui.pullRequestTabContent.query()).not.toBeInTheDocument();
   expect(ui.linkForAdmin.query()).not.toBeInTheDocument();
-  expect(ui.getBranchRow()).toHaveLength(4);
-  expect(ui.getBranchRow()[1]).toHaveTextContent('masterbranches.main_branchOK1 month ago');
-  expect(within(ui.getBranchRow()[1]).getByRole('switch')).toBeDisabled();
-  expect(within(ui.getBranchRow()[1]).getByRole('switch')).toBeChecked();
-  expect(ui.getBranchRow()[2]).toHaveTextContent('delete-branchERROR2 days ago');
-  expect(within(ui.getBranchRow()[2]).getByRole('switch')).toBeEnabled();
-  expect(within(ui.getBranchRow()[2]).getByRole('switch')).not.toBeChecked();
+  expect(await ui.branchRow.findAll()).toHaveLength(4);
+  expect(ui.branchRow.getAt(1)).toHaveTextContent('mainbranches.main_branchOK1 month ago');
+  expect(within(ui.branchRow.getAt(1)).getByRole('switch')).toBeDisabled();
+  expect(within(ui.branchRow.getAt(1)).getByRole('switch')).toBeChecked();
+  expect(ui.branchRow.getAt(2)).toHaveTextContent('delete-branchERROR2 days ago');
+  expect(within(ui.branchRow.getAt(2)).getByRole('switch')).toBeEnabled();
+  expect(within(ui.branchRow.getAt(2)).getByRole('switch')).not.toBeChecked();
 });
 
 it('should show link to change purge options for admin', async () => {
@@ -112,7 +117,7 @@ it('should be able to rename main branch, but not others', async () => {
   expect(ui.renameBranchBtn.get()).toBeInTheDocument();
   await user.click(ui.renameBranchBtn.get());
   expect(ui.renameBranchDialog.get()).toBeInTheDocument();
-  expect(within(ui.renameBranchDialog.get()).getByRole('textbox')).toHaveValue('master');
+  expect(within(ui.renameBranchDialog.get()).getByRole('textbox')).toHaveValue('main');
   expect(
     within(ui.renameBranchDialog.get()).getByRole('button', { name: 'rename' })
   ).toBeDisabled();
@@ -120,12 +125,12 @@ it('should be able to rename main branch, but not others', async () => {
   expect(
     within(ui.renameBranchDialog.get()).getByRole('button', { name: 'rename' })
   ).toBeDisabled();
-  await user.type(within(ui.renameBranchDialog.get()).getByRole('textbox'), 'main');
+  await user.type(within(ui.renameBranchDialog.get()).getByRole('textbox'), 'master');
   expect(within(ui.renameBranchDialog.get()).getByRole('button', { name: 'rename' })).toBeEnabled();
   await act(() =>
     user.click(within(ui.renameBranchDialog.get()).getByRole('button', { name: 'rename' }))
   );
-  expect(ui.getBranchRow()[1]).toHaveTextContent('mainbranches.main_branchOK1 month ago');
+  expect(ui.branchRow.getAt(1)).toHaveTextContent('masterbranches.main_branchOK1 month ago');
 
   await user.click(await ui.updateSecondBranchBtn.find());
   expect(ui.renameBranchBtn.query()).not.toBeInTheDocument();
@@ -142,7 +147,7 @@ it('should be able to delete branch, but not main', async () => {
   await act(() =>
     user.click(within(ui.deleteBranchDialog.get()).getByRole('button', { name: 'delete' }))
   );
-  expect(ui.getBranchRow()).toHaveLength(3);
+  expect(ui.branchRow.getAll()).toHaveLength(3);
 
   await user.click(await ui.updateMasterBtn.find());
   expect(ui.deleteBranchBtn.query()).not.toBeInTheDocument();
@@ -152,18 +157,18 @@ it('should exclude from purge', async () => {
   const user = userEvent.setup();
   renderProjectBranchesApp();
   expect(await ui.branchTabContent.find()).toBeInTheDocument();
-  expect(within(ui.getBranchRow()[2]).getByRole('switch')).not.toBeChecked();
-  await act(() => user.click(within(ui.getBranchRow()[2]).getByRole('switch')));
-  expect(within(ui.getBranchRow()[2]).getByRole('switch')).toBeChecked();
+  expect(within(await ui.branchRow.findAt(2)).getByRole('switch')).not.toBeChecked();
+  await act(() => user.click(within(ui.branchRow.getAt(2)).getByRole('switch')));
+  expect(within(ui.branchRow.getAt(2)).getByRole('switch')).toBeChecked();
 
-  expect(within(ui.getBranchRow()[3]).getByRole('switch')).toBeChecked();
-  await act(() => user.click(within(ui.getBranchRow()[3]).getByRole('switch')));
-  expect(within(ui.getBranchRow()[3]).getByRole('switch')).not.toBeChecked();
+  expect(within(ui.branchRow.getAt(3)).getByRole('switch')).toBeChecked();
+  await act(() => user.click(within(ui.branchRow.getAt(3)).getByRole('switch')));
+  expect(within(ui.branchRow.getAt(3)).getByRole('switch')).not.toBeChecked();
 
   await user.click(ui.pullRequestTabBtn.get());
   await user.click(ui.branchTabBtn.get());
-  expect(within(ui.getBranchRow()[2]).getByRole('switch')).toBeChecked();
-  expect(within(ui.getBranchRow()[3]).getByRole('switch')).not.toBeChecked();
+  expect(within(ui.branchRow.getAt(2)).getByRole('switch')).toBeChecked();
+  expect(within(ui.branchRow.getAt(3)).getByRole('switch')).not.toBeChecked();
 });
 
 it('should show all pull requests', async () => {
@@ -172,9 +177,9 @@ it('should show all pull requests', async () => {
   await user.click(await ui.pullRequestTabBtn.find());
   expect(await ui.pullRequestTabContent.find()).toBeInTheDocument();
   expect(ui.branchTabContent.query()).not.toBeInTheDocument();
-  expect(ui.getPullRequestRow()).toHaveLength(4);
-  expect(ui.getPullRequestRow()[1]).toHaveTextContent('01 â€“ TEST-191 update masterOK1 month ago');
-  expect(ui.getPullRequestRow()[2]).toHaveTextContent(
+  expect(await ui.pullRequestRow.findAll()).toHaveLength(4);
+  expect(ui.pullRequestRow.getAt(1)).toHaveTextContent('01 â€“ TEST-191 update masterOK1 month ago');
+  expect(ui.pullRequestRow.getAt(2)).toHaveTextContent(
     '02 â€“ TEST-192 update normal-branchERROR2 days ago'
   );
 });
@@ -183,7 +188,7 @@ it('should delete pull requests', async () => {
   const user = userEvent.setup();
   renderProjectBranchesApp();
   await user.click(await ui.pullRequestTabBtn.find());
-  expect(ui.getPullRequestRow()).toHaveLength(4);
+  expect(await ui.pullRequestRow.findAll()).toHaveLength(4);
   await user.click(ui.updateFirstPRBtn.get());
   await user.click(ui.deletePullRequestBtn.get());
   expect(await ui.deletePullRequestDialog.find()).toBeInTheDocument();
@@ -191,57 +196,20 @@ it('should delete pull requests', async () => {
   await act(() =>
     user.click(within(ui.deletePullRequestDialog.get()).getByRole('button', { name: 'delete' }))
   );
-  expect(ui.getPullRequestRow()).toHaveLength(3);
+  expect(ui.pullRequestRow.getAll()).toHaveLength(3);
 });
 
 function renderProjectBranchesApp(overrides?: Partial<AppState>) {
-  function TestWrapper(props: any) {
-    const [init, setInit] = useState<boolean>(false);
-    const [branches, setBranches] = useState<BranchLike[]>([
-      ...handler.branches,
-      ...handler.pullRequests,
-    ]);
-
-    const updateBranches = (branches: BranchLike[]) => {
-      branches.forEach((item) => {
-        props.updateBranchStatus(item, 'my-project', item.status?.qualityGateStatus);
-      });
-    };
-
-    useEffect(() => {
-      updateBranches(branches);
-      setInit(true);
-    }, []);
-
-    const onBranchesChange = () => {
-      const changedBranches = [...handler.branches, ...handler.pullRequests];
-      updateBranches(changedBranches);
-      setBranches(changedBranches);
-    };
-
-    return init ? (
-      <ComponentContext.Provider
-        value={{
-          branchLikes: branches,
-          onBranchesChange,
-          onComponentChange: jest.fn(),
-          component: mockComponent(),
-        }}
-      >
-        {props.children}
-      </ComponentContext.Provider>
-    ) : null;
-  }
-
-  const Wrapper = withBranchStatusActions(TestWrapper);
-
   return renderComponent(
-    <BranchStatusContextProvider>
-      <Wrapper>
-        <ProjectBranchesApp />
-      </Wrapper>
-    </BranchStatusContextProvider>,
-    '/',
-    { appState: mockAppState(overrides) }
+    <ComponentContext.Provider
+      value={{
+        onComponentChange: jest.fn(),
+        component: mockComponent(),
+      }}
+    >
+      <ProjectBranchesApp />
+    </ComponentContext.Provider>,
+    '/?id=my-project',
+    { appState: mockAppState(overrides), featureList: [Feature.BranchSupport] }
   );
 }
index 35d510b0e692ebf2ffeb794410d5ab582bde4044..4f6c91183f5012269727b7cd51419848f33ccc62 100644 (file)
@@ -39,7 +39,6 @@ export interface BranchLikeRowProps {
   displayPurgeSetting?: boolean;
   onDelete: () => void;
   onRename: () => void;
-  onUpdatePurgeSetting: () => void;
 }
 
 function BranchLikeRow(props: BranchLikeRowProps) {
@@ -58,16 +57,12 @@ function BranchLikeRow(props: BranchLikeRowProps) {
         </span>
       </td>
       <td className="nowrap">
-        <BranchStatus branchLike={branchLike} component={component} />
+        <BranchStatus branchLike={branchLike} />
       </td>
       <td className="nowrap">{<DateFromNow date={branchLike.analysisDate} />}</td>
       {displayPurgeSetting && isBranch(branchLike) && (
         <td className="nowrap js-test-purge-toggle-container">
-          <BranchPurgeSetting
-            branch={branchLike}
-            component={component}
-            onUpdatePurgeSetting={props.onUpdatePurgeSetting}
-          />
+          <BranchPurgeSetting branch={branchLike} component={component} />
         </td>
       )}
       <td className="nowrap">
index 6cde13e25174ad07346025f136d1e8012a50b296..159b898de030077f8d8d17726b0ac934196bf813 100644 (file)
@@ -31,7 +31,6 @@ export interface BranchLikeTableProps {
   displayPurgeSetting?: boolean;
   onDelete: (branchLike: BranchLike) => void;
   onRename: (branchLike: BranchLike) => void;
-  onUpdatePurgeSetting: () => void;
   title: string;
 }
 
@@ -81,7 +80,6 @@ function BranchLikeTable(props: BranchLikeTableProps) {
               key={getBranchLikeKey(branchLike)}
               onDelete={() => props.onDelete(branchLike)}
               onRename={() => props.onRename(branchLike)}
-              onUpdatePurgeSetting={props.onUpdatePurgeSetting}
             />
           ))}
         </tbody>
index 1bd2f6705f04f1d80d19f09d6a4b626780cd06ca..9514fdd7373418b3a7ba547c90b7f90c5149d5ec 100644 (file)
@@ -18,6 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
+import { useState } from 'react';
 import BoxedTabs, { getTabId, getTabPanelId } from '../../../components/controls/BoxedTabs';
 import BranchIcon from '../../../components/icons/BranchIcon';
 import PullRequestIcon from '../../../components/icons/PullRequestIcon';
@@ -29,22 +30,15 @@ import {
   sortPullRequests,
 } from '../../../helpers/branch-like';
 import { translate } from '../../../helpers/l10n';
-import { BranchLike } from '../../../types/branch-like';
+import { useBranchesQuery } from '../../../queries/branch';
+import { Branch, BranchLike, PullRequest } from '../../../types/branch-like';
 import { Component } from '../../../types/types';
 import BranchLikeTable from './BranchLikeTable';
 import DeleteBranchModal from './DeleteBranchModal';
 import RenameBranchModal from './RenameBranchModal';
 
 interface Props {
-  branchLikes: BranchLike[];
   component: Component;
-  onBranchesChange: () => void;
-}
-
-interface State {
-  currentTab: Tabs;
-  deleting?: BranchLike;
-  renaming?: BranchLike;
 }
 
 export enum Tabs {
@@ -77,87 +71,57 @@ const TABS = [
   },
 ];
 
-export default class BranchLikeTabs extends React.PureComponent<Props, State> {
-  state: State = { currentTab: Tabs.Branch };
-
-  handleTabSelect = (currentTab: Tabs) => {
-    this.setState({ currentTab });
-  };
-
-  handleDeleteBranchLike = (branchLike: BranchLike) => {
-    this.setState({ deleting: branchLike });
-  };
-
-  handleRenameBranchLike = (branchLike: BranchLike) => {
-    this.setState({ renaming: branchLike });
-  };
-
-  handleUpdatePurgeSetting = () => {
-    this.props.onBranchesChange();
-  };
+export default function BranchLikeTabs(props: Props) {
+  const { component } = props;
+  const [currentTab, setCurrentTab] = useState<Tabs>(Tabs.Branch);
+  const [renaming, setRenaming] = useState<BranchLike>();
 
-  handleClose = () => {
-    this.setState({ deleting: undefined, renaming: undefined });
-  };
+  const [deleting, setDeleting] = useState<BranchLike>();
 
-  handleModalActionFulfilled = () => {
-    this.handleClose();
-    this.props.onBranchesChange();
+  const handleClose = () => {
+    setRenaming(undefined);
+    setDeleting(undefined);
   };
 
-  render() {
-    const { branchLikes, component } = this.props;
-    const { currentTab, deleting, renaming } = this.state;
-
-    const isBranchMode = currentTab === Tabs.Branch;
-    const branchLikesToDisplay: BranchLike[] = isBranchMode
-      ? sortBranches(branchLikes.filter(isBranch))
-      : sortPullRequests(branchLikes.filter(isPullRequest));
-    const title = translate(
-      isBranchMode
-        ? 'project_branch_pull_request.table.branch'
-        : 'project_branch_pull_request.table.pull_request'
-    );
-
-    return (
-      <>
-        <BoxedTabs
-          className="branch-like-tabs"
-          onSelect={this.handleTabSelect}
-          selected={currentTab}
-          tabs={TABS}
+  const { data: { branchLikes } = { branchLikes: [] } } = useBranchesQuery(component);
+
+  const isBranchMode = currentTab === Tabs.Branch;
+  const branchLikesToDisplay: BranchLike[] = isBranchMode
+    ? sortBranches(branchLikes.filter(isBranch) as Branch[])
+    : sortPullRequests(branchLikes.filter(isPullRequest) as PullRequest[]);
+  const title = translate(
+    isBranchMode
+      ? 'project_branch_pull_request.table.branch'
+      : 'project_branch_pull_request.table.pull_request'
+  );
+
+  return (
+    <>
+      <BoxedTabs
+        className="branch-like-tabs"
+        onSelect={setCurrentTab}
+        selected={currentTab}
+        tabs={TABS}
+      />
+
+      <div role="tabpanel" id={getTabPanelId(currentTab)} aria-labelledby={getTabId(currentTab)}>
+        <BranchLikeTable
+          branchLikes={branchLikesToDisplay}
+          component={component}
+          displayPurgeSetting={isBranchMode}
+          onDelete={setDeleting}
+          onRename={setRenaming}
+          title={title}
         />
+      </div>
 
-        <div role="tabpanel" id={getTabPanelId(currentTab)} aria-labelledby={getTabId(currentTab)}>
-          <BranchLikeTable
-            branchLikes={branchLikesToDisplay}
-            component={component}
-            displayPurgeSetting={isBranchMode}
-            onDelete={this.handleDeleteBranchLike}
-            onRename={this.handleRenameBranchLike}
-            onUpdatePurgeSetting={this.handleUpdatePurgeSetting}
-            title={title}
-          />
-        </div>
+      {deleting && (
+        <DeleteBranchModal branchLike={deleting} component={component} onClose={handleClose} />
+      )}
 
-        {deleting && (
-          <DeleteBranchModal
-            branchLike={deleting}
-            component={component}
-            onClose={this.handleClose}
-            onDelete={this.handleModalActionFulfilled}
-          />
-        )}
-
-        {renaming && isMainBranch(renaming) && (
-          <RenameBranchModal
-            branch={renaming}
-            component={component}
-            onClose={this.handleClose}
-            onRename={this.handleModalActionFulfilled}
-          />
-        )}
-      </>
-    );
-  }
+      {renaming && isMainBranch(renaming) && (
+        <RenameBranchModal branch={renaming} component={component} onClose={handleClose} />
+      )}
+    </>
+  );
 }
index f6d2551e50b28900bbf4dc3ad87f1946b441683d..c69b18521fc2fb93d8fd285eb05d580c34864402 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { excludeBranchFromPurge } from '../../../api/branches';
 import HelpTooltip from '../../../components/controls/HelpTooltip';
 import Toggle from '../../../components/controls/Toggle';
 import DeferredSpinner from '../../../components/ui/DeferredSpinner';
 import { isMainBranch } from '../../../helpers/branch-like';
 import { translate } from '../../../helpers/l10n';
+import { useExcludeFromPurgeMutation } from '../../../queries/branch';
 import { Branch } from '../../../types/branch-like';
 import { Component } from '../../../types/types';
 
 interface Props {
   branch: Branch;
   component: Component;
-  onUpdatePurgeSetting: () => void;
 }
 
-interface State {
-  excludedFromPurge: boolean;
-  loading: boolean;
-}
-
-export default class BranchPurgeSetting extends React.PureComponent<Props, State> {
-  mounted = false;
-
-  constructor(props: Props) {
-    super(props);
-
-    this.state = { excludedFromPurge: props.branch.excludedFromPurge, loading: false };
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-  }
+export default function BranchPurgeSetting(props: Props) {
+  const { branch, component } = props;
+  const { mutate: excludeFromPurge, isLoading } = useExcludeFromPurgeMutation();
 
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  handleOnChange = () => {
-    const { branch, component } = this.props;
-    const { excludedFromPurge } = this.state;
-    const newValue = !excludedFromPurge;
-
-    this.setState({ loading: true });
-
-    excludeBranchFromPurge(component.key, branch.name, newValue)
-      .then(() => {
-        if (this.mounted) {
-          this.setState({
-            excludedFromPurge: newValue,
-            loading: false,
-          });
-          this.props.onUpdatePurgeSetting();
-        }
-      })
-      .catch(() => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
-      });
+  const handleOnChange = (exclude: boolean) => {
+    excludeFromPurge({ component, key: branch.name, exclude });
   };
 
-  render() {
-    const { branch } = this.props;
-    const { excludedFromPurge, loading } = this.state;
-
-    const isTheMainBranch = isMainBranch(branch);
-    const disabled = isTheMainBranch || loading;
-
-    return (
-      <>
-        <Toggle disabled={disabled} onChange={this.handleOnChange} value={excludedFromPurge} />
-        <span className="spacer-left">
-          <DeferredSpinner loading={loading} />
-        </span>
-        {isTheMainBranch && (
-          <HelpTooltip
-            overlay={translate(
-              'project_branch_pull_request.branch.auto_deletion.main_branch_tooltip'
-            )}
-          />
-        )}
-      </>
-    );
-  }
+  const isTheMainBranch = isMainBranch(branch);
+  const disabled = isTheMainBranch || isLoading;
+
+  return (
+    <>
+      <Toggle disabled={disabled} onChange={handleOnChange} value={branch.excludedFromPurge} />
+      <span className="spacer-left">
+        <DeferredSpinner loading={isLoading} />
+      </span>
+      {isTheMainBranch && (
+        <HelpTooltip
+          overlay={translate(
+            'project_branch_pull_request.branch.auto_deletion.main_branch_tooltip'
+          )}
+        />
+      )}
+    </>
+  );
 }
index 9d7d2d422ae89ae16a1a4f94b5a356adb6a51577..c11ac8febe1ea19c40ff1a75b35976a2b9b4a295 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { deleteBranch, deletePullRequest } from '../../../api/branches';
-import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
 import Modal from '../../../components/controls/Modal';
+import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
 import { getBranchLikeDisplayName, isPullRequest } from '../../../helpers/branch-like';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { useDeletBranchMutation } from '../../../queries/branch';
 import { BranchLike } from '../../../types/branch-like';
 import { Component } from '../../../types/types';
 
@@ -30,83 +30,50 @@ interface Props {
   branchLike: BranchLike;
   component: Component;
   onClose: () => void;
-  onDelete: () => void;
-}
-
-interface State {
-  loading: boolean;
 }
 
-export default class DeleteBranchModal extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = { loading: false };
+export default function DeleteBranchModal(props: Props) {
+  const { branchLike, component } = props;
+  const { mutate: deleteBranch, isLoading } = useDeletBranchMutation();
 
-  componentDidMount() {
-    this.mounted = true;
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
+  const handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
     event.preventDefault();
-    this.setState({ loading: true });
-    const request = isPullRequest(this.props.branchLike)
-      ? deletePullRequest({
-          project: this.props.component.key,
-          pullRequest: this.props.branchLike.key,
-        })
-      : deleteBranch({
-          branch: this.props.branchLike.name,
-          project: this.props.component.key,
-        });
-    request.then(
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-          this.props.onDelete();
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
+    deleteBranch(
+      { component, branchLike },
+      {
+        onSuccess: props.onClose,
       }
     );
   };
 
-  render() {
-    const { branchLike } = this.props;
-    const header = translate(
-      isPullRequest(branchLike)
-        ? 'project_branch_pull_request.pull_request.delete'
-        : 'project_branch_pull_request.branch.delete'
-    );
+  const header = translate(
+    isPullRequest(branchLike)
+      ? 'project_branch_pull_request.pull_request.delete'
+      : 'project_branch_pull_request.branch.delete'
+  );
 
-    return (
-      <Modal contentLabel={header} onRequestClose={this.props.onClose}>
-        <header className="modal-head">
-          <h2>{header}</h2>
-        </header>
-        <form onSubmit={this.handleSubmit}>
-          <div className="modal-body">
-            {translateWithParameters(
-              isPullRequest(branchLike)
-                ? 'project_branch_pull_request.pull_request.delete.are_you_sure'
-                : 'project_branch_pull_request.branch.delete.are_you_sure',
-              getBranchLikeDisplayName(branchLike)
-            )}
-          </div>
-          <footer className="modal-foot">
-            {this.state.loading && <i className="spinner spacer-right" />}
-            <SubmitButton className="button-red" disabled={this.state.loading}>
-              {translate('delete')}
-            </SubmitButton>
-            <ResetButtonLink onClick={this.props.onClose}>{translate('cancel')}</ResetButtonLink>
-          </footer>
-        </form>
-      </Modal>
-    );
-  }
+  return (
+    <Modal contentLabel={header} onRequestClose={props.onClose}>
+      <header className="modal-head">
+        <h2>{header}</h2>
+      </header>
+      <form onSubmit={handleSubmit}>
+        <div className="modal-body">
+          {translateWithParameters(
+            isPullRequest(branchLike)
+              ? 'project_branch_pull_request.pull_request.delete.are_you_sure'
+              : 'project_branch_pull_request.branch.delete.are_you_sure',
+            getBranchLikeDisplayName(branchLike)
+          )}
+        </div>
+        <footer className="modal-foot">
+          {isLoading && <i className="spinner spacer-right" />}
+          <SubmitButton className="button-red" disabled={isLoading}>
+            {translate('delete')}
+          </SubmitButton>
+          <ResetButtonLink onClick={props.onClose}>{translate('cancel')}</ResetButtonLink>
+        </footer>
+      </form>
+    </Modal>
+  );
 }
index 3b8580ce24ed6c3830ce1e76bffe38b89aca3eb0..d92b0794d424538a417c219a393d15554abab654 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { renameBranch } from '../../../api/branches';
-import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
+import { useState } from 'react';
 import Modal from '../../../components/controls/Modal';
+import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
 import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
 import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
 import { translate } from '../../../helpers/l10n';
+import { useRenameMainBranchMutation } from '../../../queries/branch';
 import { MainBranch } from '../../../types/branch-like';
 import { Component } from '../../../types/types';
 
@@ -31,90 +32,62 @@ interface Props {
   branch: MainBranch;
   component: Component;
   onClose: () => void;
-  onRename: () => void;
-}
-
-interface State {
-  loading: boolean;
-  name?: string;
 }
 
-export default class RenameBranchModal extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = { loading: false };
-
-  componentDidMount() {
-    this.mounted = true;
-  }
+export default function RenameBranchModal(props: Props) {
+  const { branch, component } = props;
+  const [name, setName] = useState<string>();
 
-  componentWillUnmount() {
-    this.mounted = false;
-  }
+  const { mutate: renameMainBranch, isLoading } = useRenameMainBranchMutation();
 
-  handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
+  const handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
     event.preventDefault();
-    if (!this.state.name) {
+    if (!name) {
       return;
     }
-    this.setState({ loading: true });
-    renameBranch(this.props.component.key, this.state.name).then(
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-          this.props.onRename();
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
-      }
-    );
+
+    renameMainBranch({ component, name }, { onSuccess: props.onClose });
   };
 
-  handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
-    this.setState({ name: event.currentTarget.value });
+  const handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
+    setName(event.currentTarget.value);
   };
 
-  render() {
-    const { branch } = this.props;
-    const header = translate('project_branch_pull_request.branch.rename');
-    const submitDisabled =
-      this.state.loading || !this.state.name || this.state.name === branch.name;
+  const header = translate('project_branch_pull_request.branch.rename');
+  const submitDisabled = isLoading || !name || name === branch.name;
 
-    return (
-      <Modal contentLabel={header} onRequestClose={this.props.onClose} size="small">
-        <header className="modal-head">
-          <h2>{header}</h2>
-        </header>
-        <form onSubmit={this.handleSubmit}>
-          <div className="modal-body">
-            <MandatoryFieldsExplanation className="modal-field" />
-            <div className="modal-field">
-              <label htmlFor="rename-branch-name">
-                {translate('new_name')}
-                <MandatoryFieldMarker />
-              </label>
-              <input
-                autoFocus
-                id="rename-branch-name"
-                maxLength={100}
-                name="name"
-                onChange={this.handleNameChange}
-                required
-                size={50}
-                type="text"
-                value={this.state.name !== undefined ? this.state.name : branch.name}
-              />
-            </div>
+  return (
+    <Modal contentLabel={header} onRequestClose={props.onClose} size="small">
+      <header className="modal-head">
+        <h2>{header}</h2>
+      </header>
+      <form onSubmit={handleSubmit}>
+        <div className="modal-body">
+          <MandatoryFieldsExplanation className="modal-field" />
+          <div className="modal-field">
+            <label htmlFor="rename-branch-name">
+              {translate('new_name')}
+              <MandatoryFieldMarker />
+            </label>
+            <input
+              autoFocus
+              id="rename-branch-name"
+              maxLength={100}
+              name="name"
+              onChange={handleNameChange}
+              required
+              size={50}
+              type="text"
+              value={name ?? branch.name}
+            />
           </div>
-          <footer className="modal-foot">
-            {this.state.loading && <i className="spinner spacer-right" />}
-            <SubmitButton disabled={submitDisabled}>{translate('rename')}</SubmitButton>
-            <ResetButtonLink onClick={this.props.onClose}>{translate('cancel')}</ResetButtonLink>
-          </footer>
-        </form>
-      </Modal>
-    );
-  }
+        </div>
+        <footer className="modal-foot">
+          {isLoading && <i className="spinner spacer-right" />}
+          <SubmitButton disabled={submitDisabled}>{translate('rename')}</SubmitButton>
+          <ResetButtonLink onClick={props.onClose}>{translate('cancel')}</ResetButtonLink>
+        </footer>
+      </form>
+    </Modal>
+  );
 }
index 5a1798e26a1ba854dda7dfbc8c71e7bc8a05dbdf..bbb45b3b947372647708843633c42ec8d09f9959 100644 (file)
@@ -21,7 +21,7 @@ import { screen } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import * as React from 'react';
 import { renderComponent } from '../../../../helpers/testReactTestingUtils';
-import { FCProps } from '../../../../helpers/testUtils';
+import { ComponentPropsType } from '../../../../helpers/testUtils';
 import CoverageFilter from '../CoverageFilter';
 
 it('renders options', () => {
@@ -47,7 +47,7 @@ it('updates the filter query', async () => {
   expect(onQueryChange).toHaveBeenCalledWith({ coverage: '3' });
 });
 
-function renderCoverageFilter(props: Partial<FCProps<typeof CoverageFilter>> = {}) {
+function renderCoverageFilter(props: Partial<ComponentPropsType<typeof CoverageFilter>> = {}) {
   renderComponent(
     <CoverageFilter
       maxFacetValue={9}
index 80fdaf6143ac64348aa6b909adac8b6df1bb8ec8..a8f55537c867e3e3cd9ef83701282aafb6e6dd49 100644 (file)
@@ -21,7 +21,7 @@ import { screen } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import * as React from 'react';
 import { renderComponent } from '../../../../helpers/testReactTestingUtils';
-import { FCProps } from '../../../../helpers/testUtils';
+import { ComponentPropsType } from '../../../../helpers/testUtils';
 import { LanguagesFilter } from '../LanguagesFilter';
 
 it('renders language names', () => {
@@ -61,7 +61,7 @@ it('updates the filter query', async () => {
   expect(onQueryChange).toHaveBeenCalledWith({ languages: 'java' });
 });
 
-function renderLanguagesFilter(props: Partial<FCProps<typeof LanguagesFilter>> = {}) {
+function renderLanguagesFilter(props: Partial<ComponentPropsType<typeof LanguagesFilter>> = {}) {
   renderComponent(
     <LanguagesFilter
       languages={{
index 699da7601bf681aa217d5cbaebf57d768046b262..c21ab619191e377fdb5a912e8f071831e5ffee81 100644 (file)
@@ -21,7 +21,7 @@ import { screen } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import * as React from 'react';
 import { renderComponent } from '../../../../helpers/testReactTestingUtils';
-import { FCProps } from '../../../../helpers/testUtils';
+import { ComponentPropsType } from '../../../../helpers/testUtils';
 import QualityGateFacet from '../QualityGateFilter';
 
 it('renders options', () => {
@@ -57,7 +57,7 @@ it('handles multiselection', async () => {
   expect(onQueryChange).toHaveBeenCalledWith({ gate: 'OK,ERROR' });
 });
 
-function renderQualityGateFilter(props: Partial<FCProps<typeof QualityGateFacet>> = {}) {
+function renderQualityGateFilter(props: Partial<ComponentPropsType<typeof QualityGateFacet>> = {}) {
   renderComponent(
     <QualityGateFacet
       maxFacetValue={9}
index 0b5d3eae408f1c55f305eb53b4639393ecd7e922..3d503e44137908a6ceaf178d24ef4b221e776946 100644 (file)
@@ -21,7 +21,6 @@ import { flatMap, range } from 'lodash';
 import * as React from 'react';
 import { getMeasures } from '../../api/measures';
 import { getSecurityHotspotList, getSecurityHotspots } from '../../api/security-hotspots';
-import withBranchStatusActions from '../../app/components/branch-status/withBranchStatusActions';
 import withComponentContext from '../../app/components/componentContext/withComponentContext';
 import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext';
 import { Location, Router, withRouter } from '../../components/hoc/withRouter';
@@ -30,6 +29,7 @@ import { getBranchLikeQuery, isPullRequest, isSameBranchLike } from '../../helpe
 import { isInput } from '../../helpers/keyboardEventHelpers';
 import { KeyboardKeys } from '../../helpers/keycodes';
 import { getStandards } from '../../helpers/security-standard';
+import { withBranchLikes } from '../../queries/branch';
 import { BranchLike } from '../../types/branch-like';
 import { SecurityStandard, Standards } from '../../types/security';
 import {
@@ -46,11 +46,8 @@ import './styles.css';
 import { SECURITY_STANDARDS, getLocations } from './utils';
 
 const PAGE_SIZE = 500;
-interface DispatchProps {
-  fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => void;
-}
 
-interface OwnProps {
+interface Props {
   branchLike?: BranchLike;
   currentUser: CurrentUser;
   component: Component;
@@ -58,8 +55,6 @@ interface OwnProps {
   router: Router;
 }
 
-type Props = DispatchProps & OwnProps;
-
 interface State {
   filterByCategory?: { standard: SecurityStandard; category: string };
   filterByCWE?: string;
@@ -117,6 +112,7 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
 
   componentDidUpdate(previous: Props) {
     if (
+      !isSameBranchLike(this.props.branchLike, previous.branchLike) ||
       this.props.component.key !== previous.component.key ||
       this.props.location.query.hotspots !== previous.location.query.hotspots ||
       SECURITY_STANDARDS.some((s) => this.props.location.query[s] !== previous.location.query[s]) ||
@@ -434,13 +430,8 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
 
   handleHotspotUpdate = (hotspotKey: string) => {
     const { hotspots, hotspotsPageIndex } = this.state;
-    const { branchLike, component } = this.props;
     const index = hotspots.findIndex((h) => h.key === hotspotKey);
 
-    if (isPullRequest(branchLike)) {
-      this.props.fetchBranchStatus(branchLike, component.key);
-    }
-
     return Promise.all(
       range(hotspotsPageIndex).map((p) =>
         this.fetchSecurityHotspots(p + 1 /* pages are 1-indexed */)
@@ -550,5 +541,5 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
 }
 
 export default withRouter(
-  withComponentContext(withCurrentUserContext(withBranchStatusActions(SecurityHotspotsApp)))
+  withComponentContext(withCurrentUserContext(withBranchLikes(SecurityHotspotsApp)))
 );
index 69f8f057701475ef205d7b6d823027a8a02e0137..3806173420e8b048bdd07e56ee7c125540edb96a 100644 (file)
@@ -21,11 +21,11 @@ import { act, screen } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import React from 'react';
 import { Route } from 'react-router-dom';
+import BranchesServiceMock from '../../../api/mocks/BranchesServiceMock';
 import CodingRulesServiceMock from '../../../api/mocks/CodingRulesServiceMock';
 import SecurityHotspotServiceMock from '../../../api/mocks/SecurityHotspotServiceMock';
 import { getSecurityHotspots, setSecurityHotspotStatus } from '../../../api/security-hotspots';
 import { searchUsers } from '../../../api/users';
-import { mockBranch, mockMainBranch } from '../../../helpers/mocks/branch-like';
 import { mockComponent } from '../../../helpers/mocks/component';
 import { openHotspot, probeSonarLintServers } from '../../../helpers/sonarlint';
 import { get, save } from '../../../helpers/storage';
@@ -107,6 +107,7 @@ const ui = {
 const originalScrollTo = window.scrollTo;
 const hotspotsHandler = new SecurityHotspotServiceMock();
 const rulesHandles = new CodingRulesServiceMock();
+const branchHandler = new BranchesServiceMock();
 let showDialog = 'true';
 
 jest.mocked(save).mockImplementation((_key: string, value?: string) => {
@@ -143,6 +144,7 @@ beforeEach(() => {
 afterEach(() => {
   hotspotsHandler.reset();
   rulesHandles.reset();
+  branchHandler.reset();
 });
 
 describe('rendering', () => {
@@ -309,6 +311,7 @@ describe('navigation', () => {
     const user = userEvent.setup();
     renderSecurityHotspotsApp();
 
+    expect(await ui.hotspotTitle(/'3' is a magic number./).find()).toBeInTheDocument();
     await user.keyboard('{ArrowDown}');
     expect(await ui.hotspotTitle(/'2' is a magic number./).find()).toBeInTheDocument();
     await user.keyboard('{ArrowUp}');
@@ -343,16 +346,13 @@ describe('navigation', () => {
     const rtl = renderSecurityHotspotsApp(
       'security_hotspots?id=guillaume-peoch-sonarsource_benflix_AYGpXq2bd8qy4i0eO9ed&hotspots=test-1'
     );
-
     expect(await ui.hotspotTitle(/'3' is a magic number./).find()).toBeInTheDocument();
 
     // On specific branch
     rtl.unmount();
     renderSecurityHotspotsApp(
-      'security_hotspots?id=guillaume-peoch-sonarsource_benflix_AYGpXq2bd8qy4i0eO9ed&hotspots=b1-test-1&branch=b1',
-      { branchLike: mockBranch({ name: 'b1' }) }
+      'security_hotspots?id=guillaume-peoch-sonarsource_benflix_AYGpXq2bd8qy4i0eO9ed&hotspots=b1-test-1&branch=normal-branch'
     );
-
     expect(await ui.hotspotTitle(/'F' is a magic number./).find()).toBeInTheDocument();
   });
 
@@ -417,7 +417,7 @@ it('should be able to filter the hotspot list', async () => {
 
   await user.click(ui.filterDropdown.get());
   await user.click(ui.filterAssigneeToMe.get());
-  expect(ui.noHotspotForFilter.get()).toBeInTheDocument();
+  expect(await ui.noHotspotForFilter.find()).toBeInTheDocument();
 
   await user.click(ui.filterToReview.get());
 
@@ -432,7 +432,7 @@ it('should be able to filter the hotspot list', async () => {
   });
 
   await user.click(ui.filterDropdown.get());
-  await user.click(ui.filterNewCode.get());
+  await user.click(await ui.filterNewCode.find());
 
   expect(getSecurityHotspots).toHaveBeenLastCalledWith({
     inNewCodePeriod: true,
@@ -458,15 +458,15 @@ function renderSecurityHotspotsApp(
     'security_hotspots',
     () => <Route path="security_hotspots" element={<SecurityHotspotsApp />} />,
     {
-      navigateTo,
+      navigateTo:
+        navigateTo ??
+        'security_hotspots?id=guillaume-peoch-sonarsource_benflix_AYGpXq2bd8qy4i0eO9ed',
       currentUser: mockLoggedInUser({
         login: 'foo',
         name: 'foo',
       }),
     },
     {
-      branchLike: mockMainBranch(),
-      onBranchesChange: jest.fn(),
       onComponentChange: jest.fn(),
       component: mockComponent({
         key: 'guillaume-peoch-sonarsource_benflix_AYGpXq2bd8qy4i0eO9ed',
index 55ec92e43909d8539a8d9e2a46bec257eacb63c0..ba7c40d5266cbc5edf79647bb69326c1d0ca2f79 100644 (file)
@@ -41,6 +41,7 @@ import {
   getPathUrlAsString,
   getRuleUrl,
 } from '../../../helpers/urls';
+import { useRefreshBranchStatus } from '../../../queries/branch';
 import { BranchLike } from '../../../types/branch-like';
 import { SecurityStandard, Standards } from '../../../types/security';
 import { Hotspot, HotspotStatusOption } from '../../../types/security-hotspots';
@@ -68,6 +69,7 @@ interface StyledHeaderProps {
 export function HotspotHeader(props: HotspotHeaderProps) {
   const { hotspot, component, branchLike, standards, tabs, isCompressed, isScrolled } = props;
   const { message, messageFormattings, rule, key } = hotspot;
+  const refrechBranchStatus = useRefreshBranchStatus();
 
   const permalink = getPathUrlAsString(
     getComponentSecurityHotspotsUrl(component.key, {
@@ -78,14 +80,15 @@ export function HotspotHeader(props: HotspotHeaderProps) {
   );
 
   const categoryStandard = standards?.[SecurityStandard.SONARSOURCE][rule.securityCategory]?.title;
+  const handleStatusChange = async (statusOption: HotspotStatusOption) => {
+    await props.onUpdateHotspot(true, statusOption);
+    refrechBranchStatus();
+  };
 
   const content = isCompressed ? (
     <div className="sw-flex sw-justify-between">
       {tabs}
-      <StatusReviewButton
-        hotspot={hotspot}
-        onStatusChange={(statusOption) => props.onUpdateHotspot(true, statusOption)}
-      />
+      <StatusReviewButton hotspot={hotspot} onStatusChange={handleStatusChange} />
     </div>
   ) : (
     <>
@@ -110,10 +113,7 @@ export function HotspotHeader(props: HotspotHeaderProps) {
               {rule.key}
             </Link>
           </div>
-          <Status
-            hotspot={hotspot}
-            onStatusChange={(statusOption) => props.onUpdateHotspot(true, statusOption)}
-          />
+          <Status hotspot={hotspot} onStatusChange={handleStatusChange} />
         </div>
         <div className="sw-flex sw-flex-col sw-gap-4">
           <HotspotHeaderRightSection
index 1f2868d28704e7c8bc0b9b0ef5623ad2863a3dee..cba93f8f92560419ebac2466870b54bc1318060e 100644 (file)
@@ -77,7 +77,6 @@ export interface Props {
   highlightedLocationMessage?: { index: number; text: string | undefined };
   onLoaded?: (component: SourceViewerFile, sources: SourceLine[], issues: Issue[]) => void;
   onLocationSelect?: (index: number) => void;
-  onIssueChange?: (issue: Issue) => void;
   onIssueSelect?: (issueKey: string) => void;
   onIssueUnselect?: () => void;
   selectedIssue?: string;
@@ -466,9 +465,6 @@ export default class SourceViewer extends React.PureComponent<Props, State> {
       );
       return { issues: newIssues, issuesByLine: issuesByLine(newIssues) };
     });
-    if (this.props.onIssueChange) {
-      this.props.onIssueChange(issue);
-    }
   };
 
   renderDuplicationPopup = (index: number, line: number) => {
index e067668bf1eb7ecdddb467612467a920b0f777ef..39989709266d7dbd63512eeb06ff4effff04289c 100644 (file)
@@ -370,7 +370,6 @@ function renderSourceViewer(override?: Partial<SourceViewer['props']>) {
       component={componentsHandler.getNonEmptyFileKey()}
       displayAllIssues
       displayLocationMarkers
-      onIssueChange={jest.fn()}
       onIssueSelect={jest.fn()}
       onLoaded={jest.fn()}
       onLocationSelect={jest.fn()}
@@ -385,7 +384,6 @@ function renderSourceViewer(override?: Partial<SourceViewer['props']>) {
         component={componentsHandler.getNonEmptyFileKey()}
         displayAllIssues
         displayLocationMarkers
-        onIssueChange={jest.fn()}
         onIssueSelect={jest.fn()}
         onLoaded={jest.fn()}
         onLocationSelect={jest.fn()}
index 31996b65f5a0c152ad9705fd591544a860c95420..fa87946a7835326e0879e6959194b066478da8cc 100644 (file)
@@ -21,6 +21,7 @@ import { Note } from 'design-system';
 import * as React from 'react';
 import { ComponentContext } from '../../app/components/componentContext/ComponentContext';
 import { translate } from '../../helpers/l10n';
+import { useBranchesQuery } from '../../queries/branch';
 import { AnalysisEvent } from '../../types/project-activity';
 import Tooltip from '../controls/Tooltip';
 import { DefinitionChangeEventInner, isDefinitionChangeEvent } from './DefinitionChangeEventInner';
@@ -32,16 +33,12 @@ export interface EventInnerProps {
 }
 
 export default function EventInner({ event, readonly }: EventInnerProps) {
+  const { component } = React.useContext(ComponentContext);
+  const { data: { branchLike } = {} } = useBranchesQuery(component);
   if (isRichQualityGateEvent(event)) {
     return <RichQualityGateEventInner event={event} readonly={readonly} />;
   } else if (isDefinitionChangeEvent(event)) {
-    return (
-      <ComponentContext.Consumer>
-        {({ branchLike }) => (
-          <DefinitionChangeEventInner branchLike={branchLike} event={event} readonly={readonly} />
-        )}
-      </ComponentContext.Consumer>
-    );
+    return <DefinitionChangeEventInner branchLike={branchLike} event={event} readonly={readonly} />;
   }
   return (
     <Tooltip overlay={event.description}>
index 79cdb85ca3bc7a07812b091220e1094820a300e5..8f1d75b645df1f5fd0ba1b3d93b91904c497f3f8 100644 (file)
@@ -26,7 +26,7 @@ import { mockHistoryItem, mockMeasureHistory } from '../../../helpers/mocks/proj
 import { mockMetric } from '../../../helpers/testMocks';
 import { renderComponent } from '../../../helpers/testReactTestingUtils';
 import { byLabelText, byPlaceholderText, byRole, byText } from '../../../helpers/testSelector';
-import { FCProps } from '../../../helpers/testUtils';
+import { ComponentPropsType } from '../../../helpers/testUtils';
 import { MetricKey } from '../../../types/metrics';
 import { GraphType, MeasureHistory } from '../../../types/project-activity';
 import { Metric } from '../../../types/types';
@@ -238,7 +238,7 @@ function getPageObject() {
 
 function renderActivityGraph(
   graphsHistoryProps: Partial<GraphsHistory['props']> = {},
-  graphsHeaderProps: Partial<FCProps<typeof GraphsHeader>> = {}
+  graphsHeaderProps: Partial<ComponentPropsType<typeof GraphsHeader>> = {}
 ) {
   function ActivityGraph() {
     const [selectedMetrics, setSelectedMetrics] = React.useState<string[]>([]);
index 46540d728c75ab3e786a64efe2eda443cbd9070a..3ba9228a84ab29c105d9360ff843337e9e11cb32 100644 (file)
@@ -21,13 +21,13 @@ import { screen } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import * as React from 'react';
 import { Route } from 'react-router-dom';
+import BranchesServiceMock from '../../../api/mocks/BranchesServiceMock';
 import { isMainBranch } from '../../../helpers/branch-like';
 import { mockBranch, mockMainBranch } from '../../../helpers/mocks/branch-like';
 import { mockAnalysisEvent } from '../../../helpers/mocks/project-activity';
 import { renderAppWithComponentContext } from '../../../helpers/testReactTestingUtils';
 import { byRole, byText } from '../../../helpers/testSelector';
-import { BranchLike } from '../../../types/branch-like';
-import { ComponentContextShape } from '../../../types/component';
+import { Branch, BranchLike } from '../../../types/branch-like';
 import {
   ApplicationAnalysisEventCategory,
   DefinitionChangeType,
@@ -43,8 +43,8 @@ const ui = {
   definitionChangeLabel: byText('event.category.DEFINITION_CHANGE', { exact: false }),
   projectAddedTxt: (branch: BranchLike) =>
     isMainBranch(branch)
-      ? byText('event.definition_change.added')
-      : byText('event.definition_change.branch_added'),
+      ? byText(/event\.definition_change\.added/)
+      : byText(/event\.definition_change\.branch_added/),
   projectRemovedTxt: (branch: BranchLike) =>
     isMainBranch(branch)
       ? byText('event.definition_change.removed')
@@ -57,10 +57,17 @@ const ui = {
   versionLabel: byText('event.category.VERSION', { exact: false }),
 };
 
+const handler = new BranchesServiceMock();
+
+beforeEach(() => {
+  handler.reset();
+});
+
 describe('DEFINITION_CHANGE events', () => {
   it.each([mockMainBranch(), mockBranch()])(
     'should render correctly for "ADDED" events',
-    async (branchLike: BranchLike) => {
+    async (branchLike: Branch) => {
+      handler.addBranch(branchLike);
       const user = userEvent.setup();
       renderEventInner(
         {
@@ -78,14 +85,14 @@ describe('DEFINITION_CHANGE events', () => {
             },
           }),
         },
-        { branchLike }
+        `branch=${branchLike.name}&id=my-project`
       );
 
-      expect(ui.definitionChangeLabel.get()).toBeInTheDocument();
+      expect(await ui.definitionChangeLabel.find()).toBeInTheDocument();
 
       await user.click(ui.showMoreBtn.get());
 
-      expect(ui.projectAddedTxt(branchLike).get()).toBeInTheDocument();
+      expect(await ui.projectAddedTxt(branchLike).find()).toBeInTheDocument();
       expect(ui.projectLink('Foo').get()).toBeInTheDocument();
       expect(screen.getByText('master-foo')).toBeInTheDocument();
     }
@@ -93,8 +100,9 @@ describe('DEFINITION_CHANGE events', () => {
 
   it.each([mockMainBranch(), mockBranch()])(
     'should render correctly for "REMOVED" events',
-    async (branchLike: BranchLike) => {
+    async (branchLike: Branch) => {
       const user = userEvent.setup();
+      handler.addBranch(branchLike);
       renderEventInner(
         {
           event: mockAnalysisEvent({
@@ -111,14 +119,14 @@ describe('DEFINITION_CHANGE events', () => {
             },
           }),
         },
-        { branchLike }
+        `branch=${branchLike.name}&id=my-project`
       );
 
       expect(ui.definitionChangeLabel.get()).toBeInTheDocument();
 
       await user.click(ui.showMoreBtn.get());
 
-      expect(ui.projectRemovedTxt(branchLike).get()).toBeInTheDocument();
+      expect(await ui.projectRemovedTxt(branchLike).find()).toBeInTheDocument();
       expect(ui.projectLink('Bar').get()).toBeInTheDocument();
       expect(screen.getByText('master-bar')).toBeInTheDocument();
     }
@@ -228,14 +236,10 @@ describe('VERSION events', () => {
   });
 });
 
-function renderEventInner(
-  props: Partial<EventInnerProps> = {},
-  componentContext: Partial<ComponentContextShape> = {}
-) {
+function renderEventInner(props: Partial<EventInnerProps> = {}, params?: string) {
   return renderAppWithComponentContext(
     '/',
     () => <Route path="*" element={<EventInner event={mockAnalysisEvent()} {...props} />} />,
-    {},
-    componentContext
+    { navigateTo: params ? `/?id=my-project&${params}` : '/?id=my-project' }
   );
 }
diff --git a/server/sonar-web/src/main/js/components/common/AnalysisWarningsModal.tsx b/server/sonar-web/src/main/js/components/common/AnalysisWarningsModal.tsx
deleted file mode 100644 (file)
index 5ec98f9..0000000
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * 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 {
-  DangerButtonSecondary,
-  DeferredSpinner,
-  FlagMessage,
-  HtmlFormatter,
-  Modal,
-} from 'design-system';
-import * as React from 'react';
-import { dismissAnalysisWarning, getTask } from '../../api/ce';
-import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext';
-import { translate } from '../../helpers/l10n';
-import { sanitizeStringRestricted } from '../../helpers/sanitize';
-import { TaskWarning } from '../../types/tasks';
-import { CurrentUser } from '../../types/users';
-
-interface Props {
-  componentKey?: string;
-  currentUser: CurrentUser;
-  onClose: () => void;
-  onWarningDismiss?: () => void;
-  taskId?: string;
-  warnings?: TaskWarning[];
-}
-
-interface State {
-  loading: boolean;
-  dismissedWarning?: string;
-  warnings: TaskWarning[];
-}
-
-export class AnalysisWarningsModal extends React.PureComponent<Props, State> {
-  mounted = false;
-
-  constructor(props: Props) {
-    super(props);
-    this.state = {
-      loading: !props.warnings,
-      warnings: props.warnings || [],
-    };
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-    if (!this.props.warnings && this.props.taskId) {
-      this.loadWarnings(this.props.taskId);
-    }
-  }
-
-  componentDidUpdate(prevProps: Props) {
-    const { taskId, warnings } = this.props;
-    if (!warnings && taskId && prevProps.taskId !== taskId) {
-      this.loadWarnings(taskId);
-    } else if (warnings && prevProps.warnings !== warnings) {
-      this.setState({ warnings });
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  handleDismissMessage = async (messageKey: string) => {
-    const { componentKey } = this.props;
-
-    if (componentKey === undefined) {
-      return;
-    }
-
-    this.setState({ dismissedWarning: messageKey });
-
-    try {
-      await dismissAnalysisWarning(componentKey, messageKey);
-
-      if (this.props.onWarningDismiss) {
-        this.props.onWarningDismiss();
-      }
-    } catch (e) {
-      // Noop
-    }
-
-    if (this.mounted) {
-      this.setState({ dismissedWarning: undefined });
-    }
-  };
-
-  loadWarnings = async (taskId: string) => {
-    this.setState({ loading: true });
-    try {
-      const { warnings = [] } = await getTask(taskId, ['warnings']);
-
-      if (this.mounted) {
-        this.setState({
-          loading: false,
-          warnings: warnings.map((w) => ({ key: w, message: w, dismissable: false })),
-        });
-      }
-    } catch (e) {
-      if (this.mounted) {
-        this.setState({ loading: false });
-      }
-    }
-  };
-
-  render() {
-    const { currentUser } = this.props;
-    const { loading, dismissedWarning, warnings } = this.state;
-
-    const header = translate('warnings');
-
-    const body = (
-      <DeferredSpinner loading={loading}>
-        {warnings.map(({ dismissable, key, message }) => (
-          <React.Fragment key={key}>
-            <div className="sw-flex sw-items-center sw-mt-2">
-              <FlagMessage variant="warning">
-                <HtmlFormatter>
-                  <span
-                    // eslint-disable-next-line react/no-danger
-                    dangerouslySetInnerHTML={{
-                      __html: sanitizeStringRestricted(message.trim().replace(/\n/g, '<br />')),
-                    }}
-                  />
-                </HtmlFormatter>
-              </FlagMessage>
-            </div>
-            <div>
-              {dismissable && currentUser.isLoggedIn && (
-                <div className="sw-mt-4">
-                  <DangerButtonSecondary
-                    disabled={Boolean(dismissedWarning)}
-                    onClick={() => {
-                      this.handleDismissMessage(key);
-                    }}
-                  >
-                    {translate('dismiss_permanently')}
-                  </DangerButtonSecondary>
-
-                  <DeferredSpinner className="sw-ml-2" loading={dismissedWarning === key} />
-                </div>
-              )}
-            </div>
-          </React.Fragment>
-        ))}
-      </DeferredSpinner>
-    );
-
-    return (
-      <Modal
-        headerTitle={header}
-        onClose={this.props.onClose}
-        body={body}
-        primaryButton={null}
-        secondaryButtonLabel={translate('close')}
-      />
-    );
-  }
-}
-
-export default withCurrentUserContext(AnalysisWarningsModal);
index de5ef2406d073bad53aed8664df1b61667ba41dc..016a54b3447f829e01f503472f223977289fac15 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import withBranchStatus from '../../app/components/branch-status/withBranchStatus';
 import Level from '../../components/ui/Level';
-import { BranchStatusData } from '../../types/branch-like';
+import { BranchLike } from '../../types/branch-like';
 
-export type BranchStatusProps = Pick<BranchStatusData, 'status'>;
+export interface BranchStatusProps {
+  branchLike: BranchLike;
+}
 
-export function BranchStatus(props: BranchStatusProps) {
-  const { status } = props;
+export default function BranchStatus(props: BranchStatusProps) {
+  const { branchLike } = props;
 
-  if (!status) {
+  if (!branchLike.status) {
     return null;
   }
 
-  return <Level level={status} small />;
+  return <Level level={branchLike.status.qualityGateStatus} small />;
 }
-
-export default withBranchStatus(BranchStatus);
index 28bb2f1ddf8ef19d8712fc69f3ef5996a2f1fb9b..ae64ae53f60c14924a7dc4ee821ece5c14e6c7a4 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
-import { screen, waitFor } from '@testing-library/react';
+import { screen } from '@testing-library/react';
 import * as React from 'react';
-import { getTask } from '../../../api/ce';
+import { AnalysisWarningsModal } from '../../../app/components/nav/component/AnalysisWarningsModal';
+import { mockComponent } from '../../../helpers/mocks/component';
 import { mockTaskWarning } from '../../../helpers/mocks/tasks';
 import { mockCurrentUser } from '../../../helpers/testMocks';
 import { renderComponent } from '../../../helpers/testReactTestingUtils';
-import { AnalysisWarningsModal } from '../AnalysisWarningsModal';
+import { ComponentPropsType } from '../../../helpers/testUtils';
 
 jest.mock('../../../api/ce', () => ({
   dismissAnalysisWarning: jest.fn().mockResolvedValue(null),
@@ -60,26 +61,12 @@ describe('should render correctly', () => {
   });
 });
 
-it('should not fetch task warnings if it does not have to', () => {
-  renderAnalysisWarningsModal();
-
-  expect(getTask).not.toHaveBeenCalled();
-});
-
-it('should fetch task warnings if it has to', async () => {
-  renderAnalysisWarningsModal({ taskId: 'abcd1234', warnings: undefined });
-
-  expect(screen.queryByText('message foo')).not.toBeInTheDocument();
-  expect(getTask).toHaveBeenCalledWith('abcd1234', ['warnings']);
-
-  await waitFor(() => {
-    expect(screen.getByText('message foo')).toBeInTheDocument();
-  });
-});
-
-function renderAnalysisWarningsModal(props: Partial<AnalysisWarningsModal['props']> = {}) {
+function renderAnalysisWarningsModal(
+  props: Partial<ComponentPropsType<typeof AnalysisWarningsModal>> = {}
+) {
   return renderComponent(
     <AnalysisWarningsModal
+      component={mockComponent()}
       currentUser={mockCurrentUser({ isLoggedIn: true })}
       onClose={jest.fn()}
       warnings={[
index 886fd31dfbcf53948b237bcb8adf102a58fda00b..d2c1b766aee106a91d2f2e57bab4d6d355ee09a5 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 { shallow } from 'enzyme';
+import { screen } from '@testing-library/react';
 import * as React from 'react';
-import { BranchStatus, BranchStatusProps } from '../BranchStatus';
+import BranchesServiceMock from '../../../api/mocks/BranchesServiceMock';
+import { mockBranch } from '../../../helpers/mocks/branch-like';
+import { renderComponent } from '../../../helpers/testReactTestingUtils';
+import BranchStatus, { BranchStatusProps } from '../BranchStatus';
 
-it('should render correctly', () => {
-  expect(shallowRender().type()).toBeNull();
-  expect(
-    shallowRender({
-      status: 'OK',
-    })
-  ).toMatchSnapshot('Successful');
-  expect(
-    shallowRender({
-      status: 'ERROR',
-    })
-  ).toMatchSnapshot('Error');
+const handler = new BranchesServiceMock();
+
+beforeEach(() => {
+  handler.reset();
+});
+
+it('should render ok status', async () => {
+  renderBranchStatus({ branchLike: mockBranch({ status: { qualityGateStatus: 'OK' } }) });
+
+  expect(await screen.findByText('OK')).toBeInTheDocument();
+});
+
+it('should render error status', async () => {
+  renderBranchStatus({ branchLike: mockBranch({ status: { qualityGateStatus: 'ERROR' } }) });
+
+  expect(await screen.findByText('ERROR')).toBeInTheDocument();
 });
 
-function shallowRender(overrides: Partial<BranchStatusProps> = {}) {
-  return shallow(<BranchStatus {...overrides} />);
+function renderBranchStatus(overrides: Partial<BranchStatusProps> = {}) {
+  const defaultProps = {
+    branchLike: mockBranch(),
+  } as const;
+  return renderComponent(<BranchStatus {...defaultProps} {...overrides} />);
 }
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchStatus-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchStatus-test.tsx.snap
deleted file mode 100644 (file)
index aeb2bda..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly: Error 1`] = `
-<Level
-  level="ERROR"
-  small={true}
-/>
-`;
-
-exports[`should render correctly: Successful 1`] = `
-<Level
-  level="OK"
-  small={true}
-/>
-`;
index 1794ecd47c77ec2bccf00db0b3c95138041ce758..9aed4c3dcb4567cf62cf366827b9f17117d0037d 100644 (file)
@@ -21,7 +21,7 @@ import styled from '@emotion/styled';
 import * as React from 'react';
 import { colors, sizes } from '../../app/theme';
 
-export interface BoxedTabsProps<K extends string | number> {
+export interface BoxedTabsProps<K> {
   className?: string;
   onSelect: (key: K) => void;
   selected?: K;
@@ -72,7 +72,7 @@ const ActiveBorder = styled.div<{ active: boolean }>`
   top: -1px;
 `;
 
-export default function BoxedTabs<K extends string | number>(props: BoxedTabsProps<K>) {
+export default function BoxedTabs<K>(props: BoxedTabsProps<K>) {
   const { className, tabs, selected } = props;
 
   return (
@@ -96,10 +96,10 @@ export default function BoxedTabs<K extends string | number>(props: BoxedTabsPro
   );
 }
 
-export function getTabPanelId(key: string | number) {
+export function getTabPanelId<K>(key: K) {
   return `tabpanel-${key}`;
 }
 
-export function getTabId(key: string | number) {
+export function getTabId<K>(key: K) {
   return `tab-${key}`;
 }
index d1f7bd57f85b25fda338972330c3cdb40fcebaf8..760025788b6afa05f5c606b4bd4084795f14474b 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 { flow } from 'lodash';
 import * as React from 'react';
+import { useCallback } from 'react';
 import { setIssueAssignee } from '../../api/issues';
 import { isInput, isShortcut } from '../../helpers/keyboardEventHelpers';
 import { KeyboardKeys } from '../../helpers/keycodes';
 import { getKeyboardShortcutEnabled } from '../../helpers/preferences';
+import { useRefreshBranchStatus } from '../../queries/branch';
 import { BranchLike } from '../../types/branch-like';
 import { Issue as TypeIssue } from '../../types/types';
 import { updateIssue } from './actions';
@@ -41,89 +44,93 @@ interface Props {
   selected: boolean;
 }
 
-export default class Issue extends React.PureComponent<Props> {
-  static defaultProps = {
-    selected: false,
-  };
+export default function Issue(props: Props) {
+  const {
+    selected = false,
+    issue,
+    branchLike,
+    checked,
+    openPopup,
+    displayWhyIsThisAnIssue,
+    onCheck,
+    onPopupToggle,
+  } = props;
 
-  componentDidMount() {
-    if (this.props.selected) {
-      document.addEventListener('keydown', this.handleKeyDown, { capture: true });
-    }
-  }
+  const refreshStatus = useRefreshBranchStatus();
 
-  componentDidUpdate(prevProps: Props) {
-    if (!prevProps.selected && this.props.selected) {
-      document.addEventListener('keydown', this.handleKeyDown, { capture: true });
-    } else if (prevProps.selected && !this.props.selected) {
-      document.removeEventListener('keydown', this.handleKeyDown, { capture: true });
-    }
-  }
+  const onChange = flow([props.onChange, refreshStatus]);
 
-  componentWillUnmount() {
-    if (this.props.selected) {
-      document.removeEventListener('keydown', this.handleKeyDown, { capture: true });
-    }
-  }
+  const togglePopup = useCallback(
+    (popupName: string, open?: boolean) => {
+      onPopupToggle(issue.key, popupName, open);
+    },
+    [issue.key, onPopupToggle]
+  );
 
-  handleKeyDown = (event: KeyboardEvent) => {
-    if (!getKeyboardShortcutEnabled() || isInput(event) || isShortcut(event)) {
-      return true;
-    } else if (event.key === KeyboardKeys.KeyF) {
-      event.preventDefault();
-      return this.togglePopup('transition');
-    } else if (event.key === KeyboardKeys.KeyA) {
-      event.preventDefault();
-      return this.togglePopup('assign');
-    } else if (event.key === KeyboardKeys.KeyM && this.props.issue.actions.includes('assign')) {
-      event.preventDefault();
-      return this.handleAssignement('_me');
-    } else if (event.key === KeyboardKeys.KeyI) {
-      event.preventDefault();
-      return this.togglePopup('set-severity');
-    } else if (event.key === KeyboardKeys.KeyC) {
-      event.preventDefault();
-      return this.togglePopup('comment');
-    } else if (event.key === KeyboardKeys.KeyT) {
-      event.preventDefault();
-      return this.togglePopup('edit-tags');
-    } else if (event.key === KeyboardKeys.Space) {
-      event.preventDefault();
-      if (this.props.onCheck) {
-        return this.props.onCheck(this.props.issue.key);
+  const handleAssignement = useCallback(
+    (login: string) => {
+      if (issue.assignee !== login) {
+        updateIssue(onChange, setIssueAssignee({ issue: issue.key, assignee: login }));
       }
-    }
-    return true;
-  };
+      togglePopup('assign', false);
+    },
+    [issue.assignee, issue.key, onChange, togglePopup]
+  );
 
-  togglePopup = (popupName: string, open?: boolean) => {
-    this.props.onPopupToggle(this.props.issue.key, popupName, open);
-  };
+  const handleKeyDown = useCallback(
+    (event: KeyboardEvent) => {
+      if (!getKeyboardShortcutEnabled() || isInput(event) || isShortcut(event)) {
+        return true;
+      } else if (event.key === KeyboardKeys.KeyF) {
+        event.preventDefault();
+        return togglePopup('transition');
+      } else if (event.key === KeyboardKeys.KeyA) {
+        event.preventDefault();
+        return togglePopup('assign');
+      } else if (event.key === KeyboardKeys.KeyM && issue.actions.includes('assign')) {
+        event.preventDefault();
+        return handleAssignement('_me');
+      } else if (event.key === KeyboardKeys.KeyI) {
+        event.preventDefault();
+        return togglePopup('set-severity');
+      } else if (event.key === KeyboardKeys.KeyC) {
+        event.preventDefault();
+        return togglePopup('comment');
+      } else if (event.key === KeyboardKeys.KeyT) {
+        event.preventDefault();
+        return togglePopup('edit-tags');
+      } else if (event.key === KeyboardKeys.Space) {
+        event.preventDefault();
+        if (onCheck) {
+          return onCheck(issue.key);
+        }
+      }
+      return true;
+    },
+    [issue.actions, issue.key, togglePopup, handleAssignement, onCheck]
+  );
 
-  handleAssignement = (login: string) => {
-    const { issue } = this.props;
-    if (issue.assignee !== login) {
-      updateIssue(this.props.onChange, setIssueAssignee({ issue: issue.key, assignee: login }));
+  React.useEffect(() => {
+    if (selected) {
+      document.addEventListener('keydown', handleKeyDown, { capture: true });
     }
-    this.togglePopup('assign', false);
-  };
+    return () => document.removeEventListener('keydown', handleKeyDown, { capture: true });
+  }, [handleKeyDown, selected]);
 
-  render() {
-    return (
-      <IssueView
-        branchLike={this.props.branchLike}
-        checked={this.props.checked}
-        currentPopup={this.props.openPopup}
-        displayWhyIsThisAnIssue={this.props.displayWhyIsThisAnIssue}
-        issue={this.props.issue}
-        onAssign={this.handleAssignement}
-        onChange={this.props.onChange}
-        onCheck={this.props.onCheck}
-        onClick={this.props.onClick}
-        onSelect={this.props.onSelect}
-        selected={this.props.selected}
-        togglePopup={this.togglePopup}
-      />
-    );
-  }
+  return (
+    <IssueView
+      branchLike={branchLike}
+      checked={checked}
+      currentPopup={openPopup}
+      displayWhyIsThisAnIssue={displayWhyIsThisAnIssue}
+      issue={issue}
+      onAssign={handleAssignement}
+      onChange={onChange}
+      onCheck={props.onCheck}
+      onClick={props.onClick}
+      onSelect={props.onSelect}
+      selected={selected}
+      togglePopup={togglePopup}
+    />
+  );
 }
index e8bac03ed08263441dc4ad1383eef6adf0de582e..2560444a0b101f68166a7d4abd1fc9f0eb819cc0 100644 (file)
@@ -28,6 +28,7 @@ import { KeyboardKeys } from '../../../helpers/keycodes';
 import { mockIssue, mockLoggedInUser, mockRawIssue } from '../../../helpers/testMocks';
 import { renderAppRoutes } from '../../../helpers/testReactTestingUtils';
 import { byLabelText, byRole, byText } from '../../../helpers/testSelector';
+import { ComponentPropsType } from '../../../helpers/testUtils';
 import {
   IssueActions,
   IssueSeverity,
@@ -416,8 +417,12 @@ function getPageObject() {
   return { ui, user };
 }
 
-function renderIssue(props: Partial<Omit<Issue['props'], 'onChange' | 'onPopupToggle'>> = {}) {
-  function Wrapper(wrapperProps: Omit<Issue['props'], 'onChange' | 'onPopupToggle'>) {
+function renderIssue(
+  props: Partial<Omit<ComponentPropsType<typeof Issue>, 'onChange' | 'onPopupToggle'>> = {}
+) {
+  function Wrapper(
+    wrapperProps: Omit<ComponentPropsType<typeof Issue>, 'onChange' | 'onPopupToggle'>
+  ) {
     const [issue, setIssue] = React.useState(wrapperProps.issue);
     const [openPopup, setOpenPopup] = React.useState<string | undefined>();
     return (
index 83ebf05e9aacd7b710847f93ba678b3c9e054931..3cb64f78607fa978e13db74a6d2948836c4ef83d 100644 (file)
@@ -21,12 +21,9 @@ import * as React from 'react';
 import { getAlmSettingsNoCatch } from '../../api/alm-settings';
 import { getScannableProjects } from '../../api/components';
 import { getValue } from '../../api/settings';
-import { ComponentContext } from '../../app/components/componentContext/ComponentContext';
-import { isMainBranch } from '../../helpers/branch-like';
 import { getHostUrl } from '../../helpers/urls';
 import { hasGlobalPermission } from '../../helpers/users';
 import { AlmSettingsInstance, ProjectAlmBindingResponse } from '../../types/alm-settings';
-import { MainBranch } from '../../types/branch-like';
 import { Permissions } from '../../types/permissions';
 import { SettingsKey } from '../../types/settings';
 import { Component } from '../../types/types';
@@ -50,8 +47,6 @@ interface State {
   loading: boolean;
 }
 
-const DEFAULT_MAIN_BRANCH_NAME = 'main';
-
 export class TutorialSelection extends React.PureComponent<Props, State> {
   mounted = false;
   state: State = {
@@ -121,25 +116,17 @@ export class TutorialSelection extends React.PureComponent<Props, State> {
     const selectedTutorial: TutorialModes | undefined = location.query?.selectedTutorial;
 
     return (
-      <ComponentContext.Consumer>
-        {({ branchLikes }) => (
-          <TutorialSelectionRenderer
-            almBinding={almBinding}
-            baseUrl={baseUrl}
-            component={component}
-            currentUser={currentUser}
-            currentUserCanScanProject={currentUserCanScanProject}
-            loading={loading}
-            mainBranchName={
-              (branchLikes.find((b) => isMainBranch(b)) as MainBranch | undefined)?.name ||
-              DEFAULT_MAIN_BRANCH_NAME
-            }
-            projectBinding={projectBinding}
-            selectedTutorial={selectedTutorial}
-            willRefreshAutomatically={willRefreshAutomatically}
-          />
-        )}
-      </ComponentContext.Consumer>
+      <TutorialSelectionRenderer
+        almBinding={almBinding}
+        baseUrl={baseUrl}
+        component={component}
+        currentUser={currentUser}
+        currentUserCanScanProject={currentUserCanScanProject}
+        loading={loading}
+        projectBinding={projectBinding}
+        selectedTutorial={selectedTutorial}
+        willRefreshAutomatically={willRefreshAutomatically}
+      />
     );
   }
 }
index 4bdf4962281468bdc15ff337a68218dc4938e7aa..e3328ad6ef756f26bbc21ec47113288349bb0719 100644 (file)
@@ -28,10 +28,13 @@ import {
   Title,
 } from 'design-system';
 import * as React from 'react';
+import { isMainBranch } from '../../helpers/branch-like';
 import { translate } from '../../helpers/l10n';
 import { getBaseUrl } from '../../helpers/system';
 import { getProjectTutorialLocation, getProjectUrl } from '../../helpers/urls';
+import { useBranchesQuery } from '../../queries/branch';
 import { AlmKeys, AlmSettingsInstance, ProjectAlmBindingResponse } from '../../types/alm-settings';
+import { MainBranch } from '../../types/branch-like';
 import { Component } from '../../types/types';
 import { LoggedInUser } from '../../types/users';
 import { Alert } from '../ui/Alert';
@@ -43,6 +46,8 @@ import JenkinsTutorial from './jenkins/JenkinsTutorial';
 import OtherTutorial from './other/OtherTutorial';
 import { TutorialModes } from './types';
 
+const DEFAULT_MAIN_BRANCH_NAME = 'main';
+
 export interface TutorialSelectionRendererProps {
   almBinding?: AlmSettingsInstance;
   baseUrl: string;
@@ -50,7 +55,6 @@ export interface TutorialSelectionRendererProps {
   currentUser: LoggedInUser;
   currentUserCanScanProject: boolean;
   loading: boolean;
-  mainBranchName: string;
   projectBinding?: ProjectAlmBindingResponse;
   selectedTutorial?: TutorialModes;
   willRefreshAutomatically?: boolean;
@@ -85,11 +89,17 @@ export default function TutorialSelectionRenderer(props: TutorialSelectionRender
     currentUser,
     currentUserCanScanProject,
     loading,
-    mainBranchName,
     projectBinding,
     selectedTutorial,
     willRefreshAutomatically,
   } = props;
+
+  const { data: { branchLikes } = { branchLikes: [] } } = useBranchesQuery(component);
+
+  const mainBranchName =
+    (branchLikes.find((b) => isMainBranch(b)) as MainBranch | undefined)?.name ||
+    DEFAULT_MAIN_BRANCH_NAME;
+
   if (loading) {
     return <i aria-label={translate('loading')} className="spinner" />;
   }
index a13a514b11a4bff904a91a4ce7557a65f30ad59c..d8404cc1e4b1ec7886c603c841527a4b646eb3fb 100644 (file)
@@ -25,24 +25,23 @@ import { getAlmSettingsNoCatch } from '../../../api/alm-settings';
 import { getScannableProjects } from '../../../api/components';
 import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock';
 import UserTokensMock from '../../../api/mocks/UserTokensMock';
-import {
-  mockGithubBindingDefinition,
-  mockProjectAlmBindingResponse,
-} from '../../../helpers/mocks/alm-settings';
+import { mockProjectAlmBindingResponse } from '../../../helpers/mocks/alm-settings';
 import { mockComponent } from '../../../helpers/mocks/component';
 import { mockLoggedInUser } from '../../../helpers/testMocks';
 import { renderApp } from '../../../helpers/testReactTestingUtils';
 import { byLabelText, byRole, byText } from '../../../helpers/testSelector';
+import { ComponentPropsType } from '../../../helpers/testUtils';
 import { AlmKeys } from '../../../types/alm-settings';
 import { Feature } from '../../../types/features';
 import { Permissions } from '../../../types/permissions';
 import { SettingsKey } from '../../../types/settings';
-import { withRouter } from '../../hoc/withRouter';
-import { TutorialSelection } from '../TutorialSelection';
+import TutorialSelection from '../TutorialSelection';
 import { TutorialModes } from '../types';
 
 jest.mock('../../../api/user-tokens');
 
+jest.mock('../../../api/branches');
+
 jest.mock('../../../helpers/urls', () => ({
   ...jest.requireActual('../../../helpers/urls'),
   getHostUrl: jest.fn().mockReturnValue('http://host.url'),
@@ -120,9 +119,11 @@ it.each([
 });
 
 it('should correctly fetch the corresponding ALM setting', async () => {
-  (getAlmSettingsNoCatch as jest.Mock).mockResolvedValueOnce([
-    mockGithubBindingDefinition({ key: 'binding', url: 'https://enterprise.github.com' }),
-  ]);
+  jest
+    .mocked(getAlmSettingsNoCatch)
+    .mockResolvedValueOnce([
+      { key: 'binding', url: 'https://enterprise.github.com', alm: AlmKeys.GitHub },
+    ]);
   const user = userEvent.setup();
   renderTutorialSelection(
     {
@@ -160,7 +161,9 @@ it('should fallback on the host URL', async () => {
 });
 
 it('should not display a warning if the user has no global scan permission, but can scan the project', async () => {
-  (getScannableProjects as jest.Mock).mockResolvedValueOnce({ projects: [{ key: 'foo' }] });
+  jest
+    .mocked(getScannableProjects)
+    .mockResolvedValueOnce({ projects: [{ key: 'foo', name: 'foo' }] });
   renderTutorialSelection({ currentUser: mockLoggedInUser() });
   await waitOnDataLoaded();
 
@@ -194,16 +197,12 @@ async function startJenkinsTutorial(user: UserEvent) {
 }
 
 function renderTutorialSelection(
-  props: Partial<TutorialSelection['props']> = {},
+  props: Partial<ComponentPropsType<typeof TutorialSelection>> = {},
   navigateTo: string = 'dashboard?id=bar'
 ) {
-  const Wrapper = withRouter(({ location, ...subProps }: TutorialSelection['props']) => {
-    return <TutorialSelection location={location} {...subProps} />;
-  });
-
   return renderApp(
     '/dashboard',
-    <Wrapper
+    <TutorialSelection
       component={mockComponent({ key: 'foo' })}
       currentUser={mockLoggedInUser({ permissions: { global: [Permissions.Scan] } })}
       {...props}
index fe7a4e1a8cd8145b2827b0ff07c9878fe73605cb..5688285e13bcecb336446199abd4d25b2e7cd853 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 { debounce } from 'lodash';
 import * as React from 'react';
-import { getParents } from '../../api/components';
-import withBranchStatusActions from '../../app/components/branch-status/withBranchStatusActions';
-import { isPullRequest } from '../../helpers/branch-like';
-import { BranchLike } from '../../types/branch-like';
-import { Issue, SourceViewerFile } from '../../types/types';
+import { SourceViewerFile } from '../../types/types';
 import SourceViewer from '../SourceViewer/SourceViewer';
 import WorkspaceComponentTitle from './WorkspaceComponentTitle';
 import WorkspaceHeader, { Props as WorkspaceHeaderProps } from './WorkspaceHeader';
@@ -31,20 +26,14 @@ import { ComponentDescriptor } from './context';
 
 export interface Props extends Omit<WorkspaceHeaderProps, 'children' | 'onClose'> {
   component: ComponentDescriptor;
-  fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => Promise<void>;
   height: number;
   onClose: (componentKey: string) => void;
   onLoad: (details: { key: string; name: string; qualifier: string }) => void;
 }
 
-export class WorkspaceComponentViewer extends React.PureComponent<Props> {
+export default class WorkspaceComponentViewer extends React.PureComponent<Props> {
   container?: HTMLElement | null;
 
-  constructor(props: Props) {
-    super(props);
-    this.refreshBranchStatus = debounce(this.refreshBranchStatus, 1000);
-  }
-
   componentDidMount() {
     if (document.documentElement) {
       document.documentElement.classList.add('with-workspace');
@@ -61,10 +50,6 @@ export class WorkspaceComponentViewer extends React.PureComponent<Props> {
     this.props.onClose(this.props.component.key);
   };
 
-  handleIssueChange = (_: Issue) => {
-    this.refreshBranchStatus();
-  };
-
   handleLoaded = (component: SourceViewerFile) => {
     this.props.onLoad({
       key: this.props.component.key,
@@ -82,21 +67,6 @@ export class WorkspaceComponentViewer extends React.PureComponent<Props> {
     }
   };
 
-  refreshBranchStatus = () => {
-    const { component } = this.props;
-    const { branchLike } = component;
-    if (branchLike && isPullRequest(branchLike)) {
-      getParents(component.key).then(
-        (parents?: any[]) => {
-          if (parents && parents.length > 0) {
-            this.props.fetchBranchStatus(branchLike, parents.pop().key);
-          }
-        },
-        () => {}
-      );
-    }
-  };
-
   render() {
     const { component } = this.props;
 
@@ -123,7 +93,6 @@ export class WorkspaceComponentViewer extends React.PureComponent<Props> {
             branchLike={component.branchLike}
             component={component.key}
             highlightedLine={component.line}
-            onIssueChange={this.handleIssueChange}
             onLoaded={this.handleLoaded}
           />
         </div>
@@ -131,5 +100,3 @@ export class WorkspaceComponentViewer extends React.PureComponent<Props> {
     );
   }
 }
-
-export default withBranchStatusActions(WorkspaceComponentViewer);
index f278e12fb9ace37affeba5be1465a5e316282aa1..5efd0bdb0e661169f574d16ee202cd00eec7df41 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import { getParents } from '../../../api/components';
-import { mockPullRequest } from '../../../helpers/mocks/branch-like';
-import { mockIssue } from '../../../helpers/testMocks';
-import { waitAndUpdate } from '../../../helpers/testUtils';
-import { Props, WorkspaceComponentViewer } from '../WorkspaceComponentViewer';
+import WorkspaceComponentViewer, { Props } from '../WorkspaceComponentViewer';
 
 jest.mock('../../../api/components', () => ({
   getParents: jest.fn().mockResolvedValue([{ key: 'bar' }]),
@@ -55,28 +51,10 @@ it('should call back after load', () => {
   expect(onLoad).toHaveBeenCalledWith({ key: 'foo', name: 'src/foo.js', qualifier: 'FIL' });
 });
 
-it('should refresh branch status if issues are updated', async () => {
-  const fetchBranchStatus = jest.fn();
-  const branchLike = mockPullRequest();
-  const component = {
-    branchLike,
-    key: 'foo',
-  };
-  const wrapper = shallowRender({ component, fetchBranchStatus });
-  const instance = wrapper.instance();
-  await waitAndUpdate(wrapper);
-
-  instance.handleIssueChange(mockIssue());
-  expect(getParents).toHaveBeenCalledWith(component.key);
-  await waitAndUpdate(wrapper);
-  expect(fetchBranchStatus).toHaveBeenCalledWith(branchLike, 'bar');
-});
-
 function shallowRender(props?: Partial<Props>) {
   return shallow<WorkspaceComponentViewer>(
     <WorkspaceComponentViewer
       component={{ branchLike: undefined, key: 'foo' }}
-      fetchBranchStatus={jest.fn()}
       height={300}
       onClose={jest.fn()}
       onCollapse={jest.fn()}
index e7ef86876090172b5b8c2497946324ab0a532ee0..8a4dba80db189fde73a838747a49ae5ed5219eca 100644 (file)
@@ -53,7 +53,7 @@ exports[`should render correctly: open component 1`] = `
         }
       }
     />
-    <withBranchStatusActions(WorkspaceComponentViewer)
+    <WorkspaceComponentViewer
       component={
         {
           "branchLike": {
index c1190c2ad0274ccc90def0c1a97c6ff9fe7ad38f..65ba4da42547c993e715dbaadde322f9d20fd85f 100644 (file)
@@ -34,7 +34,6 @@ exports[`should render 1`] = `
       displayIssueLocationsCount={true}
       displayIssueLocationsLink={true}
       displayLocationMarkers={true}
-      onIssueChange={[Function]}
       onLoaded={[Function]}
     />
   </div>
index 1b8f75fcb6dfee51e38f01d0cec4dc50e9f078c8..f5c6f5d5f51eee637b1ea877f41c91a8a7e53840 100644 (file)
@@ -23,11 +23,9 @@ import {
   BranchLike,
   BranchLikeTree,
   BranchParameters,
-  BranchStatusData,
   MainBranch,
   PullRequest,
 } from '../types/branch-like';
-import { Dict } from '../types/types';
 
 export function isBranch(branchLike?: BranchLike): branchLike is Branch {
   return branchLike !== undefined && (branchLike as Branch).isMain !== undefined;
@@ -139,12 +137,3 @@ export function fillBranchLike(
   }
   return undefined;
 }
-
-export function getBranchStatusByBranchLike(
-  branchStatusByComponent: Dict<Dict<BranchStatusData>>,
-  component: string,
-  branchLike: BranchLike
-): BranchStatusData {
-  const branchLikeKey = getBranchLikeKey(branchLike);
-  return branchStatusByComponent[component] && branchStatusByComponent[component][branchLikeKey];
-}
index 2a4a246f4062dcb4af5c9c129dcf54a0b2057772..55c791d724adf13c1178e2ed2a14fea918d3d69f 100644 (file)
@@ -20,6 +20,7 @@
 import {
   QualityGateApplicationStatus,
   QualityGateProjectStatus,
+  QualityGateProjectStatusCondition,
   QualityGateStatus,
   QualityGateStatusCondition,
   QualityGateStatusConditionEnhanced,
@@ -48,6 +49,20 @@ export function mockQualityGateStatus(
   };
 }
 
+export function mockQualityGateProjectCondition(
+  overrides: Partial<QualityGateProjectStatusCondition> = {}
+): QualityGateProjectStatusCondition {
+  return {
+    actualValue: '10',
+    errorThreshold: '0',
+    status: 'ERROR',
+    metricKey: 'foo',
+    comparator: 'GT',
+    periodIndex: 1,
+    ...overrides,
+  };
+}
+
 export function mockQualityGateStatusCondition(
   overrides: Partial<QualityGateStatusCondition> = {}
 ): QualityGateStatusCondition {
index 2ef2063a9ea416db4b4cb27aafe0ea89b43b2712..3b8fcced33e8e61a7cbd3d72d2b9da2197231d12 100644 (file)
@@ -94,7 +94,7 @@ export function renderAppWithAdminContext(
 export function renderComponent(
   component: React.ReactElement,
   pathname = '/',
-  { appState = mockAppState() }: RenderContext = {}
+  { appState = mockAppState(), featureList = [] }: RenderContext = {}
 ) {
   function Wrapper({ children }: { children: React.ReactElement }) {
     const queryClient = new QueryClient();
@@ -103,13 +103,15 @@ export function renderComponent(
       <IntlProvider defaultLocale="en" locale="en">
         <QueryClientProvider client={queryClient}>
           <HelmetProvider>
-            <AppStateContextProvider appState={appState}>
-              <MemoryRouter initialEntries={[pathname]}>
-                <Routes>
-                  <Route path="*" element={children} />
-                </Routes>
-              </MemoryRouter>
-            </AppStateContextProvider>
+            <AvailableFeaturesContext.Provider value={featureList}>
+              <AppStateContextProvider appState={appState}>
+                <MemoryRouter initialEntries={[pathname]}>
+                  <Routes>
+                    <Route path="*" element={children} />
+                  </Routes>
+                </MemoryRouter>
+              </AppStateContextProvider>
+            </AvailableFeaturesContext.Provider>
           </HelmetProvider>
         </QueryClientProvider>
       </IntlProvider>
@@ -132,8 +134,6 @@ export function renderAppWithComponentContext(
     return (
       <ComponentContext.Provider
         value={{
-          branchLikes: [],
-          onBranchesChange: jest.fn(),
           onComponentChange: (changes: Partial<Component>) => {
             setRealComponent({ ...realComponent, ...changes });
           },
index 5a7a99f8e66b5ea645dd0bc22e29ba804ecc0b41..0f6eae1adb8e203a40a4d4ae09f0bf3506638810 100644 (file)
@@ -52,6 +52,15 @@ export interface ReactTestingQuery {
   byLabelText(...args: Parameters<BoundFunction<GetByText>>): ReactTestingQuery;
   byTestId(...args: Parameters<BoundFunction<GetByBoundAttribute>>): ReactTestingQuery;
   byDisplayValue(...args: Parameters<BoundFunction<GetByBoundAttribute>>): ReactTestingQuery;
+
+  getAt<T extends HTMLElement = HTMLElement>(index: number, container?: HTMLElement): T;
+  findAt<T extends HTMLElement = HTMLElement>(
+    index: number,
+    container?: HTMLElement,
+    waitForOptions?: waitForOptions
+  ): Promise<T>;
+
+  queryAt<T extends HTMLElement = HTMLElement>(index: number, container?: HTMLElement): T | null;
 }
 
 abstract class ChainingQuery implements ReactTestingQuery {
@@ -73,6 +82,26 @@ abstract class ChainingQuery implements ReactTestingQuery {
 
   abstract queryAll<T extends HTMLElement = HTMLElement>(container?: HTMLElement): T[] | null;
 
+  getAt<T extends HTMLElement = HTMLElement>(index: number, container?: HTMLElement): T {
+    return this.getAll<T>(container)[index];
+  }
+
+  async findAt<T extends HTMLElement = HTMLElement>(
+    index: number,
+    container?: HTMLElement,
+    waitForOptions?: waitForOptions
+  ): Promise<T> {
+    return (await this.findAll<T>(container, waitForOptions))[index];
+  }
+
+  queryAt<T extends HTMLElement = HTMLElement>(index: number, container?: HTMLElement): T | null {
+    const all = this.queryAll<T>(container);
+    if (all) {
+      return all[index];
+    }
+    return null;
+  }
+
   byText(...args: Parameters<BoundFunction<GetByText>>): ReactTestingQuery {
     return new ChainDispatch(this, new DispatchByText(args));
   }
index 2a7e904c232b26fd8348a0688d26336082721202..3506f663e3c26a4f4f233d9b22ffecd828fdee3b 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { ReactWrapper, ShallowWrapper } from 'enzyme';
+import { ComponentClass, FunctionComponent } from 'react';
 import { setImmediate } from 'timers';
 import { KeyboardKeys } from './keycodes';
 
-export type FCProps<T extends React.FunctionComponent<any>> = Parameters<T>[0];
+export type ComponentPropsType<T extends ComponentClass | FunctionComponent<any>> =
+  T extends ComponentClass<infer P> ? P : T extends FunctionComponent<infer P> ? P : never;
 
 export function mockEvent(overrides = {}) {
   return {
index a5a368827911cba1b81b971be06d1b2defed2251..e00acea704f2c0f06a3f3d0558734da51a2da2bf 100644 (file)
@@ -451,10 +451,13 @@ export function isRelativeUrl(url?: string): boolean {
   return Boolean(url && regex.test(url));
 }
 
-export function searchParamsToQuery(searchParams: URLSearchParams) {
+export function searchParamsToQuery(searchParams: URLSearchParams, omitKey: string[] = []) {
   const result: RawQuery = {};
 
   searchParams.forEach((value, key) => {
+    if (omitKey.includes(key)) {
+      return;
+    }
     if (result[key]) {
       result[key] = ([] as string[]).concat(result[key], value);
     } else {
diff --git a/server/sonar-web/src/main/js/queries/branch.tsx b/server/sonar-web/src/main/js/queries/branch.tsx
new file mode 100644 (file)
index 0000000..1d99799
--- /dev/null
@@ -0,0 +1,333 @@
+/*
+ * 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 { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { debounce, flatten } from 'lodash';
+import * as React from 'react';
+import { useCallback, useContext } from 'react';
+import { useSearchParams } from 'react-router-dom';
+import {
+  deleteBranch,
+  deletePullRequest,
+  excludeBranchFromPurge,
+  getBranches,
+  getPullRequests,
+  renameBranch,
+} from '../api/branches';
+import { dismissAnalysisWarning, getAnalysisStatus } from '../api/ce';
+import { getQualityGateProjectStatus } from '../api/quality-gates';
+import { AvailableFeaturesContext } from '../app/components/available-features/AvailableFeaturesContext';
+import { useLocation } from '../components/hoc/withRouter';
+import { isBranch, isPullRequest } from '../helpers/branch-like';
+import { extractStatusConditionsFromProjectStatus } from '../helpers/qualityGates';
+import { searchParamsToQuery } from '../helpers/urls';
+import { BranchLike } from '../types/branch-like';
+import { isApplication, isPortfolioLike, isProject } from '../types/component';
+import { Feature } from '../types/features';
+import { Component } from '../types/types';
+
+// This will prevent refresh when navigating from page to page.
+const BRANCHES_STALE_TIME = 30_000;
+
+enum InnerState {
+  Details = 'details',
+  Warning = 'warning',
+  Status = 'status',
+}
+
+function useBranchesQueryKey(innerState: InnerState) {
+  // Currently, we do not have the component in a react-state ready
+  // Once we refactor we will be able to fetch it from query state.
+  // We will be able to make sure that the component is not a portfolio.
+  // Mixing query param and react-state is dangerous.
+  // It should be avoided as much as possible.
+  const { search } = useLocation();
+  const searchParams = new URLSearchParams(search);
+
+  if (searchParams.has('pullRequest') && searchParams.has('id')) {
+    return [
+      'branches',
+      searchParams.get('id') as string,
+      'pull-request',
+      searchParams.get('pullRequest') as string,
+      innerState,
+    ] as const;
+  } else if (searchParams.has('branch') && searchParams.has('id')) {
+    return [
+      'branches',
+      searchParams.get('id') as string,
+      'branch',
+      searchParams.get('branch') as string,
+      innerState,
+    ] as const;
+  } else if (searchParams.has('id')) {
+    return ['branches', searchParams.get('id') as string, innerState] as const;
+  }
+  return ['branches'];
+}
+
+function useMutateBranchQueryKey() {
+  const { search } = useLocation();
+  const searchParams = new URLSearchParams(search);
+
+  if (searchParams.has('id')) {
+    return ['branches', searchParams.get('id') as string] as const;
+  }
+  return ['branches'];
+}
+
+function getContext(key: ReturnType<typeof useBranchesQueryKey>) {
+  const [_b, componentKey, prOrBranch, branchKey] = key;
+  if (prOrBranch === 'pull-request') {
+    return { componentKey, query: { pullRequest: branchKey } };
+  }
+  if (prOrBranch === 'branch') {
+    return { componentKey, query: { branch: branchKey } };
+  }
+  return { componentKey, query: {} };
+}
+
+export function useBranchesQuery(component?: Component) {
+  const features = useContext(AvailableFeaturesContext);
+  const key = useBranchesQueryKey(InnerState.Details);
+  return useQuery({
+    queryKey: key,
+    queryFn: async ({ queryKey: [_, key, prOrBranch, name] }) => {
+      if (component === undefined || key === undefined) {
+        return { branchLikes: [] };
+      }
+      if (isPortfolioLike(component.qualifier)) {
+        return { branchLikes: [] };
+      }
+
+      const branchLikesPromise =
+        isProject(component.qualifier) && features.includes(Feature.BranchSupport)
+          ? [getBranches(key), getPullRequests(key)]
+          : [getBranches(key)];
+      const branchLikes = await Promise.all(branchLikesPromise).then(flatten<BranchLike>);
+      const branchLike =
+        prOrBranch === 'pull-request'
+          ? branchLikes.find((b) => isPullRequest(b) && b.key === name)
+          : branchLikes.find(
+              (b) => isBranch(b) && (prOrBranch === 'branch' ? b.name === name : b.isMain)
+            );
+      return { branchLikes, branchLike };
+    },
+    // The check of the key must desapear once component state is in react-query
+    enabled: !!component && component.key === key[1],
+    staleTime: BRANCHES_STALE_TIME,
+  });
+}
+
+export function useBranchStatusQuery(component: Component) {
+  const key = useBranchesQueryKey(InnerState.Status);
+  return useQuery({
+    queryKey: key,
+    queryFn: async ({ queryKey }) => {
+      const { query } = getContext(queryKey);
+      if (!isProject(component.qualifier)) {
+        return {};
+      }
+      const projectStatus = await getQualityGateProjectStatus({
+        projectKey: component.key,
+        ...query,
+      }).catch(() => undefined);
+      if (projectStatus === undefined) {
+        return {};
+      }
+
+      const { ignoredConditions, status } = projectStatus;
+      const conditions = extractStatusConditionsFromProjectStatus(projectStatus);
+      return {
+        conditions,
+        ignoredConditions,
+        status,
+      };
+    },
+    enabled: isProject(component.qualifier) || isApplication(component.qualifier),
+    staleTime: BRANCHES_STALE_TIME,
+  });
+}
+
+export function useBranchWarrningQuery(component: Component) {
+  const branchQuery = useBranchesQuery(component);
+  const branchLike = branchQuery.data?.branchLike;
+  return useQuery({
+    queryKey: useBranchesQueryKey(InnerState.Warning),
+    queryFn: async ({ queryKey }) => {
+      const { query, componentKey } = getContext(queryKey);
+      const { component: branchStatus } = await getAnalysisStatus({
+        component: componentKey,
+        ...query,
+      });
+      return branchStatus.warnings;
+    },
+    enabled: !!branchLike && isProject(component.qualifier),
+    staleTime: BRANCHES_STALE_TIME,
+  });
+}
+
+export function useDismissBranchWarningMutation() {
+  type DismissArg = { component: Component; key: string };
+  const queryClient = useQueryClient();
+  const invalidateKey = useBranchesQueryKey(InnerState.Warning);
+
+  return useMutation({
+    mutationFn: async ({ component, key }: DismissArg) => {
+      await dismissAnalysisWarning(component.key, key);
+    },
+    onSuccess() {
+      queryClient.invalidateQueries({ queryKey: invalidateKey });
+    },
+  });
+}
+
+export function useExcludeFromPurgeMutation() {
+  const queryClient = useQueryClient();
+  const invalidateKey = useMutateBranchQueryKey();
+
+  type ExcludeFromPurgeArg = { component: Component; key: string; exclude: boolean };
+
+  return useMutation({
+    mutationFn: async ({ component, key, exclude }: ExcludeFromPurgeArg) => {
+      await excludeBranchFromPurge(component.key, key, exclude);
+    },
+    onSuccess() {
+      queryClient.invalidateQueries({ queryKey: invalidateKey });
+    },
+  });
+}
+
+export function useDeletBranchMutation() {
+  type DeleteArg = { branchLike: BranchLike; component: Component };
+  const queryClient = useQueryClient();
+  const [params, setSearhParam] = useSearchParams();
+  const invalidateKey = useMutateBranchQueryKey();
+
+  return useMutation({
+    mutationFn: async ({ branchLike, component }: DeleteArg) => {
+      await (isPullRequest(branchLike)
+        ? deletePullRequest({
+            project: component.key,
+            pullRequest: branchLike.key,
+          })
+        : deleteBranch({
+            branch: branchLike.name,
+            project: component.key,
+          }));
+
+      if (
+        isBranch(branchLike) &&
+        params.has('branch') &&
+        params.get('branch') === branchLike.name
+      ) {
+        setSearhParam(searchParamsToQuery(params, ['branch']));
+        return { navigate: true };
+      }
+
+      if (
+        isPullRequest(branchLike) &&
+        params.has('pullRequest') &&
+        params.get('pullRequest') === branchLike.key
+      ) {
+        setSearhParam(searchParamsToQuery(params, ['pullRequest']));
+        return { navigate: true };
+      }
+      return { navigate: false };
+    },
+    onSuccess({ navigate }) {
+      if (!navigate) {
+        queryClient.invalidateQueries({ queryKey: invalidateKey });
+      }
+    },
+  });
+}
+
+export function useRenameMainBranchMutation() {
+  type RenameMainBranchArg = { name: string; component: Component };
+  const queryClient = useQueryClient();
+  const invalidateKey = useMutateBranchQueryKey();
+
+  return useMutation({
+    mutationFn: async ({ component, name }: RenameMainBranchArg) => {
+      await renameBranch(component.key, name);
+    },
+    onSuccess() {
+      queryClient.invalidateQueries({ queryKey: invalidateKey });
+    },
+  });
+}
+
+/**
+ * Helper functions that sould be avoid. Instead convert the component into functional
+ * and/or use proper react-query
+ */
+const DELAY_REFRECH = 1_000;
+
+export function useRefreshBranchStatus(): () => void {
+  const queryClient = useQueryClient();
+  const invalidateStatusKey = useBranchesQueryKey(InnerState.Status);
+  const invalidateDetailsKey = useBranchesQueryKey(InnerState.Details);
+
+  return useCallback(
+    debounce(() => {
+      queryClient.invalidateQueries({
+        queryKey: invalidateStatusKey,
+      });
+      queryClient.invalidateQueries({
+        queryKey: invalidateDetailsKey,
+      });
+    }, DELAY_REFRECH),
+    [invalidateDetailsKey, invalidateStatusKey]
+  );
+}
+
+export function useRefreshBranches() {
+  const queryClient = useQueryClient();
+  const invalidateKey = useMutateBranchQueryKey();
+
+  return () => {
+    queryClient.invalidateQueries({ queryKey: invalidateKey });
+  };
+}
+
+export function withBranchLikes<P extends { component?: Component }>(
+  WrappedComponent: React.ComponentType<P & { branchLikes?: BranchLike[]; branchLike?: BranchLike }>
+): React.ComponentType<Omit<P, 'branchLike' | 'branchLikes'>> {
+  return function WithBranchLike(p: P) {
+    const { data } = useBranchesQuery(p.component);
+    return (
+      <WrappedComponent
+        branchLikes={data?.branchLikes ?? []}
+        branchLike={data?.branchLike}
+        {...p}
+      />
+    );
+  };
+}
+
+export function withBranchStatusRefresh<
+  P extends { refreshBranchStatus: ReturnType<typeof useRefreshBranchStatus> }
+>(WrappedComponent: React.ComponentType<P>): React.ComponentType<Omit<P, 'refreshBranchStatus'>> {
+  return function WithBranchStatusRefresh(props: P) {
+    const refresh = useRefreshBranchStatus();
+
+    return <WrappedComponent {...props} refreshBranchStatus={refresh} />;
+  };
+}
diff --git a/server/sonar-web/src/main/js/queries/withQueryClientHoc.tsx b/server/sonar-web/src/main/js/queries/withQueryClientHoc.tsx
new file mode 100644 (file)
index 0000000..21beffc
--- /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 { QueryClient, useQueryClient } from '@tanstack/react-query';
+import * as React from 'react';
+import { ComponentClass, VFC } from 'react';
+
+export function withQueryClient<P>(
+  Component:
+    | ComponentClass<P & { queryClient: QueryClient }>
+    | VFC<P & { queryClient: QueryClient }>
+): VFC<Omit<P, 'queryClient'>> {
+  return function WithQueryClient(props: P) {
+    const queryClient = useQueryClient();
+    return <Component {...props} queryClient={queryClient} />;
+  };
+}
index ab5a3b6fd8e59f6936bf3109dacf8cb513ff7477..11feb73a504507340630b3970699c062fbc86e67 100644 (file)
@@ -3,7 +3,6 @@
 exports[`[Function isApplication] should work properly 1`] = `
 {
   "APP": true,
-  "DEV": false,
   "DIR": false,
   "FIL": false,
   "SVW": false,
@@ -16,7 +15,6 @@ exports[`[Function isApplication] should work properly 1`] = `
 exports[`[Function isFile] should work properly 1`] = `
 {
   "APP": false,
-  "DEV": false,
   "DIR": false,
   "FIL": true,
   "SVW": false,
@@ -29,7 +27,6 @@ exports[`[Function isFile] should work properly 1`] = `
 exports[`[Function isPortfolioLike] should work properly 1`] = `
 {
   "APP": false,
-  "DEV": false,
   "DIR": false,
   "FIL": false,
   "SVW": true,
@@ -42,7 +39,6 @@ exports[`[Function isPortfolioLike] should work properly 1`] = `
 exports[`[Function isProject] should work properly 1`] = `
 {
   "APP": false,
-  "DEV": false,
   "DIR": false,
   "FIL": false,
   "SVW": false,
@@ -55,7 +51,6 @@ exports[`[Function isProject] should work properly 1`] = `
 exports[`[Function isView] should work properly 1`] = `
 {
   "APP": true,
-  "DEV": false,
   "DIR": false,
   "FIL": false,
   "SVW": true,
index e0b98f13d3ef6f2154831c20bd0aaa67b17b80e6..4e7a678cc5c5354e884430e48c462399dfa1ce4a 100644 (file)
@@ -18,7 +18,6 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { ProjectAlmBindingResponse } from './alm-settings';
-import { BranchLike } from './branch-like';
 import { Component, LightComponent } from './types';
 
 export enum Visibility {
@@ -29,7 +28,6 @@ export enum Visibility {
 export enum ComponentQualifier {
   Application = 'APP',
   Directory = 'DIR',
-  Developper = 'DEV',
   File = 'FIL',
   Portfolio = 'VW',
   Project = 'TRK',
@@ -98,12 +96,9 @@ export function isView(
 }
 
 export interface ComponentContextShape {
-  branchLike?: BranchLike;
-  branchLikes: BranchLike[];
   component?: Component;
   isInProgress?: boolean;
   isPending?: boolean;
-  onBranchesChange: (updateBranches?: boolean, updatePRs?: boolean) => void;
   onComponentChange: (changes: Partial<Component>) => void;
   projectBinding?: ProjectAlmBindingResponse;
 }
index 1f70b29f9ccf71de390e7610a1cf578c547ce07e..efcf9622b452f59644bf36ac8b36a35f50202ba9 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { QueryClient } from '@tanstack/react-query';
 import { Theme } from 'design-system';
 import { IntlShape } from 'react-intl';
 import { Location, Router } from '../components/hoc/withRouter';
@@ -59,6 +60,7 @@ export interface ExtensionStartMethodParameter {
   dsTheme: Theme;
   baseUrl: string;
   l10nBundle: L10nBundle;
+  queryClient: QueryClient;
   // See SONAR-16207 and core-extension-enterprise-server/src/main/js/portfolios/components/Header.tsx
   // for more information on why we're passing this as a prop to an extension.
   updateCurrentUserHomepage: (homepage: HomePage) => void;
index 79aa25884c05734381202a7909c4ec960460d1d0..41b5a82ee18ba77d68e8783a73ab5fc90ef2d82f 100644 (file)
@@ -412,7 +412,6 @@ qualifier.APP=Application
 qualifier.FIL=File
 qualifier.CLA=File
 qualifier.UTS=Test File
-qualifier.DEV=Developer
 
 qualifier.configuration.TRK=Project Configuration
 qualifier.configuration.VW=Portfolio Configuration
@@ -428,16 +427,13 @@ qualifiers.APP=Applications
 qualifiers.FIL=Files
 qualifiers.CLA=Files
 qualifiers.UTS=Test Files
-qualifiers.DEV=Developers
 
 qualifiers.all.TRK=All Projects
 qualifiers.all.VW=All Portfolios
-qualifiers.all.DEV=All Developers
 qualifiers.all.APP=All Applications
 
 qualifiers.new.TRK=New Project
 qualifiers.new.VW=New Portfolio
-qualifiers.new.DEV=New Developer
 qualifiers.new.APP=New Application
 
 qualifier.delete.TRK=Delete Project
@@ -458,11 +454,9 @@ qualifiers.delete_confirm.APP=Do you want to delete these applications?
 
 qualifiers.create.TRK=Create Project
 qualifiers.create.VW=Create Portfolio
-qualifiers.create.DEV=Create Developer
 qualifiers.create.APP=Create Application
 
 qualifiers.update.VW=Update Portfolio
-qualifiers.update.DEV=Update Developer
 qualifiers.update.APP=Update Application
 
 qualifier.description.VW=Potentially multi-level, management-oriented overview aggregation.