diff options
author | Jeremy Davis <jeremy.davis@sonarsource.com> | 2022-03-22 16:24:08 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-03-23 20:02:45 +0000 |
commit | a38eda46c2f174efe54e6a584f12895a3d3c7f3c (patch) | |
tree | ef05b7691529a4cd77dba965e523469e7e5b7d3b | |
parent | 9a35b0e3e65302ee4f6c1b4cc2c5db71df5b0a5d (diff) | |
download | sonarqube-a38eda46c2f174efe54e6a584f12895a3d3c7f3c.tar.gz sonarqube-a38eda46c2f174efe54e6a584f12895a3d3c7f3c.zip |
SONAR-15914 Extract branchstatus from redux
36 files changed, 721 insertions, 1251 deletions
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<Location, 'query' | 'pathname'>; - registerBranchStatus: (branchLike: BranchLike, component: string, status: Status) => void; + updateBranchStatus: (branchLike: BranchLike, component: string, status: Status) => void; router: Pick<Router, 'replace'>; } @@ -359,7 +358,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> { 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<Props, State> { } } -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) { <div className="global-container"> <div className="page-wrapper" id="container"> <div className="page-container"> - <Workspace> - <IndexationContextProvider> - <LanguagesContextProvider> - <MetricsContextProvider> - <GlobalNav location={props.location} /> - <GlobalMessagesContainer /> - <IndexationNotification /> - <UpdateNotification dismissable={true} /> - {props.children} - </MetricsContextProvider> - </LanguagesContextProvider> - </IndexationContextProvider> - </Workspace> + <BranchStatusContextProvider> + <Workspace> + <IndexationContextProvider> + <LanguagesContextProvider> + <MetricsContextProvider> + <GlobalNav location={props.location} /> + <GlobalMessagesContainer /> + <IndexationNotification /> + <UpdateNotification dismissable={true} /> + {props.children} + </MetricsContextProvider> + </LanguagesContextProvider> + </IndexationContextProvider> + </Workspace> + </BranchStatusContextProvider> </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 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<ComponentContainer['props']> = {}) { <ComponentContainer appState={mockAppState()} location={mockLocation({ query: { id: 'foo' } })} - registerBranchStatus={jest.fn()} + updateBranchStatus={jest.fn()} router={mockRouter()} {...props}> <Inner /> 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`] = ` <div className="page-container" > - <Workspace> - <withAppStateContext(IndexationContextProvider)> - <LanguagesContextProvider> - <MetricsContextProvider> - <withCurrentUserContext(GlobalNav) - location={ - Object { - "action": "PUSH", - "hash": "", - "key": "key", - "pathname": "/path", - "query": Object {}, - "search": "", - "state": Object {}, + <BranchStatusContextProvider> + <Workspace> + <withAppStateContext(IndexationContextProvider)> + <LanguagesContextProvider> + <MetricsContextProvider> + <withCurrentUserContext(GlobalNav) + location={ + Object { + "action": "PUSH", + "hash": "", + "key": "key", + "pathname": "/path", + "query": Object {}, + "search": "", + "state": Object {}, + } } - } - /> - <Connect(GlobalMessages) /> - <withCurrentUserContext(withIndexationContext(IndexationNotification)) /> - <withCurrentUserContext(withAppStateContext(UpdateNotification)) - dismissable={true} - /> - <ChildComponent /> - </MetricsContextProvider> - </LanguagesContextProvider> - </withAppStateContext(IndexationContextProvider)> - </Workspace> + /> + <Connect(GlobalMessages) /> + <withCurrentUserContext(withIndexationContext(IndexationNotification)) /> + <withCurrentUserContext(withAppStateContext(UpdateNotification)) + dismissable={true} + /> + <ChildComponent /> + </MetricsContextProvider> + </LanguagesContextProvider> + </withAppStateContext(IndexationContextProvider)> + </Workspace> + </BranchStatusContextProvider> </div> <withCurrentUserContext(PromotionNotification) /> </div> 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<Dict<BranchStatusData>>; + fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => void; + updateBranchStatus: ( + branchLike: BranchLike, + projectKey: string, + status: Status, + conditions?: QualityGateStatusCondition[], + ignoredConditions?: boolean + ) => void; +} + +export const BranchStatusContext = React.createContext<BranchStatusContextInterface>({ + branchStatusByComponent: {}, + fetchBranchStatus: () => { + throw Error('BranchStatusContext is not provided'); + }, + updateBranchStatus: () => { + throw Error('BranchStatusContext is not provided'); + } +}); diff --git a/server/sonar-web/src/main/js/app/components/branch-status/BranchStatusContextProvider.tsx b/server/sonar-web/src/main/js/app/components/branch-status/BranchStatusContextProvider.tsx 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<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: this.fetchBranchStatus, + 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 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<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 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<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 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<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/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) { <DetachIcon className="little-spacer-left" size={12} /> </a> )} - <BranchStatus branchLike={branchLike} component={component.key} /> + <BranchStatus branchLike={branchLike} component={component} /> </div> )} </> 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} /> </a> - <Connect(BranchStatus) + <withBranchStatus(BranchStatus) branchLike={ Object { "analysisDate": "2018-01-01", @@ -208,7 +208,30 @@ exports[`should render correctly for a pull request 1`] = ` "url": "https://example.com/pull/1234", } } - component="my-project" + component={ + Object { + "analysisDate": "2017-01-02T00:00:00.000Z", + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + "version": "0.0.1", + } + } /> </div> </Fragment> 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) { )} </div> <div className="spacer-left"> - <BranchStatus branchLike={branchLike} component={component.key} /> + <BranchStatus branchLike={branchLike} component={component} /> </div> </div> </li> 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`] = ` <div className="spacer-left" > - <Connect(BranchStatus) + <withBranchStatus(BranchStatus) branchLike={ Object { "analysisDate": "2018-01-01", @@ -45,7 +45,28 @@ exports[`should render a main branch correctly 1`] = ` "name": "master", } } - component="my-project" + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } /> </div> </div> @@ -85,7 +106,7 @@ exports[`should render a non-main branch, indented and selected item correctly 1 <div className="spacer-left" > - <Connect(BranchStatus) + <withBranchStatus(BranchStatus) branchLike={ Object { "analysisDate": "2018-01-01", @@ -96,7 +117,28 @@ exports[`should render a non-main branch, indented and selected item correctly 1 "title": "Foo Bar feature", } } - component="my-project" + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } /> </div> </div> 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<void>; -} - -interface OwnProps { +interface Props { branchLike?: BranchLike; component: Component; + fetchBranchStatus: (branchLike: BranchLike, projectKey: string) => Promise<void>; location: Pick<Location, 'query'>; router: Pick<InjectedRouter, 'push'>; metrics: Dict<Metric>; } -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<Props, State> { } } -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<FetchIssuesPromise>; location: Location; onBranchesChange?: () => void; router: Pick<Router, 'push' | 'replace'>; @@ -405,6 +406,19 @@ export default class App extends React.PureComponent<Props, State> { 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<FetchIssuesPromise> => { const { component } = this.props; const { myIssues, openFacets, query } = this.state; @@ -437,7 +451,8 @@ export default class App extends React.PureComponent<Props, State> { 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<Props, State> { 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<App['props']> = {}) { @@ -594,16 +620,6 @@ function shallowRender(props: Partial<App['props']> = {}) { }} 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`] = ` </section> </div> `; - -exports[`should switch to source view if an issue is selected 1`] = ` -<div - className="layout-page issues" - id="issues-page" -> - <Suggestions - suggestions="issues" - /> - <Helmet - defer={false} - encodeSpecialCharacters={true} - prioritizeSeoTags={false} - title="issues.page" - /> - <h1 - className="a11y-hidden" - > - issues.page - </h1> - <ScreenPositionHelper - className="layout-page-side-outer" - > - <Component /> - </ScreenPositionHelper> - <div - className="layout-page-main" - role="main" - > - <div - className="layout-page-header-panel layout-page-main-header issues-main-header" - > - <div - className="layout-page-header-panel-inner layout-page-main-header-inner" - > - <div - className="layout-page-main-inner" - > - <A11ySkipTarget - anchor="issues_main" - /> - <div - className="pull-left" - > - <Checkbox - checked={false} - className="spacer-right text-middle" - disabled={false} - id="issues-selection" - onCheck={[Function]} - thirdState={false} - title="issues.select_all_issues" - /> - <Button - disabled={true} - id="issues-bulk-change" - onClick={[Function]} - > - bulk_change - </Button> - </div> - <PageActions - canSetHome={false} - effortTotal={1} - paging={ - Object { - "pageIndex": 1, - "pageSize": 100, - "total": 4, - } - } - selectedIndex={0} - /> - </div> - </div> - </div> - <div - className="layout-page-main-inner" - > - <DeferredSpinner - loading={false} - > - <div> - <h2 - className="a11y-hidden" - > - list_of_issues - </h2> - <IssuesList - checked={Array []} - component={ - Object { - "breadcrumbs": Array [], - "key": "foo", - "name": "bar", - "qualifier": "Doe", - } - } - issues={ - Array [ - Object { - "actions": Array [], - "component": "main.js", - "componentLongName": "main.js", - "componentQualifier": "FIL", - "componentUuid": "foo1234", - "creationDate": "2017-03-01T09:36:01+0100", - "flows": Array [], - "fromHotspot": false, - "key": "foo", - "line": 25, - "message": "Reduce the number of conditional operators (4) used in the expression", - "project": "myproject", - "projectKey": "foo", - "projectName": "Foo", - "rule": "javascript:S1067", - "ruleName": "foo", - "secondaryLocations": Array [], - "severity": "MAJOR", - "status": "OPEN", - "textRange": Object { - "endLine": 26, - "endOffset": 15, - "startLine": 25, - "startOffset": 0, - }, - "transitions": Array [], - "type": "BUG", - }, - Object { - "actions": Array [], - "component": "main.js", - "componentLongName": "main.js", - "componentQualifier": "FIL", - "componentUuid": "foo1234", - "creationDate": "2017-03-01T09:36:01+0100", - "flows": Array [], - "fromHotspot": false, - "key": "bar", - "line": 25, - "message": "Reduce the number of conditional operators (4) used in the expression", - "project": "myproject", - "projectKey": "foo", - "projectName": "Foo", - "rule": "javascript:S1067", - "ruleName": "foo", - "secondaryLocations": Array [], - "severity": "MAJOR", - "status": "OPEN", - "textRange": Object { - "endLine": 26, - "endOffset": 15, - "startLine": 25, - "startOffset": 0, - }, - "transitions": Array [], - "type": "BUG", - }, - Object { - "actions": Array [], - "component": "main.js", - "componentLongName": "main.js", - "componentQualifier": "FIL", - "componentUuid": "foo1234", - "creationDate": "2017-03-01T09:36:01+0100", - "flows": Array [ - Array [ - Object { - "component": "main.js", - "textRange": Object { - "endLine": 2, - "endOffset": 2, - "startLine": 1, - "startOffset": 1, - }, - }, - Object { - "component": "main.js", - "textRange": Object { - "endLine": 2, - "endOffset": 2, - "startLine": 1, - "startOffset": 1, - }, - }, - Object { - "component": "main.js", - "textRange": Object { - "endLine": 2, - "endOffset": 2, - "startLine": 1, - "startOffset": 1, - }, - }, - ], - Array [ - Object { - "component": "main.js", - "textRange": Object { - "endLine": 2, - "endOffset": 2, - "startLine": 1, - "startOffset": 1, - }, - }, - Object { - "component": "main.js", - "textRange": Object { - "endLine": 2, - "endOffset": 2, - "startLine": 1, - "startOffset": 1, - }, - }, - ], - ], - "fromHotspot": false, - "key": "third", - "line": 25, - "message": "Reduce the number of conditional operators (4) used in the expression", - "project": "myproject", - "projectKey": "foo", - "projectName": "Foo", - "rule": "javascript:S1067", - "ruleName": "foo", - "secondaryLocations": Array [ - Object { - "component": "main.js", - "textRange": Object { - "endLine": 2, - "endOffset": 2, - "startLine": 1, - "startOffset": 1, - }, - }, - Object { - "component": "main.js", - "textRange": Object { - "endLine": 2, - "endOffset": 2, - "startLine": 1, - "startOffset": 1, - }, - }, - ], - "severity": "MAJOR", - "status": "OPEN", - "textRange": Object { - "endLine": 26, - "endOffset": 15, - "startLine": 25, - "startOffset": 0, - }, - "transitions": Array [], - "type": "BUG", - }, - Object { - "actions": Array [], - "component": "main.js", - "componentLongName": "main.js", - "componentQualifier": "FIL", - "componentUuid": "foo1234", - "creationDate": "2017-03-01T09:36:01+0100", - "flows": Array [], - "fromHotspot": false, - "key": "fourth", - "line": 25, - "message": "Reduce the number of conditional operators (4) used in the expression", - "project": "myproject", - "projectKey": "foo", - "projectName": "Foo", - "rule": "javascript:S1067", - "ruleName": "foo", - "secondaryLocations": Array [], - "severity": "MAJOR", - "status": "OPEN", - "textRange": Object { - "endLine": 26, - "endOffset": 15, - "startLine": 25, - "startOffset": 0, - }, - "transitions": Array [], - "type": "BUG", - }, - ] - } - onFilterChange={[Function]} - onIssueChange={[Function]} - onIssueCheck={[Function]} - onIssueClick={[Function]} - onPopupToggle={[Function]} - selectedIssue={ - Object { - "actions": Array [], - "component": "main.js", - "componentLongName": "main.js", - "componentQualifier": "FIL", - "componentUuid": "foo1234", - "creationDate": "2017-03-01T09:36:01+0100", - "flows": Array [], - "fromHotspot": false, - "key": "foo", - "line": 25, - "message": "Reduce the number of conditional operators (4) used in the expression", - "project": "myproject", - "projectKey": "foo", - "projectName": "Foo", - "rule": "javascript:S1067", - "ruleName": "foo", - "secondaryLocations": Array [], - "severity": "MAJOR", - "status": "OPEN", - "textRange": Object { - "endLine": 26, - "endOffset": 15, - "startLine": 25, - "startOffset": 0, - }, - "transitions": Array [], - "type": "BUG", - } - } - /> - <ListFooter - count={4} - loadMore={[Function]} - loading={false} - total={4} - /> - </div> - </DeferredSpinner> - </div> - </div> -</div> -`; - -exports[`should switch to source view if an issue is selected 2`] = ` -<div - className="layout-page issues" - id="issues-page" -> - <Suggestions - suggestions="issues" - /> - <Helmet - defer={false} - encodeSpecialCharacters={true} - prioritizeSeoTags={false} - title="Reduce the number of conditional operators (4) used in the expression" - /> - <h1 - className="a11y-hidden" - > - issues.page - </h1> - <ScreenPositionHelper - className="layout-page-side-outer" - > - <Component /> - </ScreenPositionHelper> - <div - className="layout-page-main" - role="main" - > - <A11ySkipTarget - anchor="issues_main" - /> - <div - className="layout-page-main-inner" - > - <IssuesSourceViewer - issues={ - Array [ - Object { - "actions": Array [], - "component": "main.js", - "componentLongName": "main.js", - "componentQualifier": "FIL", - "componentUuid": "foo1234", - "creationDate": "2017-03-01T09:36:01+0100", - "flows": Array [], - "fromHotspot": false, - "key": "foo", - "line": 25, - "message": "Reduce the number of conditional operators (4) used in the expression", - "project": "myproject", - "projectKey": "foo", - "projectName": "Foo", - "rule": "javascript:S1067", - "ruleName": "foo", - "secondaryLocations": Array [], - "severity": "MAJOR", - "status": "OPEN", - "textRange": Object { - "endLine": 26, - "endOffset": 15, - "startLine": 25, - "startOffset": 0, - }, - "transitions": Array [], - "type": "BUG", - }, - Object { - "actions": Array [], - "component": "main.js", - "componentLongName": "main.js", - "componentQualifier": "FIL", - "componentUuid": "foo1234", - "creationDate": "2017-03-01T09:36:01+0100", - "flows": Array [], - "fromHotspot": false, - "key": "bar", - "line": 25, - "message": "Reduce the number of conditional operators (4) used in the expression", - "project": "myproject", - "projectKey": "foo", - "projectName": "Foo", - "rule": "javascript:S1067", - "ruleName": "foo", - "secondaryLocations": Array [], - "severity": "MAJOR", - "status": "OPEN", - "textRange": Object { - "endLine": 26, - "endOffset": 15, - "startLine": 25, - "startOffset": 0, - }, - "transitions": Array [], - "type": "BUG", - }, - Object { - "actions": Array [], - "component": "main.js", - "componentLongName": "main.js", - "componentQualifier": "FIL", - "componentUuid": "foo1234", - "creationDate": "2017-03-01T09:36:01+0100", - "flows": Array [ - Array [ - Object { - "component": "main.js", - "textRange": Object { - "endLine": 2, - "endOffset": 2, - "startLine": 1, - "startOffset": 1, - }, - }, - Object { - "component": "main.js", - "textRange": Object { - "endLine": 2, - "endOffset": 2, - "startLine": 1, - "startOffset": 1, - }, - }, - Object { - "component": "main.js", - "textRange": Object { - "endLine": 2, - "endOffset": 2, - "startLine": 1, - "startOffset": 1, - }, - }, - ], - Array [ - Object { - "component": "main.js", - "textRange": Object { - "endLine": 2, - "endOffset": 2, - "startLine": 1, - "startOffset": 1, - }, - }, - Object { - "component": "main.js", - "textRange": Object { - "endLine": 2, - "endOffset": 2, - "startLine": 1, - "startOffset": 1, - }, - }, - ], - ], - "fromHotspot": false, - "key": "third", - "line": 25, - "message": "Reduce the number of conditional operators (4) used in the expression", - "project": "myproject", - "projectKey": "foo", - "projectName": "Foo", - "rule": "javascript:S1067", - "ruleName": "foo", - "secondaryLocations": Array [ - Object { - "component": "main.js", - "textRange": Object { - "endLine": 2, - "endOffset": 2, - "startLine": 1, - "startOffset": 1, - }, - }, - Object { - "component": "main.js", - "textRange": Object { - "endLine": 2, - "endOffset": 2, - "startLine": 1, - "startOffset": 1, - }, - }, - ], - "severity": "MAJOR", - "status": "OPEN", - "textRange": Object { - "endLine": 26, - "endOffset": 15, - "startLine": 25, - "startOffset": 0, - }, - "transitions": Array [], - "type": "BUG", - }, - Object { - "actions": Array [], - "component": "main.js", - "componentLongName": "main.js", - "componentQualifier": "FIL", - "componentUuid": "foo1234", - "creationDate": "2017-03-01T09:36:01+0100", - "flows": Array [], - "fromHotspot": false, - "key": "fourth", - "line": 25, - "message": "Reduce the number of conditional operators (4) used in the expression", - "project": "myproject", - "projectKey": "foo", - "projectName": "Foo", - "rule": "javascript:S1067", - "ruleName": "foo", - "secondaryLocations": Array [], - "severity": "MAJOR", - "status": "OPEN", - "textRange": Object { - "endLine": 26, - "endOffset": 15, - "startLine": 25, - "startOffset": 0, - }, - "transitions": Array [], - "type": "BUG", - }, - ] - } - loadIssues={[Function]} - locationsNavigator={false} - onIssueChange={[Function]} - onIssueSelect={[Function]} - onLocationSelect={[Function]} - openIssue={ - Object { - "actions": Array [], - "component": "main.js", - "componentLongName": "main.js", - "componentQualifier": "FIL", - "componentUuid": "foo1234", - "creationDate": "2017-03-01T09:36:01+0100", - "flows": Array [ - Array [ - Object { - "component": "main.js", - "textRange": Object { - "endLine": 2, - "endOffset": 2, - "startLine": 1, - "startOffset": 1, - }, - }, - Object { - "component": "main.js", - "textRange": Object { - "endLine": 2, - "endOffset": 2, - "startLine": 1, - "startOffset": 1, - }, - }, - Object { - "component": "main.js", - "textRange": Object { - "endLine": 2, - "endOffset": 2, - "startLine": 1, - "startOffset": 1, - }, - }, - ], - Array [ - Object { - "component": "main.js", - "textRange": Object { - "endLine": 2, - "endOffset": 2, - "startLine": 1, - "startOffset": 1, - }, - }, - Object { - "component": "main.js", - "textRange": Object { - "endLine": 2, - "endOffset": 2, - "startLine": 1, - "startOffset": 1, - }, - }, - ], - ], - "fromHotspot": false, - "key": "third", - "line": 25, - "message": "Reduce the number of conditional operators (4) used in the expression", - "project": "myproject", - "projectKey": "foo", - "projectName": "Foo", - "rule": "javascript:S1067", - "ruleName": "foo", - "secondaryLocations": Array [ - Object { - "component": "main.js", - "textRange": Object { - "endLine": 2, - "endOffset": 2, - "startLine": 1, - "startOffset": 1, - }, - }, - Object { - "component": "main.js", - "textRange": Object { - "endLine": 2, - "endOffset": 2, - "startLine": 1, - "startOffset": 1, - }, - }, - ], - "severity": "MAJOR", - "status": "OPEN", - "textRange": Object { - "endLine": 26, - "endOffset": 15, - "startLine": 25, - "startOffset": 0, - }, - "transitions": Array [], - "type": "BUG", - } - } - /> - </div> - </div> -</div> -`; 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<BranchStatusContextInterface, 'fetchBranchStatus'> { 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<Props, State> { } } -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<StateProps, DispatchProps, OwnProps>( - 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) { </span> </td> <td className="nowrap"> - <BranchStatus branchLike={branchLike} component={component.key} /> + <BranchStatus branchLike={branchLike} component={component} /> </td> <td className="nowrap">{<DateFromNow date={branchLike.analysisDate} />}</td> {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`] = ` <td className="nowrap" > - <Connect(BranchStatus) + <withBranchStatus(BranchStatus) branchLike={ Object { "analysisDate": "2018-01-01", @@ -35,7 +35,28 @@ exports[`should render correctly for branch 1`] = ` "name": "branch-6.7", } } - component="my-project" + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } /> </td> <td @@ -130,7 +151,7 @@ exports[`should render correctly for main branch 1`] = ` <td className="nowrap" > - <Connect(BranchStatus) + <withBranchStatus(BranchStatus) branchLike={ Object { "analysisDate": "2018-01-01", @@ -139,7 +160,28 @@ exports[`should render correctly for main branch 1`] = ` "name": "master", } } - component="my-project" + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } /> </td> <td @@ -229,7 +271,7 @@ exports[`should render correctly for pull request 1`] = ` <td className="nowrap" > - <Connect(BranchStatus) + <withBranchStatus(BranchStatus) branchLike={ Object { "analysisDate": "2018-01-01", @@ -240,7 +282,28 @@ exports[`should render correctly for pull request 1`] = ` "title": "Foo Bar feature", } } - component="my-project" + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } /> </td> <td 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 2eaf52d2950..f9814f838c7 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,9 +21,9 @@ import { Location } from 'history'; import key from 'keymaster'; import { flatMap, range } from 'lodash'; import * as React from 'react'; -import { connect } from 'react-redux'; import { getMeasures } from '../../api/measures'; import { getSecurityHotspotList, getSecurityHotspots } from '../../api/security-hotspots'; +import withBranchStatusActions from '../../app/components/branch-status/withBranchStatusActions'; import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext'; import { Router } from '../../components/hoc/withRouter'; import { getLeakValue } from '../../components/measure/utils'; @@ -31,7 +31,6 @@ import { getBranchLikeQuery, isPullRequest, isSameBranchLike } from '../../helpe import { KeyboardCodes, KeyboardKeys } from '../../helpers/keycodes'; import { scrollToElement } from '../../helpers/scrolling'; import { getStandards } from '../../helpers/security-standard'; -import { fetchBranchStatus } from '../../store/branches'; import { BranchLike } from '../../types/branch-like'; import { SecurityStandard, Standards } from '../../types/security'; import { @@ -547,6 +546,4 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { } } -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<BranchStatusData, 'status'>; -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 <Level level={status} small={true} />; } -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(<BranchStatus status={status} />); +function shallowRender(overrides: Partial<BranchStatusProps> = {}) { + return shallow(<BranchStatus {...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 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`] = ` <Level - level="OK" + level="ERROR" small={true} /> `; -exports[`should render correctly 2`] = ` +exports[`should render correctly: Successful 1`] = ` <Level - level="ERROR" + level="OK" small={true} /> `; 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<Props> { } } -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<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/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> = {}): 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<Issue> = {}) { 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 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<Dict<BranchStatusData>>; -} - -const enum Actions { - RegisterBranchStatus = 'REGISTER_BRANCH_STATUS' -} - -type Action = ActionType<typeof registerBranchStatusAction, Actions.RegisterBranchStatus>; - -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<Store>({ - 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; +} |