@@ -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))); |
@@ -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> |
@@ -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 /> |
@@ -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> |
@@ -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'); | |||
} | |||
}); |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} | |||
}; | |||
} |
@@ -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> | |||
); | |||
} | |||
}; | |||
} |
@@ -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> | |||
)} | |||
</> |
@@ -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> |
@@ -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> |
@@ -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> |
@@ -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)); |
@@ -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)); |
@@ -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))); |
@@ -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) => { |
@@ -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' }); | |||
}); | |||
}); |
@@ -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()} |
@@ -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> | |||
`; |
@@ -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)); |
@@ -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) && ( |
@@ -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 |
@@ -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)); |
@@ -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); |
@@ -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} />); | |||
} |
@@ -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} | |||
/> | |||
`; |
@@ -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); |
@@ -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]; | |||
} |
@@ -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: [], |
@@ -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' | |||
}); | |||
}); | |||
}); |
@@ -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] | |||
]); | |||
}); | |||
}); |
@@ -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)); | |||
}; | |||
} |
@@ -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' | |||
} |
@@ -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); | |||
} |
@@ -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; | |||
} |