From a38eda46c2f174efe54e6a584f12895a3d3c7f3c Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Tue, 22 Mar 2022 16:24:08 +0100 Subject: [PATCH] SONAR-15914 Extract branchstatus from redux --- .../js/app/components/ComponentContainer.tsx | 13 +- .../js/app/components/GlobalContainer.tsx | 29 +- .../__tests__/ComponentContainer-test.tsx | 8 +- .../GlobalContainer-test.tsx.snap | 54 +- .../branch-status/BranchStatusContext.tsx | 46 ++ .../BranchStatusContextProvider.tsx | 100 +++ .../BranchStatusContextProvider-test.tsx | 73 ++ .../branch-status/withBranchStatus.tsx | 59 ++ .../branch-status/withBranchStatusActions.tsx | 51 ++ .../components/nav/component/HeaderMeta.tsx | 2 +- .../__snapshots__/HeaderMeta-test.tsx.snap | 27 +- .../nav/component/branch-like/MenuItem.tsx | 2 +- .../__snapshots__/MenuItem-test.tsx.snap | 50 +- .../main/js/apps/code/components/CodeApp.tsx | 18 +- .../component-measures/components/App.tsx | 7 +- .../apps/issues/components/AppContainer.tsx | 31 +- .../js/apps/issues/components/IssuesApp.tsx | 21 +- .../__tests__/AppContainer-test.tsx | 50 -- .../components/__tests__/IssuesApp-test.tsx | 124 ++-- .../__snapshots__/IssuesApp-test.tsx.snap | 671 ------------------ .../pullRequests/PullRequestOverview.tsx | 41 +- .../components/BranchLikeRow.tsx | 2 +- .../__snapshots__/BranchLikeRow-test.tsx.snap | 75 +- .../security-hotspots/SecurityHotspotsApp.tsx | 7 +- .../js/components/common/BranchStatus.tsx | 24 +- .../common/__tests__/BranchStatus-test.tsx | 18 +- .../__snapshots__/BranchStatus-test.tsx.snap | 8 +- .../workspace/WorkspaceComponentViewer.tsx | 7 +- .../src/main/js/helpers/branch-like.ts | 11 + .../src/main/js/helpers/testMocks.ts | 26 + .../main/js/store/__tests__/branches-test.ts | 149 ---- ...ducers-test.tsx => globalMessages-test.ts} | 32 +- .../sonar-web/src/main/js/store/branches.ts | 115 --- .../src/main/js/store/globalMessages.ts | 2 +- .../src/main/js/store/rootReducer.ts | 12 - .../src/main/js/types/branch-like.ts | 7 + 36 files changed, 721 insertions(+), 1251 deletions(-) create mode 100644 server/sonar-web/src/main/js/app/components/branch-status/BranchStatusContext.tsx create mode 100644 server/sonar-web/src/main/js/app/components/branch-status/BranchStatusContextProvider.tsx create mode 100644 server/sonar-web/src/main/js/app/components/branch-status/__tests__/BranchStatusContextProvider-test.tsx create mode 100644 server/sonar-web/src/main/js/app/components/branch-status/withBranchStatus.tsx create mode 100644 server/sonar-web/src/main/js/app/components/branch-status/withBranchStatusActions.tsx delete mode 100644 server/sonar-web/src/main/js/apps/issues/components/__tests__/AppContainer-test.tsx delete mode 100644 server/sonar-web/src/main/js/store/__tests__/branches-test.ts rename server/sonar-web/src/main/js/store/__tests__/{rootReducers-test.tsx => globalMessages-test.ts} (52%) delete mode 100644 server/sonar-web/src/main/js/store/branches.ts 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 fc98bc60e17..a3d571fcb2e 100644 --- a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx @@ -19,7 +19,6 @@ */ import { differenceBy } from 'lodash'; import * as React from 'react'; -import { connect } from 'react-redux'; import { getProjectAlmBinding, validateProjectAlmBinding } from '../../api/alm-settings'; import { getBranches, getPullRequests } from '../../api/branches'; import { getAnalysisStatus, getTasksForComponent } from '../../api/ce'; @@ -34,7 +33,6 @@ import { } from '../../helpers/branch-like'; import { HttpStatus } from '../../helpers/request'; import { getPortfolioUrl } from '../../helpers/urls'; -import { registerBranchStatus } from '../../store/branches'; import { ProjectAlmBindingConfigurationErrors, ProjectAlmBindingResponse @@ -46,6 +44,7 @@ import { Task, TaskStatuses, TaskTypes, TaskWarning } from '../../types/tasks'; import { Component, Status } from '../../types/types'; import handleRequiredAuthorization from '../utils/handleRequiredAuthorization'; import withAppStateContext from './app-state/withAppStateContext'; +import withBranchStatusActions from './branch-status/withBranchStatusActions'; import ComponentContainerNotFound from './ComponentContainerNotFound'; import { ComponentContext } from './ComponentContext'; import PageUnavailableDueToIndexation from './indexation/PageUnavailableDueToIndexation'; @@ -55,7 +54,7 @@ interface Props { appState: AppState; children: React.ReactElement; location: Pick; - registerBranchStatus: (branchLike: BranchLike, component: string, status: Status) => void; + updateBranchStatus: (branchLike: BranchLike, component: string, status: Status) => void; router: Pick; } @@ -359,7 +358,7 @@ export class ComponentContainer extends React.PureComponent { registerBranchStatuses = (branchLikes: BranchLike[], component: Component) => { branchLikes.forEach(branchLike => { if (branchLike.status) { - this.props.registerBranchStatus( + this.props.updateBranchStatus( branchLike, component.key, branchLike.status.qualityGateStatus @@ -467,8 +466,4 @@ export class ComponentContainer extends React.PureComponent { } } -const mapDispatchToProps = { registerBranchStatus }; - -export default withRouter( - connect(null, mapDispatchToProps)(withAppStateContext(ComponentContainer)) -); +export default withRouter(withAppStateContext(withBranchStatusActions(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 52a5eccf943..3ee41a0606e 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx @@ -21,6 +21,7 @@ import * as React from 'react'; import Workspace from '../../components/workspace/Workspace'; import A11yProvider from './a11y/A11yProvider'; import A11ySkipLinks from './a11y/A11ySkipLinks'; +import BranchStatusContextProvider from './branch-status/BranchStatusContextProvider'; import SuggestionsProvider from './embed-docs-modal/SuggestionsProvider'; import GlobalFooter from './GlobalFooter'; import GlobalMessagesContainer from './GlobalMessagesContainer'; @@ -50,19 +51,21 @@ export default function GlobalContainer(props: Props) {
- - - - - - - - - {props.children} - - - - + + + + + + + + + + {props.children} + + + + +
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 a0bbceabb68..f1c4226abca 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 @@ -144,10 +144,10 @@ it("doesn't load branches portfolio", async () => { }); it('updates branches on change', async () => { - const registerBranchStatus = jest.fn(); + const updateBranchStatus = jest.fn(); const wrapper = shallowRender({ location: mockLocation({ query: { id: 'portfolioKey' } }), - registerBranchStatus + updateBranchStatus }); wrapper.setState({ branchLikes: [mockMainBranch()], @@ -160,7 +160,7 @@ it('updates branches on change', async () => { expect(getBranches).toBeCalledWith('projectKey'); expect(getPullRequests).toBeCalledWith('projectKey'); await waitAndUpdate(wrapper); - expect(registerBranchStatus).toBeCalledTimes(2); + expect(updateBranchStatus).toBeCalledTimes(2); }); it('fetches status', async () => { @@ -441,7 +441,7 @@ function shallowRender(props: Partial = {}) { diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalContainer-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalContainer-test.tsx.snap index ab5e5905229..2f15d0754f4 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalContainer-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalContainer-test.tsx.snap @@ -15,33 +15,35 @@ exports[`should render correctly 1`] = `
- - - - - + + + + + - - - - - - - - + /> + + + + + + + + +
diff --git a/server/sonar-web/src/main/js/app/components/branch-status/BranchStatusContext.tsx b/server/sonar-web/src/main/js/app/components/branch-status/BranchStatusContext.tsx new file mode 100644 index 00000000000..64220f8c13b --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/branch-status/BranchStatusContext.tsx @@ -0,0 +1,46 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import * as React from 'react'; +import { BranchLike, BranchStatusData } from '../../../types/branch-like'; +import { QualityGateStatusCondition } from '../../../types/quality-gates'; +import { Dict, Status } from '../../../types/types'; + +export interface BranchStatusContextInterface { + branchStatusByComponent: Dict>; + fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => void; + updateBranchStatus: ( + branchLike: BranchLike, + projectKey: string, + status: Status, + conditions?: QualityGateStatusCondition[], + ignoredConditions?: boolean + ) => void; +} + +export const BranchStatusContext = React.createContext({ + branchStatusByComponent: {}, + fetchBranchStatus: () => { + throw Error('BranchStatusContext is not provided'); + }, + updateBranchStatus: () => { + throw Error('BranchStatusContext is not provided'); + } +}); diff --git a/server/sonar-web/src/main/js/app/components/branch-status/BranchStatusContextProvider.tsx b/server/sonar-web/src/main/js/app/components/branch-status/BranchStatusContextProvider.tsx new file mode 100644 index 00000000000..fc0e828d5a2 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/branch-status/BranchStatusContextProvider.tsx @@ -0,0 +1,100 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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>; +} + +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 ( + + {this.props.children} + + ); + } +} 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 new file mode 100644 index 00000000000..9b9e26dfea3 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/branch-status/__tests__/BranchStatusContextProvider-test.tsx @@ -0,0 +1,73 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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 { QualityGateProjectStatus } from '../../../../types/quality-gates'; +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: QualityGateProjectStatus = { + status: 'OK', + conditions: [], + ignoredConditions: false + }; + (getQualityGateProjectStatus as jest.Mock).mockResolvedValueOnce(status); + const wrapper = shallowRender(); + + wrapper.instance().fetchBranchStatus(mockBranch({ name: branchName }), projectKey); + + expect(getQualityGateProjectStatus).toBeCalledWith({ 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( + +
+ + ); +} 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 new file mode 100644 index 00000000000..8f1ccf414de --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/branch-status/withBranchStatus.tsx @@ -0,0 +1,59 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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

) { + return class WithBranchStatus extends React.PureComponent> { + static displayName = getWrappedDisplayName(WrappedComponent, 'withBranchStatus'); + + render() { + const { branchLike, component } = this.props; + + return ( + + {({ branchStatusByComponent }) => { + const { conditions, ignoredConditions, status } = getBranchStatusByBranchLike( + branchStatusByComponent, + component.key, + branchLike + ); + + return ( + + ); + }} + + ); + } + }; +} 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 new file mode 100644 index 00000000000..97ffc3de903 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/branch-status/withBranchStatusActions.tsx @@ -0,0 +1,51 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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 + | Pick; + +export default function withBranchStatusActions

( + WrappedComponent: React.ComponentType

+) { + return class WithBranchStatusActions extends React.PureComponent< + Omit + > { + static displayName = getWrappedDisplayName(WrappedComponent, 'withBranchStatusActions'); + + render() { + return ( + + {({ fetchBranchStatus, updateBranchStatus }) => ( + + )} + + ); + } + }; +} 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 f77c4429974..1729155f1a0 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 @@ -86,7 +86,7 @@ export function HeaderMeta(props: HeaderMetaProps) { )} - +

)} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/HeaderMeta-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/HeaderMeta-test.tsx.snap index 14a257a1b3c..110542461d9 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/HeaderMeta-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/HeaderMeta-test.tsx.snap @@ -196,7 +196,7 @@ exports[`should render correctly for a pull request 1`] = ` size={12} /> - 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 eeb24400e8a..298ae9448f0 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 @@ -58,7 +58,7 @@ export function MenuItem(props: MenuItemProps) { )}
- +
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItem-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItem-test.tsx.snap index 393f960966d..e9df1e0578f 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItem-test.tsx.snap @@ -36,7 +36,7 @@ exports[`should render a main branch correctly 1`] = `
-
@@ -85,7 +106,7 @@ exports[`should render a non-main branch, indented and selected item correctly 1
-
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 57bb881e842..c6e55679357 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 @@ -24,9 +24,9 @@ import { Location } from 'history'; import { debounce, intersection } from 'lodash'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; -import { connect } from 'react-redux'; import { InjectedRouter } from 'react-router'; import A11ySkipTarget from '../../../app/components/a11y/A11ySkipTarget'; +import withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions'; import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; import withMetricsContext from '../../../app/components/metrics/withMetricsContext'; import HelpTooltip from '../../../components/controls/HelpTooltip'; @@ -35,7 +35,6 @@ import { Alert } from '../../../components/ui/Alert'; import { isPullRequest, isSameBranchLike } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; import { getCodeUrl, getProjectUrl } from '../../../helpers/urls'; -import { fetchBranchStatus } from '../../../store/branches'; import { BranchLike } from '../../../types/branch-like'; import { isPortfolioLike } from '../../../types/component'; import { Breadcrumb, Component, ComponentMeasure, Dict, Issue, Metric } from '../../../types/types'; @@ -52,20 +51,15 @@ import Components from './Components'; import Search from './Search'; import SourceViewerWrapper from './SourceViewerWrapper'; -interface DispatchToProps { - fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => Promise; -} - -interface OwnProps { +interface Props { branchLike?: BranchLike; component: Component; + fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => Promise; location: Pick; router: Pick; metrics: Dict; } -type Props = DispatchToProps & OwnProps; - interface State { baseComponent?: ComponentMeasure; breadcrumbs: Breadcrumb[]; @@ -404,8 +398,4 @@ const AlertContent = styled.div` align-items: center; `; -const mapDispatchToProps: DispatchToProps = { - fetchBranchStatus: fetchBranchStatus as any -}; - -export default connect(null, mapDispatchToProps)(withMetricsContext(CodeApp)); +export default withBranchStatusActions(withMetricsContext(CodeApp)); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/App.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/App.tsx index ce61093edf2..df8f249abe2 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/App.tsx @@ -22,10 +22,10 @@ import key from 'keymaster'; import { debounce, keyBy } from 'lodash'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; -import { connect } from 'react-redux'; import { withRouter, WithRouterProps } from 'react-router'; import { getMeasuresWithPeriod } from '../../../api/measures'; import { getAllMetrics } from '../../../api/metrics'; +import withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions'; import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; import HelpTooltip from '../../../components/controls/HelpTooltip'; @@ -44,7 +44,6 @@ import { removeSideBarClass, removeWhitePageClass } from '../../../helpers/pages'; -import { fetchBranchStatus } from '../../../store/branches'; import { BranchLike } from '../../../types/branch-like'; import { ComponentQualifier, isPortfolioLike } from '../../../types/component'; import { @@ -354,11 +353,9 @@ export class App extends React.PureComponent { } } -const mapDispatchToProps = { fetchBranchStatus: fetchBranchStatus as any }; - const AlertContent = styled.div` display: flex; align-items: center; `; -export default withRouter(connect(null, mapDispatchToProps)(App)); +export default withRouter(withBranchStatusActions(App)); diff --git a/server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx b/server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx index 49e326575cb..e3060ab0d0f 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx @@ -17,38 +17,11 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { connect } from 'react-redux'; -import { searchIssues } from '../../../api/issues'; +import withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions'; import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; import { withRouter } from '../../../components/hoc/withRouter'; import { lazyLoadComponent } from '../../../components/lazyLoadComponent'; -import { parseIssueFromResponse } from '../../../helpers/issues'; -import { fetchBranchStatus } from '../../../store/branches'; -import { Store } from '../../../store/rootReducer'; -import { FetchIssuesPromise } from '../../../types/issues'; -import { RawQuery } from '../../../types/types'; const IssuesAppContainer = lazyLoadComponent(() => import('./IssuesApp'), 'IssuesAppContainer'); -const fetchIssues = (query: RawQuery) => { - return searchIssues({ - ...query, - additionalFields: '_all', - timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone - }).then(response => { - const parsedIssues = response.issues.map(issue => - parseIssueFromResponse(issue, response.components, response.users, response.rules) - ); - return { ...response, issues: parsedIssues } as FetchIssuesPromise; - }); -}; - -const mapStateToProps = (_state: Store) => ({ - fetchIssues -}); - -const mapDispatchToProps = { fetchBranchStatus }; - -export default withRouter( - withCurrentUserContext(connect(mapStateToProps, mapDispatchToProps)(IssuesAppContainer)) -); +export default withRouter(withCurrentUserContext(withBranchStatusActions(IssuesAppContainer))); 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 92db8e0acc8..7da860bcc43 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 @@ -23,6 +23,7 @@ import { debounce, 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 A11ySkipTarget from '../../../app/components/a11y/A11ySkipTarget'; import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; import EmptySearch from '../../../components/common/EmptySearch'; @@ -43,6 +44,7 @@ import { isSameBranchLike } from '../../../helpers/branch-like'; import handleRequiredAuthentication from '../../../helpers/handleRequiredAuthentication'; +import { parseIssueFromResponse } from '../../../helpers/issues'; import { KeyboardCodes, KeyboardKeys } from '../../../helpers/keycodes'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { @@ -97,7 +99,6 @@ interface Props { component?: Component; currentUser: CurrentUser; fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => void; - fetchIssues: (query: RawQuery) => Promise; location: Location; onBranchesChange?: () => void; router: Pick; @@ -405,6 +406,19 @@ export default class App extends React.PureComponent { createdAfterIncludesTime = () => Boolean(this.props.location.query.createdAfter?.includes('T')); + fetchIssuesHelper = (query: RawQuery) => { + return searchIssues({ + ...query, + additionalFields: '_all', + timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone + }).then(response => { + const parsedIssues = response.issues.map(issue => + parseIssueFromResponse(issue, response.components, response.users, response.rules) + ); + return { ...response, issues: parsedIssues } as FetchIssuesPromise; + }); + }; + fetchIssues = (additional: RawQuery, requestFacets = false): Promise => { const { component } = this.props; const { myIssues, openFacets, query } = this.state; @@ -437,7 +451,8 @@ export default class App extends React.PureComponent { if (myIssues) { Object.assign(parameters, { assignees: '__me__' }); } - return this.props.fetchIssues(parameters); + + return this.fetchIssuesHelper(parameters); }; fetchFirstIssues() { @@ -701,7 +716,7 @@ export default class App extends React.PureComponent { Object.assign(parameters, { assignees: '__me__' }); } - return this.props.fetchIssues(parameters).then(({ facets }) => parseFacets(facets)[property]); + return this.fetchIssuesHelper(parameters).then(({ facets }) => parseFacets(facets)[property]); }; closeFacet = (property: string) => { diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/AppContainer-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/AppContainer-test.tsx deleted file mode 100644 index bc78aeab32e..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/AppContainer-test.tsx +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { connect } from 'react-redux'; -import { searchIssues } from '../../../../api/issues'; -import { fetchBranchStatus } from '../../../../store/branches'; -import '../AppContainer'; - -jest.mock('react-redux', () => ({ - connect: jest.fn(() => (a: any) => a) -})); - -jest.mock('../../../../api/issues', () => ({ - searchIssues: jest.fn().mockResolvedValue({ issues: [{ some: 'issue' }], bar: 'baz' }) -})); - -jest.mock('../../../../helpers/issues', () => ({ - parseIssueFromResponse: jest.fn(() => 'parsedIssue') -})); - -describe('redux', () => { - it('should correctly map state and dispatch props', async () => { - const [mapStateToProps, mapDispatchToProps] = (connect as jest.Mock).mock.calls[0]; - const { fetchIssues } = mapStateToProps({}); - - expect(mapDispatchToProps).toEqual(expect.objectContaining({ fetchBranchStatus })); - - const result = await fetchIssues({ foo: 'bar' }); - expect(searchIssues).toBeCalledWith( - expect.objectContaining({ foo: 'bar', additionalFields: '_all' }) - ); - expect(result).toEqual({ issues: ['parsedIssue'], bar: 'baz' }); - }); -}); diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx index 0e4ed2e7a22..5a43255cc15 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx @@ -20,6 +20,7 @@ import { shallow } from 'enzyme'; import key from 'keymaster'; import * as React from 'react'; +import { searchIssues } from '../../../../api/issues'; import handleRequiredAuthentication from '../../../../helpers/handleRequiredAuthentication'; import { KeyboardCodes, KeyboardKeys } from '../../../../helpers/keycodes'; import { mockPullRequest } from '../../../../helpers/mocks/branch-like'; @@ -36,10 +37,12 @@ import { mockIssue, mockLocation, mockLoggedInUser, + mockRawIssue, mockRouter } from '../../../../helpers/testMocks'; import { KEYCODE_MAP, keydown, waitAndUpdate } from '../../../../helpers/testUtils'; import { ComponentQualifier } from '../../../../types/component'; +import { ReferencedComponent } from '../../../../types/issues'; import { Issue, Paging } from '../../../../types/types'; import { disableLocationsNavigator, @@ -51,6 +54,7 @@ import { } from '../../actions'; import BulkChangeModal from '../BulkChangeModal'; import App from '../IssuesApp'; +import IssuesSourceViewer from '../IssuesSourceViewer'; jest.mock('../../../../helpers/pages', () => ({ addSideBarClass: jest.fn(), @@ -82,6 +86,16 @@ jest.mock('keymaster', () => { return key; }); +jest.mock('../../../../api/issues', () => ({ + searchIssues: jest.fn().mockResolvedValue({ facets: [], issues: [] }) +})); + +const RAW_ISSUES = [ + mockRawIssue(false, { key: 'foo' }), + mockRawIssue(false, { key: 'bar' }), + mockRawIssue(true, { key: 'third' }), + mockRawIssue(false, { key: 'fourth' }) +]; const ISSUES = [ mockIssue(false, { key: 'foo' }), mockIssue(false, { key: 'bar' }), @@ -91,7 +105,7 @@ const ISSUES = [ const FACETS = [{ property: 'severities', values: [{ val: 'MINOR', count: 4 }] }]; const PAGING = { pageIndex: 1, pageSize: 100, total: 4 }; -const referencedComponent = { key: 'foo-key', name: 'bar', uuid: 'foo-uuid' }; +const referencedComponent: ReferencedComponent = { key: 'foo-key', name: 'bar', uuid: 'foo-uuid' }; const originalAddEventListener = window.addEventListener; const originalRemoveEventListener = window.removeEventListener; @@ -103,6 +117,17 @@ beforeEach(() => { Object.defineProperty(window, 'removeEventListener', { value: jest.fn() }); + + (searchIssues as jest.Mock).mockResolvedValue({ + components: [referencedComponent], + effortTotal: 1, + facets: FACETS, + issues: RAW_ISSUES, + languages: [], + paging: PAGING, + rules: [], + users: [] + }); }); afterEach(() => { @@ -112,6 +137,9 @@ afterEach(() => { Object.defineProperty(window, 'removeEventListener', { value: originalRemoveEventListener }); + + jest.clearAllMocks(); + (searchIssues as jest.Mock).mockReset(); }); it('should show warnning when not all projects are accessible', () => { @@ -205,11 +233,11 @@ it('should open standard facets for vulnerabilities and hotspots', () => { it('should switch to source view if an issue is selected', async () => { const wrapper = shallowRender(); await waitAndUpdate(wrapper); - expect(wrapper).toMatchSnapshot(); + expect(wrapper.find(IssuesSourceViewer).exists()).toBe(false); wrapper.setProps({ location: mockLocation({ query: { open: 'third' } }) }); await waitAndUpdate(wrapper); - expect(wrapper).toMatchSnapshot(); + expect(wrapper.find(IssuesSourceViewer).exists()).toBe(true); }); it('should correctly bind key events for issue navigation', async () => { @@ -297,18 +325,18 @@ it('should be able to check all issue with global checkbox', async () => { }); it('should check all issues, even the ones that are not visible', async () => { - const wrapper = shallowRender({ - fetchIssues: jest.fn().mockResolvedValue({ - components: [referencedComponent], - effortTotal: 1, - facets: FACETS, - issues: ISSUES, - languages: [], - paging: { pageIndex: 1, pageSize: 100, total: 250 }, - rules: [], - users: [] - }) + (searchIssues as jest.Mock).mockResolvedValueOnce({ + components: [referencedComponent], + effortTotal: 1, + facets: FACETS, + issues: ISSUES, + languages: [], + paging: { pageIndex: 1, pageSize: 100, total: 250 }, + rules: [], + users: [] }); + + const wrapper = shallowRender(); const instance = wrapper.instance(); await waitAndUpdate(wrapper); @@ -319,18 +347,17 @@ it('should check all issues, even the ones that are not visible', async () => { }); it('should check max 500 issues', async () => { - const wrapper = shallowRender({ - fetchIssues: jest.fn().mockResolvedValue({ - components: [referencedComponent], - effortTotal: 1, - facets: FACETS, - issues: ISSUES, - languages: [], - paging: { pageIndex: 1, pageSize: 100, total: 1000 }, - rules: [], - users: [] - }) + (searchIssues as jest.Mock).mockResolvedValue({ + components: [referencedComponent], + effortTotal: 1, + facets: FACETS, + issues: ISSUES, + languages: [], + paging: { pageIndex: 1, pageSize: 100, total: 1000 }, + rules: [], + users: [] }); + const wrapper = shallowRender(); const instance = wrapper.instance(); await waitAndUpdate(wrapper); @@ -342,8 +369,8 @@ it('should check max 500 issues', async () => { }); it('should fetch issues for component', async () => { + (searchIssues as jest.Mock).mockImplementation(mockSearchIssuesResponse()); const wrapper = shallowRender({ - fetchIssues: fetchIssuesMockFactory(), location: mockLocation({ query: { open: '0' } }) @@ -405,11 +432,12 @@ it('should correctly handle filter changes', () => { }); it('should fetch issues until defined', async () => { + (searchIssues as jest.Mock).mockImplementation(mockSearchIssuesResponse()); + const mockDone = (_: Issue[], paging: Paging) => paging.total <= paging.pageIndex * paging.pageSize; const wrapper = shallowRender({ - fetchIssues: fetchIssuesMockFactory(), location: mockLocation({ query: { open: '0' } }) @@ -488,9 +516,8 @@ describe('keyup event handler', () => { }); it('should fetch more issues', async () => { - const wrapper = shallowRender({ - fetchIssues: fetchIssuesMockFactory() - }); + (searchIssues as jest.Mock).mockImplementation(mockSearchIssuesResponse()); + const wrapper = shallowRender({}); const instance = wrapper.instance(); await waitAndUpdate(wrapper); @@ -509,7 +536,7 @@ it('should refresh branch status if issues are updated', async () => { const updatedIssue: Issue = { ...ISSUES[0], type: 'SECURITY_HOTSPOT' }; instance.handleIssueChange(updatedIssue); - expect(wrapper.state().issues).toEqual([updatedIssue, ISSUES[1], ISSUES[2], ISSUES[3]]); + expect(wrapper.state().issues[0].type).toEqual(updatedIssue.type); expect(fetchBranchStatus).toBeCalledWith(branchLike, component.key); fetchBranchStatus.mockClear(); @@ -530,9 +557,9 @@ it('should update the open issue when it is changed', async () => { }); it('should handle createAfter query param with time', async () => { - const fetchIssues = fetchIssuesMockFactory(); + (searchIssues as jest.Mock).mockImplementation(mockSearchIssuesResponse()); + const wrapper = shallowRender({ - fetchIssues, location: mockLocation({ query: { createdAfter: '2020-10-21' } }) }); expect(wrapper.instance().createdAfterIncludesTime()).toBe(false); @@ -541,23 +568,23 @@ it('should handle createAfter query param with time', async () => { wrapper.setProps({ location: mockLocation({ query: { createdAfter: '2020-10-21T17:21:00Z' } }) }); expect(wrapper.instance().createdAfterIncludesTime()).toBe(true); - fetchIssues.mockClear(); + (searchIssues as jest.Mock).mockClear(); wrapper.instance().fetchIssues({}); - expect(fetchIssues).toBeCalledWith( + expect(searchIssues).toBeCalledWith( expect.objectContaining({ createdAfter: '2020-10-21T17:21:00+0000' }) ); }); -function fetchIssuesMockFactory(keyCount = 0, lineCount = 1) { - return jest.fn().mockImplementation(({ p }: { p: number }) => +function mockSearchIssuesResponse(keyCount = 0, lineCount = 1) { + return ({ p = 1 }) => Promise.resolve({ components: [referencedComponent], effortTotal: 1, facets: FACETS, issues: [ - mockIssue(false, { - key: '' + keyCount++, + mockRawIssue(false, { + key: `${keyCount++}`, textRange: { startLine: lineCount++, endLine: lineCount, @@ -565,8 +592,8 @@ function fetchIssuesMockFactory(keyCount = 0, lineCount = 1) { endOffset: 15 } }), - mockIssue(false, { - key: '' + keyCount++, + mockRawIssue(false, { + key: `${keyCount}`, textRange: { startLine: lineCount++, endLine: lineCount, @@ -576,11 +603,10 @@ function fetchIssuesMockFactory(keyCount = 0, lineCount = 1) { }) ], languages: [], - paging: { pageIndex: p || 1, pageSize: 2, total: 6 }, + paging: { pageIndex: p, pageSize: 2, total: 6 }, rules: [], users: [] - }) - ); + }); } function shallowRender(props: Partial = {}) { @@ -594,16 +620,6 @@ function shallowRender(props: Partial = {}) { }} currentUser={mockLoggedInUser()} fetchBranchStatus={jest.fn()} - fetchIssues={jest.fn().mockResolvedValue({ - components: [referencedComponent], - effortTotal: 1, - facets: FACETS, - issues: ISSUES, - languages: [], - paging: PAGING, - rules: [], - users: [] - })} location={mockLocation({ pathname: '/issues', query: {} })} onBranchesChange={() => {}} router={mockRouter()} diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesApp-test.tsx.snap index 5af18892050..13685dbe0a5 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesApp-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesApp-test.tsx.snap @@ -149,674 +149,3 @@ exports[`should show warnning when not all projects are accessible 1`] = ` `; - -exports[`should switch to source view if an issue is selected 1`] = ` -
- - -

- issues.page -

- - - -
-
-
-
- -
- - -
- -
-
-
-
- -
-

- list_of_issues -

- - -
-
-
-
-
-`; - -exports[`should switch to source view if an issue is selected 2`] = ` -
- - -

- issues.page -

- - - -
- -
- -
-
-
-`; 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 bfb4c232ec3..50b98a20b64 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 @@ -20,20 +20,19 @@ import classNames from 'classnames'; import { differenceBy, uniq } from 'lodash'; import * as React from 'react'; -import { connect } from 'react-redux'; 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 { Alert } from '../../../components/ui/Alert'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; import { enhanceConditionWithMeasure, enhanceMeasuresWithMetrics } from '../../../helpers/measures'; import { isDefined } from '../../../helpers/types'; -import { fetchBranchStatus } from '../../../store/branches'; -import { getBranchStatusByBranchLike, Store } from '../../../store/rootReducer'; -import { BranchLike, PullRequest } from '../../../types/branch-like'; +import { BranchStatusData, PullRequest } from '../../../types/branch-like'; import { IssueType } from '../../../types/issues'; -import { QualityGateStatusCondition } from '../../../types/quality-gates'; -import { Component, MeasureEnhanced, Status } from '../../../types/types'; +import { Component, MeasureEnhanced } from '../../../types/types'; import IssueLabel from '../components/IssueLabel'; import IssueRating from '../components/IssueRating'; import MeasurementLabel from '../components/MeasurementLabel'; @@ -44,23 +43,11 @@ import { MeasurementType, PR_METRICS } from '../utils'; import AfterMergeEstimate from './AfterMergeEstimate'; import LargeQualityGateBadge from './LargeQualityGateBadge'; -interface StateProps { - conditions?: QualityGateStatusCondition[]; - ignoredConditions?: boolean; - status?: Status; -} - -interface DispatchProps { - fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => void; -} - -interface OwnProps { +interface Props extends BranchStatusData, Pick { branchLike: PullRequest; component: Component; } -type Props = StateProps & DispatchProps & OwnProps; - interface State { loading: boolean; measures: MeasureEnhanced[]; @@ -281,18 +268,4 @@ export class PullRequestOverview extends React.PureComponent { } } -const mapStateToProps = (state: Store, { branchLike, component }: Props) => { - const { conditions, ignoredConditions, status } = getBranchStatusByBranchLike( - state, - component.key, - branchLike - ); - return { conditions, ignoredConditions, status }; -}; - -const mapDispatchToProps = { fetchBranchStatus: fetchBranchStatus as any }; - -export default connect( - mapStateToProps, - mapDispatchToProps -)(PullRequestOverview); +export default withBranchStatus(withBranchStatusActions(PullRequestOverview)); 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 5a37613cc24..50aa2dc36d9 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 @@ -58,7 +58,7 @@ export function BranchLikeRow(props: BranchLikeRowProps) { - + {} {displayPurgeSetting && isBranch(branchLike) && ( diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchLikeRow-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchLikeRow-test.tsx.snap index a39fb5dd0ec..ba11b5fc0c2 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchLikeRow-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchLikeRow-test.tsx.snap @@ -26,7 +26,7 @@ exports[`should render correctly for branch 1`] = ` - - - { } } -const mapDispatchToProps = { fetchBranchStatus }; - -export default withCurrentUserContext(connect(null, mapDispatchToProps)(SecurityHotspotsApp)); +export default withCurrentUserContext(withBranchStatusActions(SecurityHotspotsApp)); 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 570c60afcca..04f153888bc 100644 --- a/server/sonar-web/src/main/js/components/common/BranchStatus.tsx +++ b/server/sonar-web/src/main/js/components/common/BranchStatus.tsx @@ -18,21 +18,15 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { connect } from 'react-redux'; +import withBranchStatus from '../../app/components/branch-status/withBranchStatus'; import Level from '../../components/ui/Level'; -import { getBranchStatusByBranchLike, Store } from '../../store/rootReducer'; -import { BranchLike } from '../../types/branch-like'; +import { BranchStatusData } from '../../types/branch-like'; -interface ExposedProps { - branchLike: BranchLike; - component: string; -} +export type BranchStatusProps = Pick; -interface BranchStatusProps { - status?: string; -} +export function BranchStatus(props: BranchStatusProps) { + const { status } = props; -export function BranchStatus({ status }: BranchStatusProps) { if (!status) { return null; } @@ -40,10 +34,4 @@ export function BranchStatus({ status }: BranchStatusProps) { return ; } -const mapStateToProps = (state: Store, props: ExposedProps) => { - const { branchLike, component } = props; - const { status } = getBranchStatusByBranchLike(state, component, branchLike); - return { status }; -}; - -export default connect(mapStateToProps)(BranchStatus); +export default withBranchStatus(BranchStatus); 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 3c725df069e..551674c475b 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 @@ -19,14 +19,22 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { BranchStatus } from '../BranchStatus'; +import { BranchStatus, BranchStatusProps } from '../BranchStatus'; it('should render correctly', () => { expect(shallowRender().type()).toBeNull(); - expect(shallowRender('OK')).toMatchSnapshot(); - expect(shallowRender('ERROR')).toMatchSnapshot(); + expect( + shallowRender({ + status: 'OK' + }) + ).toMatchSnapshot('Successful'); + expect( + shallowRender({ + status: 'ERROR' + }) + ).toMatchSnapshot('Error'); }); -function shallowRender(status?: string) { - return shallow(); +function shallowRender(overrides: Partial = {}) { + return shallow(); } 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 index df907fd0bfb..aeb2bda6418 100644 --- 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 @@ -1,15 +1,15 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should render correctly 1`] = ` +exports[`should render correctly: Error 1`] = ` `; -exports[`should render correctly 2`] = ` +exports[`should render correctly: Successful 1`] = ` `; 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 23cf4dfcd3c..d7561d6e604 100644 --- a/server/sonar-web/src/main/js/components/workspace/WorkspaceComponentViewer.tsx +++ b/server/sonar-web/src/main/js/components/workspace/WorkspaceComponentViewer.tsx @@ -19,11 +19,10 @@ */ import { debounce } from 'lodash'; import * as React from 'react'; -import { connect } from 'react-redux'; import { getParents } from '../../api/components'; +import withBranchStatusActions from '../../app/components/branch-status/withBranchStatusActions'; import { isPullRequest } from '../../helpers/branch-like'; import { scrollToElement } from '../../helpers/scrolling'; -import { fetchBranchStatus } from '../../store/branches'; import { BranchLike } from '../../types/branch-like'; import { Issue, SourceViewerFile } from '../../types/types'; import SourceViewer from '../SourceViewer/SourceViewer'; @@ -137,6 +136,4 @@ export class WorkspaceComponentViewer extends React.PureComponent { } } -const mapDispatchToProps = { fetchBranchStatus: fetchBranchStatus as any }; - -export default connect(null, mapDispatchToProps)(WorkspaceComponentViewer); +export default withBranchStatusActions(WorkspaceComponentViewer); 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 b332e9acd2e..ce6cc4ec13f 100644 --- a/server/sonar-web/src/main/js/helpers/branch-like.ts +++ b/server/sonar-web/src/main/js/helpers/branch-like.ts @@ -23,9 +23,11 @@ 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; @@ -136,3 +138,12 @@ export function fillBranchLike( } return undefined; } + +export function getBranchStatusByBranchLike( + branchStatusByComponent: Dict>, + 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/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts index 3c4c452ddd7..7269073b760 100644 --- a/server/sonar-web/src/main/js/helpers/testMocks.ts +++ b/server/sonar-web/src/main/js/helpers/testMocks.ts @@ -24,6 +24,7 @@ import { DocumentationEntry } from '../apps/documentation/utils'; import { Exporter, Profile } from '../apps/quality-profiles/types'; import { AppState } from '../types/appstate'; import { EditionKey } from '../types/editions'; +import { RawIssue } from '../types/issues'; import { Language } from '../types/languages'; import { DumpStatus, DumpTask } from '../types/project-dump'; import { TaskStatuses } from '../types/tasks'; @@ -367,6 +368,31 @@ export function mockEvent(overrides = {}) { } as any; } +export function mockRawIssue(withLocations = false, overrides: Partial = {}): RawIssue { + const rawIssue: RawIssue = { + component: 'main.js', + key: 'AVsae-CQS-9G3txfbFN2', + line: 25, + project: 'myproject', + rule: 'javascript:S1067', + severity: 'MAJOR', + status: 'OPEN', + textRange: { startLine: 25, endLine: 26, startOffset: 0, endOffset: 15 }, + ...overrides + }; + + if (withLocations) { + const loc = mockFlowLocation; + + rawIssue.flows = [{ locations: [loc(), loc()] }]; + } + + return { + ...rawIssue, + ...overrides + }; +} + export function mockIssue(withLocations = false, overrides: Partial = {}) { const issue: Issue = { actions: [], diff --git a/server/sonar-web/src/main/js/store/__tests__/branches-test.ts b/server/sonar-web/src/main/js/store/__tests__/branches-test.ts deleted file mode 100644 index eb52257e42a..00000000000 --- a/server/sonar-web/src/main/js/store/__tests__/branches-test.ts +++ /dev/null @@ -1,149 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { getBranchLikeKey } from '../../helpers/branch-like'; -import { mockBranch, mockPullRequest } from '../../helpers/mocks/branch-like'; -import { mockQualityGateStatusCondition } from '../../helpers/mocks/quality-gates'; -import { BranchLike } from '../../types/branch-like'; -import { QualityGateStatusCondition } from '../../types/quality-gates'; -import { Status } from '../../types/types'; -import reducer, { - fetchBranchStatus, - getBranchStatusByBranchLike, - registerBranchStatus, - registerBranchStatusAction, - State -} from '../branches'; - -type TestArgs = [BranchLike, string, Status, QualityGateStatusCondition[], boolean?]; - -const FAILING_CONDITION = mockQualityGateStatusCondition(); -const COMPONENT = 'foo'; -const BRANCH_STATUS_1: TestArgs = [mockPullRequest(), COMPONENT, 'ERROR', [FAILING_CONDITION]]; -const BRANCH_STATUS_2: TestArgs = [mockBranch(), 'bar', 'OK', [], true]; -const BRANCH_STATUS_3: TestArgs = [mockBranch(), COMPONENT, 'OK', []]; - -it('should allow to register new branche statuses', () => { - const initialState: State = convertToState(); - - const newState = reducer(initialState, registerBranchStatusAction(...BRANCH_STATUS_1)); - expect(newState).toEqual(convertToState([BRANCH_STATUS_1])); - - const newerState = reducer(newState, registerBranchStatusAction(...BRANCH_STATUS_2)); - expect(newerState).toEqual(convertToState([BRANCH_STATUS_1, BRANCH_STATUS_2])); - expect(newState).toEqual(convertToState([BRANCH_STATUS_1])); -}); - -it('should allow to update branche statuses', () => { - const initialState: State = convertToState([BRANCH_STATUS_1, BRANCH_STATUS_2, BRANCH_STATUS_3]); - const branchLike: BranchLike = { ...BRANCH_STATUS_1[0], status: { qualityGateStatus: 'OK' } }; - const branchStatus: TestArgs = [branchLike, COMPONENT, 'OK', []]; - - const newState = reducer(initialState, registerBranchStatusAction(...branchStatus)); - expect(newState).toEqual(convertToState([branchStatus, BRANCH_STATUS_2, BRANCH_STATUS_3])); - expect(initialState).toEqual(convertToState([BRANCH_STATUS_1, BRANCH_STATUS_2, BRANCH_STATUS_3])); -}); - -it('should get the branche statuses from state', () => { - const initialState: State = convertToState([BRANCH_STATUS_1, BRANCH_STATUS_2]); - - const [branchLike, component] = BRANCH_STATUS_1; - expect(getBranchStatusByBranchLike(initialState, component, branchLike)).toEqual({ - conditions: [FAILING_CONDITION], - status: 'ERROR' - }); - expect(getBranchStatusByBranchLike(initialState, component, BRANCH_STATUS_2[0])).toBeUndefined(); -}); - -function convertToState(items: TestArgs[] = []) { - const state: State = { byComponent: {} }; - - items.forEach(item => { - const [branchLike, component, status, conditions, ignoredConditions] = item; - state.byComponent[component] = { - ...(state.byComponent[component] || {}), - [getBranchLikeKey(branchLike)]: { conditions, ignoredConditions, status } - }; - }); - - return state; -} - -jest.mock('../../app/utils/addGlobalErrorMessage', () => ({ - __esModule: true, - default: jest.fn() -})); - -jest.mock('../../api/quality-gates', () => { - const { mockQualityGateProjectStatus } = jest.requireActual('../../helpers/mocks/quality-gates'); - return { - getQualityGateProjectStatus: jest.fn().mockResolvedValue( - mockQualityGateProjectStatus({ - conditions: [ - { - actualValue: '10', - comparator: 'GT', - errorThreshold: '0', - metricKey: 'foo', - periodIndex: 1, - status: 'ERROR' - } - ] - }) - ) - }; -}); - -describe('branch store actions', () => { - const branchLike = mockBranch(); - const component = 'foo'; - const status = 'OK'; - - it('correctly registers a new branch status', () => { - const dispatch = jest.fn(); - - registerBranchStatus(branchLike, component, status)(dispatch); - expect(dispatch).toBeCalledWith({ - branchLike, - component, - status, - type: 'REGISTER_BRANCH_STATUS' - }); - }); - - it('correctly fetches a branch status', async () => { - const dispatch = jest.fn(); - - fetchBranchStatus(branchLike, component)(dispatch); - await new Promise(setImmediate); - - expect(dispatch).toBeCalledWith({ - branchLike, - component, - status, - conditions: [ - mockQualityGateStatusCondition({ - period: 1 - }) - ], - ignoredConditions: false, - type: 'REGISTER_BRANCH_STATUS' - }); - }); -}); diff --git a/server/sonar-web/src/main/js/store/__tests__/rootReducers-test.tsx b/server/sonar-web/src/main/js/store/__tests__/globalMessages-test.ts similarity index 52% rename from server/sonar-web/src/main/js/store/__tests__/rootReducers-test.tsx rename to server/sonar-web/src/main/js/store/__tests__/globalMessages-test.ts index c6cffde06bd..a5ceed74a08 100644 --- a/server/sonar-web/src/main/js/store/__tests__/rootReducers-test.tsx +++ b/server/sonar-web/src/main/js/store/__tests__/globalMessages-test.ts @@ -17,16 +17,28 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { mockPullRequest } from '../../helpers/mocks/branch-like'; -import * as fromBranches from '../branches'; -import { getBranchStatusByBranchLike, Store } from '../rootReducer'; +import globalMessagesReducer, { MessageLevel } from '../globalMessages'; -it('correctly reduce state for branches', () => { - const spiedOn = jest.spyOn(fromBranches, 'getBranchStatusByBranchLike').mockReturnValueOnce({}); +describe('globalMessagesReducer', () => { + it('should handle ADD_GLOBAL_MESSAGE', () => { + const actionAttributes = { id: 'id', message: 'There was an error', level: MessageLevel.Error }; - const branches = { byComponent: {} }; - const component = 'foo'; - const branchLike = mockPullRequest(); - getBranchStatusByBranchLike({ branches } as Store, component, branchLike); - expect(spiedOn).toBeCalledWith(branches, component, branchLike); + expect( + globalMessagesReducer([], { + type: 'ADD_GLOBAL_MESSAGE', + ...actionAttributes + }) + ).toEqual([actionAttributes]); + }); + + it('should handle CLOSE_GLOBAL_MESSAGE', () => { + const state = [ + { id: 'm1', message: 'message 1', level: MessageLevel.Success }, + { id: 'm2', message: 'message 2', level: MessageLevel.Success } + ]; + + expect(globalMessagesReducer(state, { type: 'CLOSE_GLOBAL_MESSAGE', id: 'm2' })).toEqual([ + state[0] + ]); + }); }); diff --git a/server/sonar-web/src/main/js/store/branches.ts b/server/sonar-web/src/main/js/store/branches.ts deleted file mode 100644 index e6e4dada333..00000000000 --- a/server/sonar-web/src/main/js/store/branches.ts +++ /dev/null @@ -1,115 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 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 { Dispatch } from 'redux'; -import { getQualityGateProjectStatus } from '../api/quality-gates'; -import addGlobalErrorMessage from '../app/utils/addGlobalErrorMessage'; -import { getBranchLikeKey, getBranchLikeQuery } from '../helpers/branch-like'; -import { extractStatusConditionsFromProjectStatus } from '../helpers/qualityGates'; -import { ActionType } from '../types/actions'; -import { BranchLike } from '../types/branch-like'; -import { QualityGateStatusCondition } from '../types/quality-gates'; -import { Dict, Status } from '../types/types'; - -export interface BranchStatusData { - conditions?: QualityGateStatusCondition[]; - ignoredConditions?: boolean; - status?: Status; -} - -export interface State { - byComponent: Dict>; -} - -const enum Actions { - RegisterBranchStatus = 'REGISTER_BRANCH_STATUS' -} - -type Action = ActionType; - -export function registerBranchStatusAction( - branchLike: BranchLike, - component: string, - status: Status, - conditions?: QualityGateStatusCondition[], - ignoredConditions?: boolean -) { - return { - type: Actions.RegisterBranchStatus, - branchLike, - component, - conditions, - ignoredConditions, - status - }; -} - -export default function branchesReducer(state: State = { byComponent: {} }, action: Action): State { - if (action.type === Actions.RegisterBranchStatus) { - const { component, conditions, branchLike, ignoredConditions, status } = action; - const branchLikeKey = getBranchLikeKey(branchLike); - return { - byComponent: { - ...state.byComponent, - [component]: { - ...(state.byComponent[component] || {}), - [branchLikeKey]: { - conditions, - ignoredConditions, - status - } - } - } - }; - } - - return state; -} - -export function getBranchStatusByBranchLike( - state: State, - component: string, - branchLike: BranchLike -): BranchStatusData { - const branchLikeKey = getBranchLikeKey(branchLike); - return state.byComponent[component] && state.byComponent[component][branchLikeKey]; -} - -export function fetchBranchStatus(branchLike: BranchLike, projectKey: string) { - return (dispatch: Dispatch) => { - getQualityGateProjectStatus({ projectKey, ...getBranchLikeQuery(branchLike) }).then( - projectStatus => { - const { ignoredConditions, status } = projectStatus; - const conditions = extractStatusConditionsFromProjectStatus(projectStatus); - dispatch( - registerBranchStatusAction(branchLike, projectKey, status, conditions, ignoredConditions) - ); - }, - () => { - addGlobalErrorMessage('Fetching Quality Gate status failed'); - } - ); - }; -} - -export function registerBranchStatus(branchLike: BranchLike, component: string, status: Status) { - return (dispatch: Dispatch) => { - dispatch(registerBranchStatusAction(branchLike, component, status)); - }; -} diff --git a/server/sonar-web/src/main/js/store/globalMessages.ts b/server/sonar-web/src/main/js/store/globalMessages.ts index c64ac9dafdb..58776e7d253 100644 --- a/server/sonar-web/src/main/js/store/globalMessages.ts +++ b/server/sonar-web/src/main/js/store/globalMessages.ts @@ -21,7 +21,7 @@ import { uniqueId } from 'lodash'; import { Dispatch } from 'redux'; import { ActionType } from '../types/actions'; -enum MessageLevel { +export enum MessageLevel { Error = 'ERROR', Success = 'SUCCESS' } diff --git a/server/sonar-web/src/main/js/store/rootReducer.ts b/server/sonar-web/src/main/js/store/rootReducer.ts index 885d7b25cdc..2a68fa75667 100644 --- a/server/sonar-web/src/main/js/store/rootReducer.ts +++ b/server/sonar-web/src/main/js/store/rootReducer.ts @@ -18,28 +18,16 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { combineReducers } from 'redux'; -import { BranchLike } from '../types/branch-like'; -import branches, * as fromBranches from './branches'; import globalMessages, * as fromGlobalMessages from './globalMessages'; export type Store = { - branches: fromBranches.State; globalMessages: fromGlobalMessages.State; }; export default combineReducers({ - branches, globalMessages }); export function getGlobalMessages(state: Store) { return fromGlobalMessages.getGlobalMessages(state.globalMessages); } - -export function getBranchStatusByBranchLike( - state: Store, - component: string, - branchLike: BranchLike -) { - return fromBranches.getBranchStatusByBranchLike(state.branches, component, branchLike); -} diff --git a/server/sonar-web/src/main/js/types/branch-like.ts b/server/sonar-web/src/main/js/types/branch-like.ts index 7a479af7ae2..1495e07d4cd 100644 --- a/server/sonar-web/src/main/js/types/branch-like.ts +++ b/server/sonar-web/src/main/js/types/branch-like.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 { QualityGateStatusCondition } from './quality-gates'; import { NewCodePeriod, Status } from './types'; export interface Branch { @@ -62,3 +63,9 @@ export type BranchParameters = { branch?: string } | { pullRequest?: string }; export interface BranchWithNewCodePeriod extends Branch { newCodePeriod?: NewCodePeriod; } + +export interface BranchStatusData { + conditions?: QualityGateStatusCondition[]; + ignoredConditions?: boolean; + status?: Status; +} -- 2.39.5