Browse Source

SONAR-15914 Extract branchstatus from redux

tags/9.4.0.54424
Jeremy Davis 2 years ago
parent
commit
a38eda46c2
36 changed files with 721 additions and 1251 deletions
  1. 4
    9
      server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
  2. 16
    13
      server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
  3. 4
    4
      server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
  4. 28
    26
      server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalContainer-test.tsx.snap
  5. 46
    0
      server/sonar-web/src/main/js/app/components/branch-status/BranchStatusContext.tsx
  6. 100
    0
      server/sonar-web/src/main/js/app/components/branch-status/BranchStatusContextProvider.tsx
  7. 73
    0
      server/sonar-web/src/main/js/app/components/branch-status/__tests__/BranchStatusContextProvider-test.tsx
  8. 59
    0
      server/sonar-web/src/main/js/app/components/branch-status/withBranchStatus.tsx
  9. 51
    0
      server/sonar-web/src/main/js/app/components/branch-status/withBranchStatusActions.tsx
  10. 1
    1
      server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx
  11. 25
    2
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/HeaderMeta-test.tsx.snap
  12. 1
    1
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItem.tsx
  13. 46
    4
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItem-test.tsx.snap
  14. 4
    14
      server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx
  15. 2
    5
      server/sonar-web/src/main/js/apps/component-measures/components/App.tsx
  16. 2
    29
      server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx
  17. 18
    3
      server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
  18. 0
    50
      server/sonar-web/src/main/js/apps/issues/components/__tests__/AppContainer-test.tsx
  19. 70
    54
      server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx
  20. 0
    671
      server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesApp-test.tsx.snap
  21. 7
    34
      server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx
  22. 1
    1
      server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeRow.tsx
  23. 69
    6
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchLikeRow-test.tsx.snap
  24. 2
    5
      server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
  25. 6
    18
      server/sonar-web/src/main/js/components/common/BranchStatus.tsx
  26. 13
    5
      server/sonar-web/src/main/js/components/common/__tests__/BranchStatus-test.tsx
  27. 4
    4
      server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchStatus-test.tsx.snap
  28. 2
    5
      server/sonar-web/src/main/js/components/workspace/WorkspaceComponentViewer.tsx
  29. 11
    0
      server/sonar-web/src/main/js/helpers/branch-like.ts
  30. 26
    0
      server/sonar-web/src/main/js/helpers/testMocks.ts
  31. 0
    149
      server/sonar-web/src/main/js/store/__tests__/branches-test.ts
  32. 22
    10
      server/sonar-web/src/main/js/store/__tests__/globalMessages-test.ts
  33. 0
    115
      server/sonar-web/src/main/js/store/branches.ts
  34. 1
    1
      server/sonar-web/src/main/js/store/globalMessages.ts
  35. 0
    12
      server/sonar-web/src/main/js/store/rootReducer.ts
  36. 7
    0
      server/sonar-web/src/main/js/types/branch-like.ts

+ 4
- 9
server/sonar-web/src/main/js/app/components/ComponentContainer.tsx View File

@@ -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)));

+ 16
- 13
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx View File

@@ -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>

+ 4
- 4
server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx View File

@@ -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 />

+ 28
- 26
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalContainer-test.tsx.snap View File

@@ -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>

+ 46
- 0
server/sonar-web/src/main/js/app/components/branch-status/BranchStatusContext.tsx View File

@@ -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');
}
});

+ 100
- 0
server/sonar-web/src/main/js/app/components/branch-status/BranchStatusContextProvider.tsx View File

@@ -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>
);
}
}

+ 73
- 0
server/sonar-web/src/main/js/app/components/branch-status/__tests__/BranchStatusContextProvider-test.tsx View File

@@ -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>
);
}

+ 59
- 0
server/sonar-web/src/main/js/app/components/branch-status/withBranchStatus.tsx View File

@@ -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>
);
}
};
}

+ 51
- 0
server/sonar-web/src/main/js/app/components/branch-status/withBranchStatusActions.tsx View File

@@ -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>
);
}
};
}

+ 1
- 1
server/sonar-web/src/main/js/app/components/nav/component/HeaderMeta.tsx View File

@@ -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>
)}
</>

+ 25
- 2
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/HeaderMeta-test.tsx.snap View File

@@ -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>

+ 1
- 1
server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItem.tsx View File

@@ -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>

+ 46
- 4
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItem-test.tsx.snap View File

@@ -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>

+ 4
- 14
server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx View File

@@ -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));

+ 2
- 5
server/sonar-web/src/main/js/apps/component-measures/components/App.tsx View File

@@ -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));

+ 2
- 29
server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx View File

@@ -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)));

+ 18
- 3
server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx View File

@@ -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) => {

+ 0
- 50
server/sonar-web/src/main/js/apps/issues/components/__tests__/AppContainer-test.tsx View File

@@ -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' });
});
});

+ 70
- 54
server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx View File

@@ -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()}

+ 0
- 671
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesApp-test.tsx.snap View File

@@ -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>
`;

+ 7
- 34
server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx View File

@@ -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));

+ 1
- 1
server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeRow.tsx View File

@@ -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) && (

+ 69
- 6
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchLikeRow-test.tsx.snap View File

@@ -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

+ 2
- 5
server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx View File

@@ -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));

+ 6
- 18
server/sonar-web/src/main/js/components/common/BranchStatus.tsx View File

@@ -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);

+ 13
- 5
server/sonar-web/src/main/js/components/common/__tests__/BranchStatus-test.tsx View File

@@ -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} />);
}

+ 4
- 4
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchStatus-test.tsx.snap View File

@@ -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}
/>
`;

+ 2
- 5
server/sonar-web/src/main/js/components/workspace/WorkspaceComponentViewer.tsx View File

@@ -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);

+ 11
- 0
server/sonar-web/src/main/js/helpers/branch-like.ts View File

@@ -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];
}

+ 26
- 0
server/sonar-web/src/main/js/helpers/testMocks.ts View File

@@ -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: [],

+ 0
- 149
server/sonar-web/src/main/js/store/__tests__/branches-test.ts View File

@@ -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'
});
});
});

server/sonar-web/src/main/js/store/__tests__/rootReducers-test.tsx → server/sonar-web/src/main/js/store/__tests__/globalMessages-test.ts View File

@@ -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]
]);
});
});

+ 0
- 115
server/sonar-web/src/main/js/store/branches.ts View File

@@ -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));
};
}

+ 1
- 1
server/sonar-web/src/main/js/store/globalMessages.ts View File

@@ -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'
}

+ 0
- 12
server/sonar-web/src/main/js/store/rootReducer.ts View File

@@ -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);
}

+ 7
- 0
server/sonar-web/src/main/js/types/branch-like.ts View File

@@ -17,6 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { 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;
}

Loading…
Cancel
Save