aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/sonar-web/src/main/js/api/mocks/BranchesServiceMock.ts11
-rw-r--r--server/sonar-web/src/main/js/api/mocks/SecurityHotspotServiceMock.ts4
-rw-r--r--server/sonar-web/src/main/js/app/components/ComponentContainer.tsx179
-rw-r--r--server/sonar-web/src/main/js/app/components/GlobalContainer.tsx29
-rw-r--r--server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx88
-rw-r--r--server/sonar-web/src/main/js/app/components/branch-status/BranchStatusContextProvider.tsx104
-rw-r--r--server/sonar-web/src/main/js/app/components/branch-status/__tests__/BranchStatusContextProvider-test.tsx73
-rw-r--r--server/sonar-web/src/main/js/app/components/branch-status/withBranchStatus.tsx58
-rw-r--r--server/sonar-web/src/main/js/app/components/branch-status/withBranchStatusActions.tsx50
-rw-r--r--server/sonar-web/src/main/js/app/components/componentContext/ComponentContext.ts2
-rw-r--r--server/sonar-web/src/main/js/app/components/current-user/withCurrentUserContext.tsx2
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/Extension.tsx8
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx8
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.tsx7
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectAdminPageExtension-test.tsx26
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectPageExtension-test.tsx59
-rw-r--r--server/sonar-web/src/main/js/app/components/global-search/utils.ts1
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorMessage.tsx23
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorModal.tsx4
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/AnalysisStatus.tsx23
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/AnalysisWarningsModal.tsx103
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx48
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/Header.tsx20
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx24
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx316
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisErrorMessage-test.tsx33
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx13
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/Header-test.tsx148
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/HeaderMeta-test.tsx73
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/Menu-test.tsx63
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx120
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLike.tsx6
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx1
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItem.tsx5
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItemList.tsx5
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/QualityGateStatus.tsx35
-rw-r--r--server/sonar-web/src/main/js/app/components/promotion-notification/PromotionNotification.tsx4
-rw-r--r--server/sonar-web/src/main/js/app/components/promotion-notification/__tests__/PromotionNotification-test.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/background-tasks/components/AnalysisWarningsModal.tsx (renamed from server/sonar-web/src/main/js/components/common/AnalysisWarningsModal.tsx)35
-rw-r--r--server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx25
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/CodeAppRenderer.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/SourceViewerWrapper.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx13
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx33
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx9
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx29
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-it.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/issues/issues-subnavigation/__tests__/SubnavigationIssues-it.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/App.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx24
-rw-r--r--server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx304
-rw-r--r--server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/PullRequestOverview-it.tsx72
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/BranchList.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineApp.tsx9
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineApp-it.tsx47
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/ProjectBranchesApp.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/__tests__/ProjectBranchesApp-it.tsx170
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeRow.tsx9
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTable.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTabs.tsx134
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/BranchPurgeSetting.tsx92
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/DeleteBranchModal.tsx111
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx123
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/CoverageFilter-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguagesFilter-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/QualityGateFilter-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-it.tsx20
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotHeader.tsx16
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx4
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/activity-graph/EventInner.tsx11
-rw-r--r--server/sonar-web/src/main/js/components/activity-graph/__tests__/ActivityGraph-it.tsx4
-rw-r--r--server/sonar-web/src/main/js/components/activity-graph/__tests__/EventInner-it.tsx38
-rw-r--r--server/sonar-web/src/main/js/components/common/BranchStatus.tsx17
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx29
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/BranchStatus-test.tsx42
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchStatus-test.tsx.snap15
-rw-r--r--server/sonar-web/src/main/js/components/controls/BoxedTabs.tsx8
-rw-r--r--server/sonar-web/src/main/js/components/issue/Issue.tsx159
-rw-r--r--server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx9
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/TutorialSelection.tsx35
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx14
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/__tests__/TutorialSelection-it.tsx31
-rw-r--r--server/sonar-web/src/main/js/components/workspace/WorkspaceComponentViewer.tsx37
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceComponentViewer-test.tsx24
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/Workspace-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceComponentViewer-test.tsx.snap1
-rw-r--r--server/sonar-web/src/main/js/helpers/branch-like.ts11
-rw-r--r--server/sonar-web/src/main/js/helpers/mocks/quality-gates.ts15
-rw-r--r--server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx20
-rw-r--r--server/sonar-web/src/main/js/helpers/testSelector.ts29
-rw-r--r--server/sonar-web/src/main/js/helpers/testUtils.ts4
-rw-r--r--server/sonar-web/src/main/js/helpers/urls.ts5
-rw-r--r--server/sonar-web/src/main/js/queries/branch.tsx333
-rw-r--r--server/sonar-web/src/main/js/queries/withQueryClientHoc.tsx (renamed from server/sonar-web/src/main/js/app/components/branch-status/BranchStatusContext.tsx)34
-rw-r--r--server/sonar-web/src/main/js/types/__tests__/__snapshots__/component-test.ts.snap5
-rw-r--r--server/sonar-web/src/main/js/types/component.ts5
-rw-r--r--server/sonar-web/src/main/js/types/extension.ts2
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties6
107 files changed, 1825 insertions, 2291 deletions
diff --git a/server/sonar-web/src/main/js/api/mocks/BranchesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/BranchesServiceMock.ts
index fa6847c1382..af60faa5205 100644
--- a/server/sonar-web/src/main/js/api/mocks/BranchesServiceMock.ts
+++ b/server/sonar-web/src/main/js/api/mocks/BranchesServiceMock.ts
@@ -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);
diff --git a/server/sonar-web/src/main/js/api/mocks/SecurityHotspotServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/SecurityHotspotServiceMock.ts
index c5085c5ee96..a2086d31ab7 100644
--- a/server/sonar-web/src/main/js/api/mocks/SecurityHotspotServiceMock.ts
+++ b/server/sonar-web/src/main/js/api/mocks/SecurityHotspotServiceMock.ts
@@ -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: [
diff --git a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
index 0f01470dd74..d9c9c9e9864 100644
--- a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
+++ b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
@@ -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));
diff --git a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
index b1ed60e0ce4..05c5c8148ca 100644
--- a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
+++ b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
@@ -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>
diff --git a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
index 27c68dc4db1..9faf1d9486b 100644
--- a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
@@ -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/BranchStatusContextProvider.tsx b/server/sonar-web/src/main/js/app/components/branch-status/BranchStatusContextProvider.tsx
deleted file mode 100644
index cf2d2ba3279..00000000000
--- a/server/sonar-web/src/main/js/app/components/branch-status/BranchStatusContextProvider.tsx
+++ /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
index 4add5f75713..00000000000
--- a/server/sonar-web/src/main/js/app/components/branch-status/__tests__/BranchStatusContextProvider-test.tsx
+++ /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
index 94339400002..00000000000
--- a/server/sonar-web/src/main/js/app/components/branch-status/withBranchStatus.tsx
+++ /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
index 365c0301b51..00000000000
--- a/server/sonar-web/src/main/js/app/components/branch-status/withBranchStatusActions.tsx
+++ /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>
- );
- }
- };
-}
diff --git a/server/sonar-web/src/main/js/app/components/componentContext/ComponentContext.ts b/server/sonar-web/src/main/js/app/components/componentContext/ComponentContext.ts
index 4f19ef2eda3..8fd8ae528ea 100644
--- a/server/sonar-web/src/main/js/app/components/componentContext/ComponentContext.ts
+++ b/server/sonar-web/src/main/js/app/components/componentContext/ComponentContext.ts
@@ -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,
});
diff --git a/server/sonar-web/src/main/js/app/components/current-user/withCurrentUserContext.tsx b/server/sonar-web/src/main/js/app/components/current-user/withCurrentUserContext.tsx
index 7849c5e9554..9f0e85ba96d 100644
--- a/server/sonar-web/src/main/js/app/components/current-user/withCurrentUserContext.tsx
+++ b/server/sonar-web/src/main/js/app/components/current-user/withCurrentUserContext.tsx
@@ -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');
diff --git a/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx b/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx
index d3535c539be..179d690caa9 100644
--- a/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx
+++ b/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx
@@ -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)))))
);
diff --git a/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx b/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx
index 83046f78c3a..26ed70a56e9 100644
--- a/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx
+++ b/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx
@@ -19,13 +19,17 @@
*/
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} />
);
diff --git a/server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.tsx b/server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.tsx
index 747504f02a8..33b78f11844 100644
--- a/server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.tsx
+++ b/server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.tsx
@@ -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}`
diff --git a/server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectAdminPageExtension-test.tsx b/server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectAdminPageExtension-test.tsx
index e0d6a4c327d..5d7ed345ae8 100644
--- a/server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectAdminPageExtension-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectAdminPageExtension-test.tsx
@@ -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>
);
}
diff --git a/server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectPageExtension-test.tsx b/server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectPageExtension-test.tsx
index c2f8e871846..d61aeee9739 100644
--- a/server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectPageExtension-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/extensions/__tests__/ProjectPageExtension-test.tsx
@@ -17,11 +17,13 @@
* 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>
);
}
diff --git a/server/sonar-web/src/main/js/app/components/global-search/utils.ts b/server/sonar-web/src/main/js/app/components/global-search/utils.ts
index 4fbc1c4b1db..15ca34de7c3 100644
--- a/server/sonar-web/src/main/js/app/components/global-search/utils.ts
+++ b/server/sonar-web/src/main/js/app/components/global-search/utils.ts
@@ -21,7 +21,6 @@ import { sortBy } from 'lodash';
import { ComponentQualifier } from '../../../../js/types/component';
const ORDER = [
- ComponentQualifier.Developper,
ComponentQualifier.Portfolio,
ComponentQualifier.SubPortfolio,
ComponentQualifier.Application,
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorMessage.tsx b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorMessage.tsx
index 00b8bda2c62..0ec4af077df 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorMessage.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorMessage.tsx
@@ -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();
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorModal.tsx b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorModal.tsx
index bd368784421..ea77cdbc343 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorModal.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisErrorModal.tsx
@@ -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}
/>
)}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisStatus.tsx b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisStatus.tsx
index 4d8953398fa..a082485aa65 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/AnalysisStatus.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisStatus.tsx
@@ -19,25 +19,23 @@
*/
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
index 00000000000..90f7da97609
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/AnalysisWarningsModal.tsx
@@ -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);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
index 3f1db236c3b..f81c5a66e04 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
@@ -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}
</>
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/Header.tsx b/server/sonar-web/src/main/js/app/components/nav/component/Header.tsx
index d52c2dc6728..92e82ebb4dc 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/Header.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/Header.tsx
@@ -19,38 +19,26 @@
*/
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>
);
}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx b/server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx
index 56d22719501..5e1043531c2 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx
@@ -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 && (
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx
index a3a7111bc8f..d011f9d949e 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx
@@ -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);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisErrorMessage-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisErrorMessage-test.tsx
index 77b79601c60..c7adb1be843 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisErrorMessage-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/AnalysisErrorMessage-test.tsx
@@ -19,29 +19,39 @@
*/
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] }
);
}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
index 57bd0ba3d69..a3b5a039a84 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
@@ -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}
/>
);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/Header-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/Header-test.tsx
index 4dbfe4e9a12..4b515761886 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/Header-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/Header-test.tsx
@@ -20,19 +20,15 @@
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' }
);
}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/HeaderMeta-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/HeaderMeta-test.tsx
index cb92dda3fa1..3530de943b6 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/HeaderMeta-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/HeaderMeta-test.tsx
@@ -20,23 +20,39 @@
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],
+ });
}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/Menu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/Menu-test.tsx
index 599299e7e7b..2add79f18e5 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/Menu-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/Menu-test.tsx
@@ -20,22 +20,24 @@
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] }
);
}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx
index 4c6d2c54525..3b20d3b6f89 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx
@@ -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>
+ </>
);
}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLike.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLike.tsx
index 2de026da87e..e3bc2c89a41 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLike.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLike.tsx
@@ -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>
);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx
index ea1bfbc48f6..777f2d999fd 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx
@@ -176,7 +176,6 @@ export class Menu extends React.PureComponent<Props, State> {
/>
<MenuItemList
branchLikeTree={branchLikesToDisplayTree}
- component={component}
hasResults={hasResults}
onSelect={this.handleOnSelect}
selectedBranchLike={selectedBranchLike}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItem.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItem.tsx
index cc9b8644d52..dff3e81a1cd 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItem.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItem.tsx
@@ -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
/>
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItemList.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItemList.tsx
index 93ea2516d1f..c91f6b744b7 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItemList.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItemList.tsx
@@ -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)}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/QualityGateStatus.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/QualityGateStatus.tsx
index 861f7c84ea3..8ba0149febf 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/QualityGateStatus.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/QualityGateStatus.tsx
@@ -19,45 +19,34 @@
*/
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>
);
diff --git a/server/sonar-web/src/main/js/app/components/promotion-notification/PromotionNotification.tsx b/server/sonar-web/src/main/js/app/components/promotion-notification/PromotionNotification.tsx
index 003dd7c0525..f71537e379b 100644
--- a/server/sonar-web/src/main/js/app/components/promotion-notification/PromotionNotification.tsx
+++ b/server/sonar-web/src/main/js/app/components/promotion-notification/PromotionNotification.tsx
@@ -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]) {
diff --git a/server/sonar-web/src/main/js/app/components/promotion-notification/__tests__/PromotionNotification-test.tsx b/server/sonar-web/src/main/js/app/components/promotion-notification/__tests__/PromotionNotification-test.tsx
index cd11a4f17d2..4f30edf3a9c 100644
--- a/server/sonar-web/src/main/js/app/components/promotion-notification/__tests__/PromotionNotification-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/promotion-notification/__tests__/PromotionNotification-test.tsx
@@ -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/components/common/AnalysisWarningsModal.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/AnalysisWarningsModal.tsx
index 5ec98f9c259..c585b9a8da3 100644
--- a/server/sonar-web/src/main/js/components/common/AnalysisWarningsModal.tsx
+++ b/server/sonar-web/src/main/js/apps/background-tasks/components/AnalysisWarningsModal.tsx
@@ -25,20 +25,18 @@ import {
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';
+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[];
+ taskId: string;
}
interface State {
@@ -53,24 +51,20 @@ export class AnalysisWarningsModal extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
- loading: !props.warnings,
- warnings: props.warnings || [],
+ loading: false,
+ warnings: [],
};
}
componentDidMount() {
this.mounted = true;
- if (!this.props.warnings && this.props.taskId) {
- this.loadWarnings(this.props.taskId);
- }
+ this.loadWarnings(this.props.taskId);
}
componentDidUpdate(prevProps: Props) {
- const { taskId, warnings } = this.props;
- if (!warnings && taskId && prevProps.taskId !== taskId) {
+ const { taskId } = this.props;
+ if (prevProps.taskId !== taskId) {
this.loadWarnings(taskId);
- } else if (warnings && prevProps.warnings !== warnings) {
- this.setState({ warnings });
}
}
@@ -86,13 +80,8 @@ export class AnalysisWarningsModal extends React.PureComponent<Props, State> {
}
this.setState({ dismissedWarning: messageKey });
-
try {
await dismissAnalysisWarning(componentKey, messageKey);
-
- if (this.props.onWarningDismiss) {
- this.props.onWarningDismiss();
- }
} catch (e) {
// Noop
}
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx
index 4234161e1b6..e3533e0b197 100644
--- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx
+++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx
@@ -18,11 +18,11 @@
* 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';
diff --git a/server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx b/server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx
index af5d7b5255f..25845bd1ccb 100644
--- a/server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx
+++ b/server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx
@@ -17,17 +17,15 @@
* 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))));
diff --git a/server/sonar-web/src/main/js/apps/code/components/CodeAppRenderer.tsx b/server/sonar-web/src/main/js/apps/code/components/CodeAppRenderer.tsx
index 67fc4ee4697..d7ea12754eb 100644
--- a/server/sonar-web/src/main/js/apps/code/components/CodeAppRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/code/components/CodeAppRenderer.tsx
@@ -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>
)}
diff --git a/server/sonar-web/src/main/js/apps/code/components/SourceViewerWrapper.tsx b/server/sonar-web/src/main/js/apps/code/components/SourceViewerWrapper.tsx
index e1f557e7825..652a41f6837 100644
--- a/server/sonar-web/src/main/js/apps/code/components/SourceViewerWrapper.tsx
+++ b/server/sonar-web/src/main/js/apps/code/components/SourceViewerWrapper.tsx
@@ -18,18 +18,17 @@
* 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
/>
diff --git a/server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx b/server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx
index 05d8be68ed6..c235053b9b9 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx
@@ -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 }
);
}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx
index 13aaf746887..33a9263ee3c 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx
@@ -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} />;
}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx
index 19e3a95ca0c..f055182097d 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx
@@ -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>
) : (
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx
index 8fb3ba81a25..95a8382f216 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx
@@ -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>
);
}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx
index 51667902957..f97567175bf 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.tsx
@@ -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}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
index e3e6645d3d8..8206deb02b6 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
@@ -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);
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
index 9cb91a60ee3..3d38005558c 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
@@ -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
);
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-it.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-it.tsx
index b964c4f1172..aec43a577d0 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-it.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-it.tsx
@@ -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(
diff --git a/server/sonar-web/src/main/js/apps/issues/issues-subnavigation/__tests__/SubnavigationIssues-it.tsx b/server/sonar-web/src/main/js/apps/issues/issues-subnavigation/__tests__/SubnavigationIssues-it.tsx
index 3962f6ba981..04b3969f7dd 100644
--- a/server/sonar-web/src/main/js/apps/issues/issues-subnavigation/__tests__/SubnavigationIssues-it.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/issues-subnavigation/__tests__/SubnavigationIssues-it.tsx
@@ -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
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
index 005897e31d2..8e06b68f8d9 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
@@ -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" />
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx
index 020ae38d128..cef93cc97d2 100644
--- a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx
@@ -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;
}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/App.tsx b/server/sonar-web/src/main/js/apps/overview/components/App.tsx
index b3af0f38b15..96a4bcf287b 100644
--- a/server/sonar-web/src/main/js/apps/overview/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/components/App.tsx
@@ -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')} />
diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx
index 22395f7a113..eeaeaf0c1c5 100644
--- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx
@@ -19,14 +19,21 @@
*/
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'
);
}
diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx
index 7bb2f167366..d5be3ce0291 100644
--- a/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx
@@ -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) {
diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/PullRequestOverview-it.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/PullRequestOverview-it.tsx
index aba05081a7f..53473aeb27c 100644
--- a/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/PullRequestOverview-it.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/PullRequestOverview-it.tsx
@@ -17,17 +17,23 @@
* 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' })],
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchList.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchList.tsx
index f8e5ab4c6ac..857895cb8dd 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchList.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchList.tsx
@@ -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;
}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineApp.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineApp.tsx
index f28d2d251bd..3d73558be4d 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineApp.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineApp.tsx
@@ -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)))
+);
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
index e16920aae9c..deb5da7ea24 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
@@ -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">
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineApp-it.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineApp-it.tsx
index d1fc4e010eb..6a425ddc54c 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineApp-it.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineApp-it.tsx
@@ -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' }),
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/ProjectBranchesApp.tsx b/server/sonar-web/src/main/js/apps/projectBranches/ProjectBranchesApp.tsx
index 48407906d2f..c52f677190e 100644
--- a/server/sonar-web/src/main/js/apps/projectBranches/ProjectBranchesApp.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBranches/ProjectBranchesApp.tsx
@@ -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>
);
}
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/__tests__/ProjectBranchesApp-it.tsx b/server/sonar-web/src/main/js/apps/projectBranches/__tests__/ProjectBranchesApp-it.tsx
index 5b3a1650df1..08a57185893 100644
--- a/server/sonar-web/src/main/js/apps/projectBranches/__tests__/ProjectBranchesApp-it.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBranches/__tests__/ProjectBranchesApp-it.tsx
@@ -20,56 +20,61 @@
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] }
);
}
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeRow.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeRow.tsx
index 35d510b0e69..4f6c91183f5 100644
--- a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeRow.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeRow.tsx
@@ -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">
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTable.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTable.tsx
index 6cde13e2517..159b898de03 100644
--- a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTable.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTable.tsx
@@ -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>
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTabs.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTabs.tsx
index 1bd2f6705f0..9514fdd7373 100644
--- a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTabs.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTabs.tsx
@@ -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} />
+ )}
+ </>
+ );
}
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchPurgeSetting.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchPurgeSetting.tsx
index f6d2551e50b..c69b18521fc 100644
--- a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchPurgeSetting.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchPurgeSetting.tsx
@@ -18,88 +18,44 @@
* 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'
+ )}
+ />
+ )}
+ </>
+ );
}
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/DeleteBranchModal.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/DeleteBranchModal.tsx
index 9d7d2d422ae..c11ac8febe1 100644
--- a/server/sonar-web/src/main/js/apps/projectBranches/components/DeleteBranchModal.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/DeleteBranchModal.tsx
@@ -18,11 +18,11 @@
* 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>
+ );
}
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx
index 3b8580ce24e..d92b0794d42 100644
--- a/server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx
@@ -18,12 +18,13 @@
* 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>
+ );
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/CoverageFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/CoverageFilter-test.tsx
index 5a1798e26a1..bbb45b3b947 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/CoverageFilter-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/CoverageFilter-test.tsx
@@ -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}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguagesFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguagesFilter-test.tsx
index 80fdaf6143a..a8f55537c86 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguagesFilter-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguagesFilter-test.tsx
@@ -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={{
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/QualityGateFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/QualityGateFilter-test.tsx
index 699da7601bf..c21ab619191 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/QualityGateFilter-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/QualityGateFilter-test.tsx
@@ -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}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
index 0b5d3eae408..3d503e44137 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
@@ -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)))
);
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-it.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-it.tsx
index 69f8f057701..3806173420e 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-it.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-it.tsx
@@ -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',
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotHeader.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotHeader.tsx
index 55ec92e4390..ba7c40d5266 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotHeader.tsx
@@ -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
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx
index 1f2868d2870..cba93f8f925 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx
@@ -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) => {
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx
index e067668bf1e..39989709266 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx
@@ -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()}
diff --git a/server/sonar-web/src/main/js/components/activity-graph/EventInner.tsx b/server/sonar-web/src/main/js/components/activity-graph/EventInner.tsx
index 31996b65f5a..fa87946a783 100644
--- a/server/sonar-web/src/main/js/components/activity-graph/EventInner.tsx
+++ b/server/sonar-web/src/main/js/components/activity-graph/EventInner.tsx
@@ -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}>
diff --git a/server/sonar-web/src/main/js/components/activity-graph/__tests__/ActivityGraph-it.tsx b/server/sonar-web/src/main/js/components/activity-graph/__tests__/ActivityGraph-it.tsx
index 79cdb85ca3b..8f1d75b645d 100644
--- a/server/sonar-web/src/main/js/components/activity-graph/__tests__/ActivityGraph-it.tsx
+++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/ActivityGraph-it.tsx
@@ -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[]>([]);
diff --git a/server/sonar-web/src/main/js/components/activity-graph/__tests__/EventInner-it.tsx b/server/sonar-web/src/main/js/components/activity-graph/__tests__/EventInner-it.tsx
index 46540d728c7..3ba9228a84a 100644
--- a/server/sonar-web/src/main/js/components/activity-graph/__tests__/EventInner-it.tsx
+++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/EventInner-it.tsx
@@ -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/BranchStatus.tsx b/server/sonar-web/src/main/js/components/common/BranchStatus.tsx
index de5ef2406d0..016a54b3447 100644
--- a/server/sonar-web/src/main/js/components/common/BranchStatus.tsx
+++ b/server/sonar-web/src/main/js/components/common/BranchStatus.tsx
@@ -18,20 +18,19 @@
* 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);
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx
index 28bb2f1ddf8..ae64ae53f60 100644
--- a/server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx
+++ b/server/sonar-web/src/main/js/components/common/__tests__/AnalysisWarningsModal-test.tsx
@@ -18,13 +18,14 @@
* 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={[
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/BranchStatus-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/BranchStatus-test.tsx
index 886fd31dfbc..d2c1b766aee 100644
--- a/server/sonar-web/src/main/js/components/common/__tests__/BranchStatus-test.tsx
+++ b/server/sonar-web/src/main/js/components/common/__tests__/BranchStatus-test.tsx
@@ -17,24 +17,34 @@
* 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
index aeb2bda6418..00000000000
--- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchStatus-test.tsx.snap
+++ /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}
-/>
-`;
diff --git a/server/sonar-web/src/main/js/components/controls/BoxedTabs.tsx b/server/sonar-web/src/main/js/components/controls/BoxedTabs.tsx
index 1794ecd47c7..9aed4c3dcb4 100644
--- a/server/sonar-web/src/main/js/components/controls/BoxedTabs.tsx
+++ b/server/sonar-web/src/main/js/components/controls/BoxedTabs.tsx
@@ -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}`;
}
diff --git a/server/sonar-web/src/main/js/components/issue/Issue.tsx b/server/sonar-web/src/main/js/components/issue/Issue.tsx
index d1f7bd57f85..760025788b6 100644
--- a/server/sonar-web/src/main/js/components/issue/Issue.tsx
+++ b/server/sonar-web/src/main/js/components/issue/Issue.tsx
@@ -17,11 +17,14 @@
* 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}
+ />
+ );
}
diff --git a/server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx b/server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx
index e8bac03ed08..2560444a0b1 100644
--- a/server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx
+++ b/server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx
@@ -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 (
diff --git a/server/sonar-web/src/main/js/components/tutorials/TutorialSelection.tsx b/server/sonar-web/src/main/js/components/tutorials/TutorialSelection.tsx
index 83ebf05e9aa..3cb64f78607 100644
--- a/server/sonar-web/src/main/js/components/tutorials/TutorialSelection.tsx
+++ b/server/sonar-web/src/main/js/components/tutorials/TutorialSelection.tsx
@@ -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}
+ />
);
}
}
diff --git a/server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx b/server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx
index 4bdf4962281..e3328ad6ef7 100644
--- a/server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx
+++ b/server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx
@@ -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" />;
}
diff --git a/server/sonar-web/src/main/js/components/tutorials/__tests__/TutorialSelection-it.tsx b/server/sonar-web/src/main/js/components/tutorials/__tests__/TutorialSelection-it.tsx
index a13a514b11a..d8404cc1e4b 100644
--- a/server/sonar-web/src/main/js/components/tutorials/__tests__/TutorialSelection-it.tsx
+++ b/server/sonar-web/src/main/js/components/tutorials/__tests__/TutorialSelection-it.tsx
@@ -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}
diff --git a/server/sonar-web/src/main/js/components/workspace/WorkspaceComponentViewer.tsx b/server/sonar-web/src/main/js/components/workspace/WorkspaceComponentViewer.tsx
index fe7a4e1a8cd..5688285e13b 100644
--- a/server/sonar-web/src/main/js/components/workspace/WorkspaceComponentViewer.tsx
+++ b/server/sonar-web/src/main/js/components/workspace/WorkspaceComponentViewer.tsx
@@ -17,13 +17,8 @@
* 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);
diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceComponentViewer-test.tsx b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceComponentViewer-test.tsx
index f278e12fb9a..5efd0bdb0e6 100644
--- a/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceComponentViewer-test.tsx
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceComponentViewer-test.tsx
@@ -19,11 +19,7 @@
*/
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()}
diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/Workspace-test.tsx.snap b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/Workspace-test.tsx.snap
index e7ef8687609..8a4dba80db1 100644
--- a/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/Workspace-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/Workspace-test.tsx.snap
@@ -53,7 +53,7 @@ exports[`should render correctly: open component 1`] = `
}
}
/>
- <withBranchStatusActions(WorkspaceComponentViewer)
+ <WorkspaceComponentViewer
component={
{
"branchLike": {
diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceComponentViewer-test.tsx.snap b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceComponentViewer-test.tsx.snap
index c1190c2ad02..65ba4da4254 100644
--- a/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceComponentViewer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceComponentViewer-test.tsx.snap
@@ -34,7 +34,6 @@ exports[`should render 1`] = `
displayIssueLocationsCount={true}
displayIssueLocationsLink={true}
displayLocationMarkers={true}
- onIssueChange={[Function]}
onLoaded={[Function]}
/>
</div>
diff --git a/server/sonar-web/src/main/js/helpers/branch-like.ts b/server/sonar-web/src/main/js/helpers/branch-like.ts
index 1b8f75fcb6d..f5c6f5d5f51 100644
--- a/server/sonar-web/src/main/js/helpers/branch-like.ts
+++ b/server/sonar-web/src/main/js/helpers/branch-like.ts
@@ -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];
-}
diff --git a/server/sonar-web/src/main/js/helpers/mocks/quality-gates.ts b/server/sonar-web/src/main/js/helpers/mocks/quality-gates.ts
index 2a4a246f406..55c791d724a 100644
--- a/server/sonar-web/src/main/js/helpers/mocks/quality-gates.ts
+++ b/server/sonar-web/src/main/js/helpers/mocks/quality-gates.ts
@@ -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 {
diff --git a/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx b/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx
index 2ef2063a9ea..3b8fcced33e 100644
--- a/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx
+++ b/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx
@@ -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 });
},
diff --git a/server/sonar-web/src/main/js/helpers/testSelector.ts b/server/sonar-web/src/main/js/helpers/testSelector.ts
index 5a7a99f8e66..0f6eae1adb8 100644
--- a/server/sonar-web/src/main/js/helpers/testSelector.ts
+++ b/server/sonar-web/src/main/js/helpers/testSelector.ts
@@ -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));
}
diff --git a/server/sonar-web/src/main/js/helpers/testUtils.ts b/server/sonar-web/src/main/js/helpers/testUtils.ts
index 2a7e904c232..3506f663e3c 100644
--- a/server/sonar-web/src/main/js/helpers/testUtils.ts
+++ b/server/sonar-web/src/main/js/helpers/testUtils.ts
@@ -18,10 +18,12 @@
* 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 {
diff --git a/server/sonar-web/src/main/js/helpers/urls.ts b/server/sonar-web/src/main/js/helpers/urls.ts
index a5a36882791..e00acea704f 100644
--- a/server/sonar-web/src/main/js/helpers/urls.ts
+++ b/server/sonar-web/src/main/js/helpers/urls.ts
@@ -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
index 00000000000..1d99799b290
--- /dev/null
+++ b/server/sonar-web/src/main/js/queries/branch.tsx
@@ -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/app/components/branch-status/BranchStatusContext.tsx b/server/sonar-web/src/main/js/queries/withQueryClientHoc.tsx
index 57c4ee1f654..21beffc5ec7 100644
--- a/server/sonar-web/src/main/js/app/components/branch-status/BranchStatusContext.tsx
+++ b/server/sonar-web/src/main/js/queries/withQueryClientHoc.tsx
@@ -17,29 +17,17 @@
* 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 { BranchLike, BranchStatusData } from '../../../types/branch-like';
-import { QualityGateStatusCondition } from '../../../types/quality-gates';
-import { Dict, Status } from '../../../types/types';
+import { ComponentClass, VFC } from 'react';
-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 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} />;
+ };
}
-
-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/types/__tests__/__snapshots__/component-test.ts.snap b/server/sonar-web/src/main/js/types/__tests__/__snapshots__/component-test.ts.snap
index ab5a3b6fd8e..11feb73a504 100644
--- a/server/sonar-web/src/main/js/types/__tests__/__snapshots__/component-test.ts.snap
+++ b/server/sonar-web/src/main/js/types/__tests__/__snapshots__/component-test.ts.snap
@@ -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,
diff --git a/server/sonar-web/src/main/js/types/component.ts b/server/sonar-web/src/main/js/types/component.ts
index e0b98f13d3e..4e7a678cc5c 100644
--- a/server/sonar-web/src/main/js/types/component.ts
+++ b/server/sonar-web/src/main/js/types/component.ts
@@ -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;
}
diff --git a/server/sonar-web/src/main/js/types/extension.ts b/server/sonar-web/src/main/js/types/extension.ts
index 1f70b29f9cc..efcf9622b45 100644
--- a/server/sonar-web/src/main/js/types/extension.ts
+++ b/server/sonar-web/src/main/js/types/extension.ts
@@ -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;
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index 79aa25884c0..41b5a82ee18 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -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.