@@ -23,10 +23,3 @@ import throwGlobalError from '../app/utils/throwGlobalError'; | |||
export function getBranches(project: string): Promise<any> { | |||
return getJSON('/api/project_branches/list', { project }).then(r => r.branches, throwGlobalError); | |||
} | |||
export function getBranch(project: string, branch: string): Promise<any> { | |||
return getJSON('/api/project_branches/show', { component: project, branch }).then( | |||
r => r.branch, | |||
throwGlobalError | |||
); | |||
} |
@@ -243,11 +243,11 @@ export function getSources( | |||
return getJSON('/api/sources/lines', data).then(r => r.sources); | |||
} | |||
export function getDuplications(component: string): Promise<any> { | |||
return getJSON('/api/duplications/show', { key: component }); | |||
export function getDuplications(component: string, branch?: string): Promise<any> { | |||
return getJSON('/api/duplications/show', { key: component, branch }); | |||
} | |||
export function getTests(component: string, line: number | string): Promise<any> { | |||
const data = { sourceFileKey: component, sourceFileLineNumber: line }; | |||
export function getTests(component: string, line: number | string, branch?: string): Promise<any> { | |||
const data = { sourceFileKey: component, sourceFileLineNumber: line, branch }; | |||
return getJSON('/api/tests/list', data).then(r => r.tests); | |||
} |
@@ -19,9 +19,13 @@ | |||
*/ | |||
import { getJSON, RequestData } from '../helpers/request'; | |||
export function getMeasures(componentKey: string, metrics: string[]): Promise<any> { | |||
export function getMeasures( | |||
componentKey: string, | |||
metrics: string[], | |||
branch?: string | |||
): Promise<any> { | |||
const url = '/api/measures/component'; | |||
const data = { componentKey, metricKeys: metrics.join(',') }; | |||
const data = { componentKey, metricKeys: metrics.join(','), branch }; | |||
return getJSON(url, data).then(r => r.component.measures); | |||
} | |||
@@ -30,6 +30,7 @@ interface GetProjectActivityResponse { | |||
} | |||
export function getProjectActivity(data: { | |||
branch?: string; | |||
project: string; | |||
category?: string; | |||
p?: number; |
@@ -18,13 +18,13 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import ProjectContainerNotFound from './ProjectContainerNotFound'; | |||
import ComponentNav from './nav/component/ComponentNav'; | |||
import { Branch, Component } from '../types'; | |||
import handleRequiredAuthorization from '../utils/handleRequiredAuthorization'; | |||
import { getBranch } from '../../api/branches'; | |||
import { getBranches } from '../../api/branches'; | |||
import { getComponentData } from '../../api/components'; | |||
import { getComponentNavigation } from '../../api/nav'; | |||
import { MAIN_BRANCH } from '../../helpers/branches'; | |||
interface Props { | |||
children: any; | |||
@@ -34,7 +34,7 @@ interface Props { | |||
} | |||
interface State { | |||
branch: Branch | null; | |||
branches: Branch[]; | |||
loading: boolean; | |||
component: Component | null; | |||
} | |||
@@ -44,7 +44,7 @@ export default class ProjectContainer extends React.PureComponent<Props, State> | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { branch: null, loading: true, component: null }; | |||
this.state = { branches: [], loading: true, component: null }; | |||
} | |||
componentDidMount() { | |||
@@ -52,19 +52,8 @@ export default class ProjectContainer extends React.PureComponent<Props, State> | |||
this.fetchProject(); | |||
} | |||
componentWillReceiveProps(nextProps: Props) { | |||
// if the current branch has been changed, reset `branch` in state | |||
// it prevents unwanted redirect in `overview/App#componentDidMount` | |||
if (nextProps.location.query.branch !== this.props.location.query.branch) { | |||
this.setState({ branch: null }); | |||
} | |||
} | |||
componentDidUpdate(prevProps: Props) { | |||
if ( | |||
prevProps.location.query.id !== this.props.location.query.id || | |||
prevProps.location.query.branch !== this.props.location.query.branch | |||
) { | |||
if (prevProps.location.query.id !== this.props.location.query.id) { | |||
this.fetchProject(); | |||
} | |||
} | |||
@@ -81,30 +70,27 @@ export default class ProjectContainer extends React.PureComponent<Props, State> | |||
fetchProject() { | |||
const { branch, id } = this.props.location.query; | |||
this.setState({ loading: true }); | |||
Promise.all([ | |||
getComponentNavigation(id), | |||
getComponentData(id, branch), | |||
branch && getBranch(id, branch) | |||
]).then( | |||
([nav, data, branch]) => { | |||
if (this.mounted) { | |||
this.setState({ | |||
loading: false, | |||
branch: branch || MAIN_BRANCH, | |||
component: this.addQualifier({ ...nav, ...data }) | |||
}); | |||
const onError = (error: any) => { | |||
if (this.mounted) { | |||
if (error.response && error.response.status === 403) { | |||
handleRequiredAuthorization(); | |||
} else { | |||
this.setState({ loading: false }); | |||
} | |||
}, | |||
error => { | |||
} | |||
}; | |||
Promise.all([getComponentNavigation(id), getComponentData(id, branch)]).then(([nav, data]) => { | |||
const component = this.addQualifier({ ...nav, ...data }); | |||
const project = component.breadcrumbs.find((c: Component) => c.qualifier === 'TRK'); | |||
const branchesRequest = project ? getBranches(project.key) : Promise.resolve([]); | |||
branchesRequest.then(branches => { | |||
if (this.mounted) { | |||
if (error.response && error.response.status === 403) { | |||
handleRequiredAuthorization(); | |||
} else { | |||
this.setState({ loading: false }); | |||
} | |||
this.setState({ loading: false, branches, component }); | |||
} | |||
} | |||
); | |||
}, onError); | |||
}, onError); | |||
} | |||
handleProjectChange = (changes: {}) => { | |||
@@ -114,10 +100,17 @@ export default class ProjectContainer extends React.PureComponent<Props, State> | |||
}; | |||
render() { | |||
const { branch, component } = this.state; | |||
const { query } = this.props.location; | |||
const { branches, component, loading } = this.state; | |||
if (loading) { | |||
return <i className="spinner" />; | |||
} | |||
const branch = branches.find(b => (query.branch ? b.name === query.branch : b.isMain)); | |||
if (!component || !branch) { | |||
return null; | |||
return <ProjectContainerNotFound />; | |||
} | |||
const isFile = ['FIL', 'UTS'].includes(component.qualifier); | |||
@@ -127,7 +120,8 @@ export default class ProjectContainer extends React.PureComponent<Props, State> | |||
<div> | |||
{!isFile && | |||
<ComponentNav | |||
branch={branch} | |||
branches={branches} | |||
currentBranch={branch} | |||
component={component} | |||
conf={configuration} | |||
location={this.props.location} |
@@ -0,0 +1,56 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 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 { Link } from 'react-router'; | |||
import { translate } from '../../helpers/l10n'; | |||
export default class ProjectContainerNotFound extends React.PureComponent { | |||
componentDidMount() { | |||
const html = document.querySelector('html'); | |||
if (html) { | |||
html.classList.add('dashboard-page'); | |||
} | |||
} | |||
componentWillUnmount() { | |||
const html = document.querySelector('html'); | |||
if (html) { | |||
html.classList.remove('dashboard-page'); | |||
} | |||
} | |||
render() { | |||
return ( | |||
<div id="bd" className="page-wrapper-simple"> | |||
<div id="nonav" className="page-simple"> | |||
<h2 className="big-spacer-bottom"> | |||
{translate('dashboard.project_not_found')} | |||
</h2> | |||
<p className="spacer-bottom"> | |||
{translate('dashboard.project_not_found.2')} | |||
</p> | |||
<p> | |||
<Link to="/">Go back to the homepage</Link> | |||
</p> | |||
</div> | |||
</div> | |||
); | |||
} | |||
} |
@@ -17,9 +17,23 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
jest.mock('../../../api/branches', () => ({ getBranches: jest.fn() })); | |||
jest.mock('../../../api/components', () => ({ getComponentData: jest.fn() })); | |||
jest.mock('../../../api/nav', () => ({ getComponentNavigation: jest.fn() })); | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import { shallow, mount } from 'enzyme'; | |||
import ProjectContainer from '../ProjectContainer'; | |||
import { getBranches } from '../../../api/branches'; | |||
import { getComponentData } from '../../../api/components'; | |||
import { getComponentNavigation } from '../../../api/nav'; | |||
import { doAsync } from '../../../helpers/testUtils'; | |||
beforeEach(() => { | |||
(getBranches as jest.Mock<any>).mockClear(); | |||
(getComponentData as jest.Mock<any>).mockClear(); | |||
(getComponentNavigation as jest.Mock<any>).mockClear(); | |||
}); | |||
it('changes component', () => { | |||
const Inner = () => <div />; | |||
@@ -31,7 +45,7 @@ it('changes component', () => { | |||
); | |||
(wrapper.instance() as ProjectContainer).mounted = true; | |||
wrapper.setState({ | |||
branch: { isMain: true }, | |||
branches: [{ isMain: true }], | |||
component: { qualifier: 'TRK', visibility: 'public' }, | |||
loading: false | |||
}); | |||
@@ -39,3 +53,50 @@ it('changes component', () => { | |||
(wrapper.find(Inner).prop('onComponentChange') as Function)({ visibility: 'private' }); | |||
expect(wrapper.state().component).toEqual({ qualifier: 'TRK', visibility: 'private' }); | |||
}); | |||
it("loads branches for module's project", () => { | |||
(getBranches as jest.Mock<any>).mockImplementation(() => Promise.resolve([])); | |||
(getComponentData as jest.Mock<any>).mockImplementation(() => Promise.resolve({})); | |||
(getComponentNavigation as jest.Mock<any>).mockImplementation(() => | |||
Promise.resolve({ | |||
breadcrumbs: [ | |||
{ key: 'projectKey', name: 'project', qualifier: 'TRK' }, | |||
{ key: 'moduleKey', name: 'module', qualifier: 'BRC' } | |||
] | |||
}) | |||
); | |||
mount( | |||
<ProjectContainer location={{ query: { id: 'moduleKey' } }}> | |||
<div /> | |||
</ProjectContainer> | |||
); | |||
return doAsync().then(() => { | |||
expect(getBranches).toBeCalledWith('projectKey'); | |||
expect(getComponentData).toBeCalledWith('moduleKey', undefined); | |||
expect(getComponentNavigation).toBeCalledWith('moduleKey'); | |||
}); | |||
}); | |||
it("doesn't load branches portfolio", () => { | |||
(getBranches as jest.Mock<any>).mockImplementation(() => Promise.resolve([])); | |||
(getComponentData as jest.Mock<any>).mockImplementation(() => Promise.resolve({})); | |||
(getComponentNavigation as jest.Mock<any>).mockImplementation(() => | |||
Promise.resolve({ | |||
breadcrumbs: [{ key: 'portfolioKey', name: 'portfolio', qualifier: 'VW' }] | |||
}) | |||
); | |||
mount( | |||
<ProjectContainer location={{ query: { id: 'portfolioKey' } }}> | |||
<div /> | |||
</ProjectContainer> | |||
); | |||
return doAsync().then(() => { | |||
expect(getBranches).not.toBeCalled(); | |||
expect(getComponentData).toBeCalledWith('portfolioKey', undefined); | |||
expect(getComponentNavigation).toBeCalledWith('portfolioKey'); | |||
}); | |||
}); |
@@ -1,4 +1,6 @@ | |||
.branch-status { | |||
min-width: 64px; | |||
text-align: right; | |||
} | |||
.branch-status-indicator { |
@@ -20,6 +20,7 @@ | |||
import * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import { Branch } from '../../../types'; | |||
import Level from '../../../../components/ui/Level'; | |||
import BugIcon from '../../../../components/icons-components/BugIcon'; | |||
import CodeSmellIcon from '../../../../components/icons-components/CodeSmellIcon'; | |||
import VulnerabilityIcon from '../../../../components/icons-components/VulnerabilityIcon'; | |||
@@ -32,42 +33,50 @@ interface Props { | |||
} | |||
export default function BranchStatus({ branch, concise = false }: Props) { | |||
// TODO handle long-living branches | |||
if (!isShortLivingBranch(branch)) { | |||
return null; | |||
} | |||
if (isShortLivingBranch(branch)) { | |||
if (!branch.status) { | |||
return null; | |||
} | |||
const totalIssues = branch.status.bugs + branch.status.vulnerabilities + branch.status.codeSmells; | |||
const totalIssues = | |||
branch.status.bugs + branch.status.vulnerabilities + branch.status.codeSmells; | |||
return ( | |||
<ul className="list-inline branch-status"> | |||
<li> | |||
<i | |||
className={classNames('branch-status-indicator', { | |||
'is-failed': totalIssues > 0, | |||
'is-passed': totalIssues === 0 | |||
})} | |||
/> | |||
</li> | |||
{concise && | |||
<li> | |||
{totalIssues} | |||
</li>} | |||
{!concise && | |||
<li> | |||
{branch.status.bugs} | |||
<BugIcon className="little-spacer-left" /> | |||
</li>} | |||
{!concise && | |||
return ( | |||
<ul className="list-inline branch-status"> | |||
<li> | |||
{branch.status.vulnerabilities} | |||
<VulnerabilityIcon className="little-spacer-left" /> | |||
</li>} | |||
{!concise && | |||
<li> | |||
{branch.status.codeSmells} | |||
<CodeSmellIcon className="little-spacer-left" /> | |||
</li>} | |||
</ul> | |||
); | |||
<i | |||
className={classNames('branch-status-indicator', { | |||
'is-failed': totalIssues > 0, | |||
'is-passed': totalIssues === 0 | |||
})} | |||
/> | |||
</li> | |||
{concise && | |||
<li> | |||
{totalIssues} | |||
</li>} | |||
{!concise && | |||
<li> | |||
{branch.status.bugs} | |||
<BugIcon className="little-spacer-left" /> | |||
</li>} | |||
{!concise && | |||
<li> | |||
{branch.status.vulnerabilities} | |||
<VulnerabilityIcon className="little-spacer-left" /> | |||
</li>} | |||
{!concise && | |||
<li> | |||
{branch.status.codeSmells} | |||
<CodeSmellIcon className="little-spacer-left" /> | |||
</li>} | |||
</ul> | |||
); | |||
} else { | |||
if (!branch.status) { | |||
return null; | |||
} | |||
return <Level level={branch.status.qualityGateStatus} small={true} />; | |||
} | |||
} |
@@ -31,7 +31,8 @@ import { STATUSES } from '../../../../apps/background-tasks/constants'; | |||
import './ComponentNav.css'; | |||
interface Props { | |||
branch: Branch; | |||
branches: Branch[]; | |||
currentBranch: Branch; | |||
component: Component; | |||
conf: ComponentConfiguration; | |||
location: {}; | |||
@@ -98,17 +99,21 @@ export default class ComponentNav extends React.PureComponent<Props, State> { | |||
breadcrumbs={this.props.component.breadcrumbs} | |||
/> | |||
<ComponentNavBranch branch={this.props.branch} project={this.props.component} /> | |||
<ComponentNavBranch | |||
branches={this.props.branches} | |||
currentBranch={this.props.currentBranch} | |||
project={this.props.component} | |||
/> | |||
<ComponentNavMeta | |||
branch={this.props.branch} | |||
branch={this.props.currentBranch} | |||
component={this.props.component} | |||
conf={this.props.conf} | |||
incremental={this.state.incremental} | |||
/> | |||
<ComponentNavMenu | |||
branch={this.props.branch} | |||
branch={this.props.currentBranch} | |||
component={this.props.component} | |||
conf={this.props.conf} | |||
/> |
@@ -22,10 +22,12 @@ import * as classNames from 'classnames'; | |||
import ComponentNavBranchesMenu from './ComponentNavBranchesMenu'; | |||
import { Branch, Component } from '../../../types'; | |||
import BranchIcon from '../../../../components/icons-components/BranchIcon'; | |||
import { getBranchDisplayName } from '../../../../helpers/branches'; | |||
import { isShortLivingBranch } from '../../../../helpers/branches'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
interface Props { | |||
branch: Branch; | |||
branches: Branch[]; | |||
currentBranch: Branch; | |||
project: Component; | |||
} | |||
@@ -42,7 +44,10 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State | |||
} | |||
componentWillReceiveProps(nextProps: Props) { | |||
if (nextProps.project !== this.props.project || nextProps.branch !== this.props.branch) { | |||
if ( | |||
nextProps.project !== this.props.project || | |||
nextProps.currentBranch !== this.props.currentBranch | |||
) { | |||
this.setState({ open: false }); | |||
} | |||
} | |||
@@ -65,19 +70,27 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State | |||
}; | |||
render() { | |||
const { currentBranch } = this.props; | |||
return ( | |||
<div className={classNames('navbar-context-branches', 'dropdown', { open: this.state.open })}> | |||
<a className="link-base-color link-no-underline" href="#" onClick={this.handleClick}> | |||
<BranchIcon className="little-spacer-right" /> | |||
{getBranchDisplayName(this.props.branch)} | |||
<BranchIcon branch={currentBranch} className="little-spacer-right" /> | |||
{currentBranch.name} | |||
<i className="icon-dropdown little-spacer-left" /> | |||
</a> | |||
{this.state.open && | |||
<ComponentNavBranchesMenu | |||
branch={this.props.branch} | |||
branches={this.props.branches} | |||
currentBranch={currentBranch} | |||
onClose={this.closeDropdown} | |||
project={this.props.project} | |||
/>} | |||
{isShortLivingBranch(currentBranch) && | |||
!currentBranch.isOrphan && | |||
<span className="note big-spacer-left text-lowercase"> | |||
{translate('from')} <strong>{currentBranch.mergeBranch}</strong> | |||
</span>} | |||
</div> | |||
); | |||
} |
@@ -19,83 +19,47 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import * as PropTypes from 'prop-types'; | |||
import { sortBy } from 'lodash'; | |||
import ComponentNavBranchesMenuItem from './ComponentNavBranchesMenuItem'; | |||
import { Branch, Component } from '../../../types'; | |||
import { getBranches } from '../../../../api/branches'; | |||
import { isShortLivingBranch, getBranchDisplayName } from '../../../../helpers/branches'; | |||
import { | |||
sortBranchesAsTree, | |||
isLongLivingBranch, | |||
isShortLivingBranch | |||
} from '../../../../helpers/branches'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import { getProjectBranchUrl } from '../../../../helpers/urls'; | |||
interface Props { | |||
branch: Branch; | |||
branches: Branch[]; | |||
currentBranch: Branch; | |||
onClose: () => void; | |||
project: Component; | |||
} | |||
interface State { | |||
branches: Branch[]; | |||
loading: boolean; | |||
query: string; | |||
selected: string | null; | |||
} | |||
export default class ComponentNavBranchesMenu extends React.PureComponent<Props, State> { | |||
private mounted: boolean; | |||
private node: HTMLElement | null; | |||
state = { query: '', selected: null }; | |||
static contextTypes = { | |||
router: PropTypes.object | |||
}; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
branches: [], | |||
loading: true, | |||
query: '', | |||
selected: null | |||
}; | |||
} | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.fetchBranches(); | |||
window.addEventListener('click', this.handleClickOutside); | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
window.removeEventListener('click', this.handleClickOutside); | |||
} | |||
fetchBranches = () => { | |||
this.setState({ loading: true }); | |||
getBranches(this.props.project.key).then( | |||
(branches: Branch[]) => { | |||
if (this.mounted) { | |||
this.setState({ branches: this.sortBranches(branches), loading: false }); | |||
} | |||
}, | |||
() => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
} | |||
); | |||
}; | |||
sortBranches = (branches: Branch[]): Branch[] => | |||
sortBy( | |||
branches, | |||
branch => !branch.isMain, // main branch first | |||
branch => !isShortLivingBranch(branch), // then short-living branches | |||
branch => getBranchDisplayName(branch) // then by name | |||
); | |||
getFilteredBranches = () => | |||
this.state.branches.filter(branch => | |||
getBranchDisplayName(branch).toLowerCase().includes(this.state.query.toLowerCase()) | |||
sortBranchesAsTree(this.props.branches).filter(branch => | |||
branch.name.toLowerCase().includes(this.state.query.toLowerCase()) | |||
); | |||
handleClickOutside = (event: Event) => { | |||
@@ -130,9 +94,7 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props, | |||
openSelected = () => { | |||
const selected = this.getSelected(); | |||
const branch = this.getFilteredBranches().find( | |||
branch => getBranchDisplayName(branch) === selected | |||
); | |||
const branch = this.getFilteredBranches().find(branch => branch.name === selected); | |||
if (branch) { | |||
this.context.router.push(this.getProjectBranchUrl(branch)); | |||
} | |||
@@ -141,33 +103,33 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props, | |||
selectPrevious = () => { | |||
const selected = this.getSelected(); | |||
const branches = this.getFilteredBranches(); | |||
const index = branches.findIndex(branch => getBranchDisplayName(branch) === selected); | |||
const index = branches.findIndex(branch => branch.name === selected); | |||
if (index > 0) { | |||
this.setState({ selected: getBranchDisplayName(branches[index - 1]) }); | |||
this.setState({ selected: branches[index - 1].name }); | |||
} | |||
}; | |||
selectNext = () => { | |||
const selected = this.getSelected(); | |||
const branches = this.getFilteredBranches(); | |||
const index = branches.findIndex(branch => getBranchDisplayName(branch) === selected); | |||
const index = branches.findIndex(branch => branch.name === selected); | |||
if (index >= 0 && index < branches.length - 1) { | |||
this.setState({ selected: getBranchDisplayName(branches[index + 1]) }); | |||
this.setState({ selected: branches[index + 1].name }); | |||
} | |||
}; | |||
handleSelect = (branch: Branch) => { | |||
this.setState({ selected: getBranchDisplayName(branch) }); | |||
this.setState({ selected: branch.name }); | |||
}; | |||
getSelected = () => { | |||
const branches = this.getFilteredBranches(); | |||
return this.state.selected || (branches.length > 0 && getBranchDisplayName(branches[0])); | |||
return this.state.selected || (branches.length > 0 && branches[0].name); | |||
}; | |||
getProjectBranchUrl = (branch: Branch) => getProjectBranchUrl(this.props.project.key, branch); | |||
isSelected = (branch: Branch) => getBranchDisplayName(branch) === this.getSelected(); | |||
isSelected = (branch: Branch) => branch.name === this.getSelected(); | |||
renderSearch = () => | |||
<div className="search-box menu-search"> | |||
@@ -187,35 +149,47 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props, | |||
renderBranchesList = () => { | |||
const branches = this.getFilteredBranches(); | |||
const selected = this.getSelected(); | |||
return branches.length > 0 | |||
? <ul className="menu"> | |||
{branches.map(branch => | |||
<ComponentNavBranchesMenuItem | |||
branch={branch} | |||
component={this.props.project} | |||
key={getBranchDisplayName(branch)} | |||
onSelect={this.handleSelect} | |||
selected={getBranchDisplayName(branch) === selected} | |||
/> | |||
)} | |||
</ul> | |||
: <div className="menu-message note"> | |||
if (branches.length === 0) { | |||
return ( | |||
<div className="menu-message note"> | |||
{translate('no_results')} | |||
</div>; | |||
</div> | |||
); | |||
} | |||
const menu: JSX.Element[] = []; | |||
branches.forEach((branch, index) => { | |||
const isOrphan = isShortLivingBranch(branch) && branch.isOrphan; | |||
const previous = index > 0 ? branches[index - 1] : null; | |||
const isPreviousOrphan = isShortLivingBranch(previous) ? previous.isOrphan : false; | |||
if (isLongLivingBranch(branch) || (isOrphan && !isPreviousOrphan)) { | |||
menu.push(<li key={`divider-${branch.name}`} className="divider" />); | |||
} | |||
menu.push( | |||
<ComponentNavBranchesMenuItem | |||
branch={branch} | |||
component={this.props.project} | |||
key={branch.name} | |||
onSelect={this.handleSelect} | |||
selected={branch.name === selected} | |||
/> | |||
); | |||
}); | |||
return ( | |||
<ul className="menu"> | |||
{menu} | |||
</ul> | |||
); | |||
}; | |||
render() { | |||
return ( | |||
<div className="dropdown-menu dropdown-menu-shadow" ref={node => (this.node = node)}> | |||
{this.state.loading | |||
? <i className="spinner" /> | |||
: <div> | |||
{this.renderSearch()} | |||
{this.renderBranchesList()} | |||
</div>} | |||
{this.renderSearch()} | |||
{this.renderBranchesList()} | |||
</div> | |||
); | |||
} |
@@ -23,7 +23,7 @@ import * as classNames from 'classnames'; | |||
import BranchStatus from './BranchStatus'; | |||
import { Branch, Component } from '../../../types'; | |||
import BranchIcon from '../../../../components/icons-components/BranchIcon'; | |||
import { isShortLivingBranch, getBranchDisplayName } from '../../../../helpers/branches'; | |||
import { isShortLivingBranch } from '../../../../helpers/branches'; | |||
import { getProjectBranchUrl } from '../../../../helpers/urls'; | |||
interface Props { | |||
@@ -34,14 +34,12 @@ interface Props { | |||
} | |||
export default function ComponentNavBranchesMenuItem({ branch, ...props }: Props) { | |||
const displayName = getBranchDisplayName(branch); | |||
const handleMouseEnter = () => { | |||
props.onSelect(branch); | |||
}; | |||
return ( | |||
<li key={displayName} onMouseEnter={handleMouseEnter}> | |||
<li key={branch.name} onMouseEnter={handleMouseEnter}> | |||
<Link | |||
className={classNames('navbar-context-meta-branch-menu-item', { | |||
active: props.selected | |||
@@ -49,11 +47,12 @@ export default function ComponentNavBranchesMenuItem({ branch, ...props }: Props | |||
to={getProjectBranchUrl(props.component.key, branch)}> | |||
<div> | |||
<BranchIcon | |||
branch={branch} | |||
className={classNames('little-spacer-right', { | |||
'big-spacer-left': isShortLivingBranch(branch) | |||
'big-spacer-left': isShortLivingBranch(branch) && !branch.isOrphan | |||
})} | |||
/> | |||
{displayName} | |||
{branch.name} | |||
</div> | |||
<div className="big-spacer-left note"> | |||
<BranchStatus branch={branch} concise={true} /> |
@@ -22,7 +22,7 @@ import { Link } from 'react-router'; | |||
import * as classNames from 'classnames'; | |||
import { Branch, Component, ComponentExtension, ComponentConfiguration } from '../../../types'; | |||
import NavBarTabs from '../../../../components/nav/NavBarTabs'; | |||
import { isShortLivingBranch } from '../../../../helpers/branches'; | |||
import { isShortLivingBranch, getBranchName } from '../../../../helpers/branches'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
const SETTINGS_URLS = [ | |||
@@ -71,7 +71,12 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
const pathname = this.isView() ? '/portfolio' : '/dashboard'; | |||
return ( | |||
<li> | |||
<Link to={{ pathname, query: { id: this.props.component.key } }} activeClassName="active"> | |||
<Link | |||
to={{ | |||
pathname, | |||
query: { branch: getBranchName(this.props.branch), id: this.props.component.key } | |||
}} | |||
activeClassName="active"> | |||
{translate('overview.page')} | |||
</Link> | |||
</li> | |||
@@ -88,7 +93,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
<Link | |||
to={{ | |||
pathname: '/code', | |||
query: { branch: this.props.branch.name, id: this.props.component.key } | |||
query: { branch: getBranchName(this.props.branch), id: this.props.component.key } | |||
}} | |||
activeClassName="active"> | |||
{this.isView() || this.isApplication() | |||
@@ -111,7 +116,10 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
return ( | |||
<li> | |||
<Link | |||
to={{ pathname: '/project/activity', query: { id: this.props.component.key } }} | |||
to={{ | |||
pathname: '/project/activity', | |||
query: { branch: getBranchName(this.props.branch), id: this.props.component.key } | |||
}} | |||
activeClassName="active"> | |||
{translate('project_activity.page')} | |||
</Link> | |||
@@ -126,7 +134,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
to={{ | |||
pathname: '/project/issues', | |||
query: { | |||
branch: this.props.branch.name, | |||
branch: getBranchName(this.props.branch), | |||
id: this.props.component.key, | |||
resolved: 'false' | |||
} | |||
@@ -146,7 +154,10 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
return ( | |||
<li> | |||
<Link | |||
to={{ pathname: '/component_measures', query: { id: this.props.component.key } }} | |||
to={{ | |||
pathname: '/component_measures', | |||
query: { branch: getBranchName(this.props.branch), id: this.props.component.key } | |||
}} | |||
activeClassName="active"> | |||
{translate('layout.measures')} | |||
</Link> | |||
@@ -155,7 +166,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
} | |||
renderAdministration() { | |||
if (isShortLivingBranch(this.props.branch)) { | |||
if (!this.props.branch.isMain) { | |||
return null; | |||
} | |||
@@ -25,6 +25,7 @@ import Tooltip from '../../../../components/controls/Tooltip'; | |||
import PendingIcon from '../../../../components/icons-components/PendingIcon'; | |||
import DateTimeFormatter from '../../../../components/intl/DateTimeFormatter'; | |||
import { translate, translateWithParameters } from '../../../../helpers/l10n'; | |||
import { isShortLivingBranch } from '../../../../helpers/branches'; | |||
interface Props { | |||
branch: Branch; | |||
@@ -114,7 +115,7 @@ export default function ComponentNavMeta(props: Props) { | |||
); | |||
} | |||
if (!props.branch.isMain) { | |||
if (isShortLivingBranch(props.branch)) { | |||
metaList.push( | |||
<li className="navbar-context-meta-branch" key="branch-status"> | |||
<BranchStatus branch={props.branch} /> |
@@ -20,25 +20,44 @@ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import BranchStatus from '../BranchStatus'; | |||
import { BranchType } from '../../../../types'; | |||
import { BranchType, LongLivingBranch } from '../../../../types'; | |||
it('renders', () => { | |||
check(0, 0, 0); | |||
check(0, 1, 0); | |||
check(7, 3, 6); | |||
it('renders status of short-living branches', () => { | |||
checkShort(0, 0, 0); | |||
checkShort(0, 1, 0); | |||
checkShort(7, 3, 6); | |||
function checkShort(bugs: number, codeSmells: number, vulnerabilities: number) { | |||
expect( | |||
shallow( | |||
<BranchStatus | |||
branch={{ | |||
isMain: false, | |||
mergeBranch: 'master', | |||
name: 'foo', | |||
status: { bugs, codeSmells, vulnerabilities }, | |||
type: BranchType.SHORT | |||
}} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
} | |||
}); | |||
function check(bugs: number, codeSmells: number, vulnerabilities: number) { | |||
expect( | |||
shallow( | |||
<BranchStatus | |||
branch={{ | |||
isMain: false, | |||
name: 'foo', | |||
status: { bugs, codeSmells, vulnerabilities }, | |||
type: BranchType.SHORT | |||
}} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
} | |||
it('renders status of long-living branches', () => { | |||
checkLong(); | |||
checkLong('OK'); | |||
checkLong('ERROR'); | |||
function checkLong(qualityGateStatus?: string) { | |||
const branch: LongLivingBranch = { | |||
isMain: false, | |||
name: 'foo', | |||
type: BranchType.LONG | |||
}; | |||
if (qualityGateStatus) { | |||
branch.status = { qualityGateStatus }; | |||
} | |||
expect(shallow(<BranchStatus branch={branch} />)).toMatchSnapshot(); | |||
} | |||
}); |
@@ -24,26 +24,33 @@ import { BranchType, ShortLivingBranch, MainBranch, Component } from '../../../. | |||
import { click } from '../../../../../helpers/testUtils'; | |||
it('renders main branch', () => { | |||
const branch: MainBranch = { isMain: true, name: undefined, type: BranchType.LONG }; | |||
const branch: MainBranch = { isMain: true, name: 'master' }; | |||
const component = {} as Component; | |||
expect(shallow(<ComponentNavBranch branch={branch} project={component} />)).toMatchSnapshot(); | |||
expect( | |||
shallow(<ComponentNavBranch branches={[]} currentBranch={branch} project={component} />) | |||
).toMatchSnapshot(); | |||
}); | |||
it('renders short-living branch', () => { | |||
const branch: ShortLivingBranch = { | |||
isMain: false, | |||
mergeBranch: 'master', | |||
name: 'foo', | |||
status: { bugs: 0, codeSmells: 0, vulnerabilities: 0 }, | |||
type: BranchType.SHORT | |||
}; | |||
const component = {} as Component; | |||
expect(shallow(<ComponentNavBranch branch={branch} project={component} />)).toMatchSnapshot(); | |||
expect( | |||
shallow(<ComponentNavBranch branches={[]} currentBranch={branch} project={component} />) | |||
).toMatchSnapshot(); | |||
}); | |||
it('opens menu', () => { | |||
const branch: MainBranch = { isMain: true, name: undefined, type: BranchType.LONG }; | |||
const branch: MainBranch = { isMain: true, name: 'master' }; | |||
const component = {} as Component; | |||
const wrapper = shallow(<ComponentNavBranch branch={branch} project={component} />); | |||
const wrapper = shallow( | |||
<ComponentNavBranch branches={[]} currentBranch={branch} project={component} /> | |||
); | |||
expect(wrapper.find('ComponentNavBranchesMenu')).toHaveLength(0); | |||
click(wrapper.find('a')); | |||
expect(wrapper.find('ComponentNavBranchesMenu')).toHaveLength(1); |
@@ -29,40 +29,43 @@ import { | |||
} from '../../../../types'; | |||
import { elementKeydown } from '../../../../../helpers/testUtils'; | |||
const project = { key: 'component' } as Component; | |||
it('renders list', () => { | |||
const component = { key: 'component' } as Component; | |||
const wrapper = shallow( | |||
<ComponentNavBranchesMenu branch={mainBranch()} onClose={jest.fn()} project={component} /> | |||
); | |||
wrapper.setState({ | |||
branches: [mainBranch(), shortBranch('foo'), longBranch('bar')], | |||
loading: false | |||
}); | |||
expect(wrapper).toMatchSnapshot(); | |||
expect( | |||
shallow( | |||
<ComponentNavBranchesMenu | |||
branches={[mainBranch(), shortBranch('foo'), longBranch('bar'), shortBranch('baz', true)]} | |||
currentBranch={mainBranch()} | |||
onClose={jest.fn()} | |||
project={project} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
it('searches', () => { | |||
const component = { key: 'component' } as Component; | |||
const wrapper = shallow( | |||
<ComponentNavBranchesMenu branch={mainBranch()} onClose={jest.fn()} project={component} /> | |||
<ComponentNavBranchesMenu | |||
branches={[mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')]} | |||
currentBranch={mainBranch()} | |||
onClose={jest.fn()} | |||
project={project} | |||
/> | |||
); | |||
wrapper.setState({ | |||
branches: [mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')], | |||
loading: false, | |||
query: 'bar' | |||
}); | |||
wrapper.setState({ query: 'bar' }); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('selects next & previous', () => { | |||
const component = { key: 'component' } as Component; | |||
const wrapper = shallow( | |||
<ComponentNavBranchesMenu branch={mainBranch()} onClose={jest.fn()} project={component} /> | |||
<ComponentNavBranchesMenu | |||
branches={[mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')]} | |||
currentBranch={mainBranch()} | |||
onClose={jest.fn()} | |||
project={project} | |||
/> | |||
); | |||
wrapper.setState({ | |||
branches: [mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')], | |||
loading: false | |||
}); | |||
elementKeydown(wrapper.find('input'), 40); | |||
wrapper.update(); | |||
expect(wrapper.state().selected).toBe('foo'); | |||
@@ -75,12 +78,14 @@ it('selects next & previous', () => { | |||
}); | |||
function mainBranch(): MainBranch { | |||
return { isMain: true, name: undefined, type: BranchType.LONG }; | |||
return { isMain: true, name: 'master' }; | |||
} | |||
function shortBranch(name: string): ShortLivingBranch { | |||
function shortBranch(name: string, isOrphan?: true): ShortLivingBranch { | |||
return { | |||
isMain: false, | |||
isOrphan, | |||
mergeBranch: 'master', | |||
name, | |||
status: { bugs: 0, codeSmells: 0, vulnerabilities: 0 }, | |||
type: BranchType.SHORT |
@@ -24,7 +24,7 @@ import { BranchType, MainBranch, ShortLivingBranch, Component } from '../../../. | |||
it('renders main branch', () => { | |||
const component = { key: 'component' } as Component; | |||
const mainBranch: MainBranch = { isMain: true, name: undefined, type: BranchType.LONG }; | |||
const mainBranch: MainBranch = { isMain: true, name: 'master' }; | |||
expect( | |||
shallow( | |||
<ComponentNavBranchesMenuItem | |||
@@ -41,6 +41,29 @@ it('renders short-living branch', () => { | |||
const component = { key: 'component' } as Component; | |||
const shortBranch: ShortLivingBranch = { | |||
isMain: false, | |||
mergeBranch: 'master', | |||
name: 'foo', | |||
status: { bugs: 1, codeSmells: 2, vulnerabilities: 3 }, | |||
type: BranchType.SHORT | |||
}; | |||
expect( | |||
shallow( | |||
<ComponentNavBranchesMenuItem | |||
branch={shortBranch} | |||
component={component} | |||
onSelect={jest.fn()} | |||
selected={false} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
it('renders short-living orhpan branch', () => { | |||
const component = { key: 'component' } as Component; | |||
const shortBranch: ShortLivingBranch = { | |||
isMain: false, | |||
isOrphan: true, | |||
mergeBranch: 'master', | |||
name: 'foo', | |||
status: { bugs: 1, codeSmells: 2, vulnerabilities: 3 }, | |||
type: BranchType.SHORT |
@@ -20,7 +20,18 @@ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import ComponentNavMenu from '../ComponentNavMenu'; | |||
import { Branch, Component } from '../../../../types'; | |||
import { | |||
Component, | |||
ShortLivingBranch, | |||
BranchType, | |||
LongLivingBranch, | |||
MainBranch | |||
} from '../../../../types'; | |||
const mainBranch: MainBranch = { | |||
isMain: true, | |||
name: 'master' | |||
}; | |||
it('should work with extensions', () => { | |||
const component = { | |||
@@ -33,9 +44,7 @@ it('should work with extensions', () => { | |||
extensions: [{ key: 'foo', name: 'Foo' }] | |||
}; | |||
expect( | |||
shallow( | |||
<ComponentNavMenu branch={{} as Branch} component={component as Component} conf={conf} /> | |||
) | |||
shallow(<ComponentNavMenu branch={mainBranch} component={component as Component} conf={conf} />) | |||
).toMatchSnapshot(); | |||
}); | |||
@@ -53,8 +62,30 @@ it('should work with multiple extensions', () => { | |||
extensions: [{ key: 'foo', name: 'Foo' }, { key: 'bar', name: 'Bar' }] | |||
}; | |||
expect( | |||
shallow( | |||
<ComponentNavMenu branch={{} as Branch} component={component as Component} conf={conf} /> | |||
) | |||
shallow(<ComponentNavMenu branch={mainBranch} component={component as Component} conf={conf} />) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should work for short-living branches', () => { | |||
const branch: ShortLivingBranch = { | |||
isMain: false, | |||
mergeBranch: 'master', | |||
name: 'feature', | |||
status: { bugs: 0, codeSmells: 2, vulnerabilities: 3 }, | |||
type: BranchType.SHORT | |||
}; | |||
const component = { key: 'foo', qualifier: 'TRK' } as Component; | |||
const conf = { showSettings: true }; | |||
expect( | |||
shallow(<ComponentNavMenu branch={branch} component={component} conf={conf} />) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should work for long-living branches', () => { | |||
const branch: LongLivingBranch = { isMain: false, name: 'release', type: BranchType.LONG }; | |||
const component = { key: 'foo', qualifier: 'TRK' } as Component; | |||
const conf = { showSettings: true }; | |||
expect( | |||
shallow(<ComponentNavMenu branch={branch} component={component} conf={conf} />) | |||
).toMatchSnapshot(); | |||
}); |
@@ -20,7 +20,13 @@ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import ComponentNavMeta from '../ComponentNavMeta'; | |||
import { Branch, Component } from '../../../../types'; | |||
import { | |||
Branch, | |||
Component, | |||
BranchType, | |||
ShortLivingBranch, | |||
LongLivingBranch | |||
} from '../../../../types'; | |||
it('renders incremental badge', () => { | |||
check(true); | |||
@@ -39,3 +45,28 @@ it('renders incremental badge', () => { | |||
).toHaveLength(incremental ? 1 : 0); | |||
} | |||
}); | |||
it('renders status of short-living branch', () => { | |||
const branch: ShortLivingBranch = { | |||
isMain: false, | |||
mergeBranch: 'master', | |||
name: 'feature', | |||
status: { bugs: 0, codeSmells: 2, vulnerabilities: 3 }, | |||
type: BranchType.SHORT | |||
}; | |||
expect( | |||
shallow(<ComponentNavMeta branch={branch} component={{ key: 'foo' } as Component} conf={{}} />) | |||
).toMatchSnapshot(); | |||
}); | |||
it('renders nothing for long-living branch', () => { | |||
const branch: LongLivingBranch = { | |||
isMain: false, | |||
name: 'release', | |||
status: { qualityGateStatus: 'OK' }, | |||
type: BranchType.LONG | |||
}; | |||
expect( | |||
shallow(<ComponentNavMeta branch={branch} component={{ key: 'foo' } as Component} conf={{}} />) | |||
).toMatchSnapshot(); | |||
}); |
@@ -1,6 +1,22 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders 1`] = ` | |||
exports[`renders status of long-living branches 1`] = `null`; | |||
exports[`renders status of long-living branches 2`] = ` | |||
<Level | |||
level="OK" | |||
small={true} | |||
/> | |||
`; | |||
exports[`renders status of long-living branches 3`] = ` | |||
<Level | |||
level="ERROR" | |||
small={true} | |||
/> | |||
`; | |||
exports[`renders status of short-living branches 1`] = ` | |||
<ul | |||
className="list-inline branch-status" | |||
> | |||
@@ -30,7 +46,7 @@ exports[`renders 1`] = ` | |||
</ul> | |||
`; | |||
exports[`renders 2`] = ` | |||
exports[`renders status of short-living branches 2`] = ` | |||
<ul | |||
className="list-inline branch-status" | |||
> | |||
@@ -60,7 +76,7 @@ exports[`renders 2`] = ` | |||
</ul> | |||
`; | |||
exports[`renders 3`] = ` | |||
exports[`renders status of short-living branches 3`] = ` | |||
<ul | |||
className="list-inline branch-status" | |||
> |
@@ -10,6 +10,12 @@ exports[`renders main branch 1`] = ` | |||
onClick={[Function]} | |||
> | |||
<BranchIcon | |||
branch={ | |||
Object { | |||
"isMain": true, | |||
"name": "master", | |||
} | |||
} | |||
className="little-spacer-right" | |||
/> | |||
master | |||
@@ -30,6 +36,19 @@ exports[`renders short-living branch 1`] = ` | |||
onClick={[Function]} | |||
> | |||
<BranchIcon | |||
branch={ | |||
Object { | |||
"isMain": false, | |||
"mergeBranch": "master", | |||
"name": "foo", | |||
"status": Object { | |||
"bugs": 0, | |||
"codeSmells": 0, | |||
"vulnerabilities": 0, | |||
}, | |||
"type": "SHORT", | |||
} | |||
} | |||
className="little-spacer-right" | |||
/> | |||
foo | |||
@@ -37,5 +56,14 @@ exports[`renders short-living branch 1`] = ` | |||
className="icon-dropdown little-spacer-left" | |||
/> | |||
</a> | |||
<span | |||
className="note big-spacer-left text-lowercase" | |||
> | |||
from | |||
<strong> | |||
master | |||
</strong> | |||
</span> | |||
</div> | |||
`; |
@@ -4,85 +4,139 @@ exports[`renders list 1`] = ` | |||
<div | |||
className="dropdown-menu dropdown-menu-shadow" | |||
> | |||
<div> | |||
<div | |||
className="search-box menu-search" | |||
<div | |||
className="search-box menu-search" | |||
> | |||
<button | |||
className="search-box-submit button-clean" | |||
> | |||
<button | |||
className="search-box-submit button-clean" | |||
> | |||
<i | |||
className="icon-search-new" | |||
/> | |||
</button> | |||
<input | |||
autoFocus={true} | |||
className="search-box-input" | |||
onChange={[Function]} | |||
onKeyDown={[Function]} | |||
placeholder="search_verb" | |||
type="search" | |||
value="" | |||
<i | |||
className="icon-search-new" | |||
/> | |||
</div> | |||
<ul | |||
className="menu" | |||
> | |||
<ComponentNavBranchesMenuItem | |||
branch={ | |||
Object { | |||
"isMain": true, | |||
"name": undefined, | |||
"type": "LONG", | |||
} | |||
</button> | |||
<input | |||
autoFocus={true} | |||
className="search-box-input" | |||
onChange={[Function]} | |||
onKeyDown={[Function]} | |||
placeholder="search_verb" | |||
type="search" | |||
value="" | |||
/> | |||
</div> | |||
<ul | |||
className="menu" | |||
> | |||
<ComponentNavBranchesMenuItem | |||
branch={ | |||
Object { | |||
"isMain": true, | |||
"name": "master", | |||
} | |||
component={ | |||
Object { | |||
"key": "component", | |||
} | |||
} | |||
component={ | |||
Object { | |||
"key": "component", | |||
} | |||
onSelect={[Function]} | |||
selected={true} | |||
/> | |||
<ComponentNavBranchesMenuItem | |||
branch={ | |||
Object { | |||
"isMain": false, | |||
"name": "foo", | |||
"status": Object { | |||
"bugs": 0, | |||
"codeSmells": 0, | |||
"vulnerabilities": 0, | |||
}, | |||
"type": "SHORT", | |||
} | |||
} | |||
onSelect={[Function]} | |||
selected={true} | |||
/> | |||
<li | |||
className="divider" | |||
/> | |||
<ComponentNavBranchesMenuItem | |||
branch={ | |||
Object { | |||
"isMain": false, | |||
"isOrphan": true, | |||
"mergeBranch": "master", | |||
"name": "baz", | |||
"status": Object { | |||
"bugs": 0, | |||
"codeSmells": 0, | |||
"vulnerabilities": 0, | |||
}, | |||
"type": "SHORT", | |||
} | |||
component={ | |||
Object { | |||
"key": "component", | |||
} | |||
} | |||
component={ | |||
Object { | |||
"key": "component", | |||
} | |||
onSelect={[Function]} | |||
selected={false} | |||
/> | |||
<ComponentNavBranchesMenuItem | |||
branch={ | |||
Object { | |||
"isMain": false, | |||
"name": "bar", | |||
"type": "LONG", | |||
} | |||
} | |||
onSelect={[Function]} | |||
selected={false} | |||
/> | |||
<ComponentNavBranchesMenuItem | |||
branch={ | |||
Object { | |||
"isMain": false, | |||
"isOrphan": undefined, | |||
"mergeBranch": "master", | |||
"name": "foo", | |||
"status": Object { | |||
"bugs": 0, | |||
"codeSmells": 0, | |||
"vulnerabilities": 0, | |||
}, | |||
"type": "SHORT", | |||
} | |||
component={ | |||
Object { | |||
"key": "component", | |||
} | |||
} | |||
component={ | |||
Object { | |||
"key": "component", | |||
} | |||
onSelect={[Function]} | |||
selected={false} | |||
/> | |||
</ul> | |||
</div> | |||
} | |||
onSelect={[Function]} | |||
selected={false} | |||
/> | |||
<li | |||
className="divider" | |||
/> | |||
<ComponentNavBranchesMenuItem | |||
branch={ | |||
Object { | |||
"isMain": false, | |||
"name": "bar", | |||
"type": "LONG", | |||
} | |||
} | |||
component={ | |||
Object { | |||
"key": "component", | |||
} | |||
} | |||
onSelect={[Function]} | |||
selected={false} | |||
/> | |||
<li | |||
className="divider" | |||
/> | |||
<ComponentNavBranchesMenuItem | |||
branch={ | |||
Object { | |||
"isMain": false, | |||
"isOrphan": true, | |||
"mergeBranch": "master", | |||
"name": "baz", | |||
"status": Object { | |||
"bugs": 0, | |||
"codeSmells": 0, | |||
"vulnerabilities": 0, | |||
}, | |||
"type": "SHORT", | |||
} | |||
} | |||
component={ | |||
Object { | |||
"key": "component", | |||
} | |||
} | |||
onSelect={[Function]} | |||
selected={false} | |||
/> | |||
</ul> | |||
</div> | |||
`; | |||
@@ -90,68 +144,71 @@ exports[`searches 1`] = ` | |||
<div | |||
className="dropdown-menu dropdown-menu-shadow" | |||
> | |||
<div> | |||
<div | |||
className="search-box menu-search" | |||
<div | |||
className="search-box menu-search" | |||
> | |||
<button | |||
className="search-box-submit button-clean" | |||
> | |||
<button | |||
className="search-box-submit button-clean" | |||
> | |||
<i | |||
className="icon-search-new" | |||
/> | |||
</button> | |||
<input | |||
autoFocus={true} | |||
className="search-box-input" | |||
onChange={[Function]} | |||
onKeyDown={[Function]} | |||
placeholder="search_verb" | |||
type="search" | |||
value="bar" | |||
<i | |||
className="icon-search-new" | |||
/> | |||
</div> | |||
<ul | |||
className="menu" | |||
> | |||
<ComponentNavBranchesMenuItem | |||
branch={ | |||
Object { | |||
"isMain": false, | |||
"name": "foobar", | |||
"status": Object { | |||
"bugs": 0, | |||
"codeSmells": 0, | |||
"vulnerabilities": 0, | |||
}, | |||
"type": "SHORT", | |||
} | |||
</button> | |||
<input | |||
autoFocus={true} | |||
className="search-box-input" | |||
onChange={[Function]} | |||
onKeyDown={[Function]} | |||
placeholder="search_verb" | |||
type="search" | |||
value="bar" | |||
/> | |||
</div> | |||
<ul | |||
className="menu" | |||
> | |||
<ComponentNavBranchesMenuItem | |||
branch={ | |||
Object { | |||
"isMain": false, | |||
"isOrphan": undefined, | |||
"mergeBranch": "master", | |||
"name": "foobar", | |||
"status": Object { | |||
"bugs": 0, | |||
"codeSmells": 0, | |||
"vulnerabilities": 0, | |||
}, | |||
"type": "SHORT", | |||
} | |||
component={ | |||
Object { | |||
"key": "component", | |||
} | |||
} | |||
component={ | |||
Object { | |||
"key": "component", | |||
} | |||
onSelect={[Function]} | |||
selected={true} | |||
/> | |||
<ComponentNavBranchesMenuItem | |||
branch={ | |||
Object { | |||
"isMain": false, | |||
"name": "bar", | |||
"type": "LONG", | |||
} | |||
} | |||
onSelect={[Function]} | |||
selected={true} | |||
/> | |||
<li | |||
className="divider" | |||
/> | |||
<ComponentNavBranchesMenuItem | |||
branch={ | |||
Object { | |||
"isMain": false, | |||
"name": "bar", | |||
"type": "LONG", | |||
} | |||
component={ | |||
Object { | |||
"key": "component", | |||
} | |||
} | |||
component={ | |||
Object { | |||
"key": "component", | |||
} | |||
onSelect={[Function]} | |||
selected={false} | |||
/> | |||
</ul> | |||
</div> | |||
} | |||
onSelect={[Function]} | |||
selected={false} | |||
/> | |||
</ul> | |||
</div> | |||
`; |
@@ -19,6 +19,12 @@ exports[`renders main branch 1`] = ` | |||
> | |||
<div> | |||
<BranchIcon | |||
branch={ | |||
Object { | |||
"isMain": true, | |||
"name": "master", | |||
} | |||
} | |||
className="little-spacer-right" | |||
/> | |||
master | |||
@@ -30,8 +36,7 @@ exports[`renders main branch 1`] = ` | |||
branch={ | |||
Object { | |||
"isMain": true, | |||
"name": undefined, | |||
"type": "LONG", | |||
"name": "master", | |||
} | |||
} | |||
concise={true} | |||
@@ -62,6 +67,19 @@ exports[`renders short-living branch 1`] = ` | |||
> | |||
<div> | |||
<BranchIcon | |||
branch={ | |||
Object { | |||
"isMain": false, | |||
"mergeBranch": "master", | |||
"name": "foo", | |||
"status": Object { | |||
"bugs": 1, | |||
"codeSmells": 2, | |||
"vulnerabilities": 3, | |||
}, | |||
"type": "SHORT", | |||
} | |||
} | |||
className="little-spacer-right big-spacer-left" | |||
/> | |||
foo | |||
@@ -73,6 +91,71 @@ exports[`renders short-living branch 1`] = ` | |||
branch={ | |||
Object { | |||
"isMain": false, | |||
"mergeBranch": "master", | |||
"name": "foo", | |||
"status": Object { | |||
"bugs": 1, | |||
"codeSmells": 2, | |||
"vulnerabilities": 3, | |||
}, | |||
"type": "SHORT", | |||
} | |||
} | |||
concise={true} | |||
/> | |||
</div> | |||
</Link> | |||
</li> | |||
`; | |||
exports[`renders short-living orhpan branch 1`] = ` | |||
<li | |||
onMouseEnter={[Function]} | |||
> | |||
<Link | |||
className="navbar-context-meta-branch-menu-item" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/project/issues", | |||
"query": Object { | |||
"branch": "foo", | |||
"id": "component", | |||
"resolved": "false", | |||
}, | |||
} | |||
} | |||
> | |||
<div> | |||
<BranchIcon | |||
branch={ | |||
Object { | |||
"isMain": false, | |||
"isOrphan": true, | |||
"mergeBranch": "master", | |||
"name": "foo", | |||
"status": Object { | |||
"bugs": 1, | |||
"codeSmells": 2, | |||
"vulnerabilities": 3, | |||
}, | |||
"type": "SHORT", | |||
} | |||
} | |||
className="little-spacer-right" | |||
/> | |||
foo | |||
</div> | |||
<div | |||
className="big-spacer-left note" | |||
> | |||
<BranchStatus | |||
branch={ | |||
Object { | |||
"isMain": false, | |||
"isOrphan": true, | |||
"mergeBranch": "master", | |||
"name": "foo", | |||
"status": Object { | |||
"bugs": 1, |
@@ -1,5 +1,143 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should work for long-living branches 1`] = ` | |||
<NavBarTabs> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": "release", | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
overview.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/project/issues", | |||
"query": Object { | |||
"branch": "release", | |||
"id": "foo", | |||
"resolved": "false", | |||
}, | |||
} | |||
} | |||
> | |||
issues.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/component_measures", | |||
"query": Object { | |||
"branch": "release", | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
layout.measures | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/code", | |||
"query": Object { | |||
"branch": "release", | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
code.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/project/activity", | |||
"query": Object { | |||
"branch": "release", | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
project_activity.page | |||
</Link> | |||
</li> | |||
</NavBarTabs> | |||
`; | |||
exports[`should work for short-living branches 1`] = ` | |||
<NavBarTabs> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/project/issues", | |||
"query": Object { | |||
"branch": "feature", | |||
"id": "foo", | |||
"resolved": "false", | |||
}, | |||
} | |||
} | |||
> | |||
issues.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/code", | |||
"query": Object { | |||
"branch": "feature", | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
code.page | |||
</Link> | |||
</li> | |||
</NavBarTabs> | |||
`; | |||
exports[`should work with extensions 1`] = ` | |||
<NavBarTabs> | |||
<li> | |||
@@ -11,6 +149,7 @@ exports[`should work with extensions 1`] = ` | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo", | |||
}, | |||
} | |||
@@ -47,6 +186,7 @@ exports[`should work with extensions 1`] = ` | |||
Object { | |||
"pathname": "/component_measures", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo", | |||
}, | |||
} | |||
@@ -82,6 +222,7 @@ exports[`should work with extensions 1`] = ` | |||
Object { | |||
"pathname": "/project/activity", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo", | |||
}, | |||
} | |||
@@ -212,6 +353,7 @@ exports[`should work with multiple extensions 1`] = ` | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo", | |||
}, | |||
} | |||
@@ -248,6 +390,7 @@ exports[`should work with multiple extensions 1`] = ` | |||
Object { | |||
"pathname": "/component_measures", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo", | |||
}, | |||
} | |||
@@ -283,6 +426,7 @@ exports[`should work with multiple extensions 1`] = ` | |||
Object { | |||
"pathname": "/project/activity", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo", | |||
}, | |||
} |
@@ -0,0 +1,41 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders nothing for long-living branch 1`] = ` | |||
<div | |||
className="navbar-context-meta" | |||
> | |||
<ul | |||
className="list-inline" | |||
/> | |||
</div> | |||
`; | |||
exports[`renders status of short-living branch 1`] = ` | |||
<div | |||
className="navbar-context-meta" | |||
> | |||
<ul | |||
className="list-inline" | |||
> | |||
<li | |||
className="navbar-context-meta-branch" | |||
> | |||
<BranchStatus | |||
branch={ | |||
Object { | |||
"isMain": false, | |||
"mergeBranch": "master", | |||
"name": "feature", | |||
"status": Object { | |||
"bugs": 0, | |||
"codeSmells": 2, | |||
"vulnerabilities": 3, | |||
}, | |||
"type": "SHORT", | |||
} | |||
} | |||
/> | |||
</li> | |||
</ul> | |||
</div> | |||
`; |
@@ -19,6 +19,7 @@ exports[`renders favorite 1`] = ` | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo", | |||
}, | |||
} | |||
@@ -65,6 +66,7 @@ exports[`renders match 1`] = ` | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo", | |||
}, | |||
} | |||
@@ -110,6 +112,7 @@ exports[`renders organizations 1`] = ` | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo", | |||
}, | |||
} | |||
@@ -160,6 +163,7 @@ exports[`renders organizations 2`] = ` | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo", | |||
}, | |||
} | |||
@@ -205,6 +209,7 @@ exports[`renders projects 1`] = ` | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "qwe", | |||
}, | |||
} | |||
@@ -255,6 +260,7 @@ exports[`renders recently browsed 1`] = ` | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo", | |||
}, | |||
} | |||
@@ -300,6 +306,7 @@ exports[`renders selected 1`] = ` | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo", | |||
}, | |||
} | |||
@@ -344,6 +351,7 @@ exports[`renders selected 2`] = ` | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo", | |||
}, | |||
} |
@@ -22,6 +22,7 @@ exports[`should match snapshot 1`] = ` | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo", | |||
}, | |||
} |
@@ -17,35 +17,54 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
let bucket = {}; | |||
let childrenBucket = {}; | |||
let breadcrumbsBucket = {}; | |||
import { Breadcrumb, Component } from './types'; | |||
export function addComponent(component) { | |||
let bucket: { [key: string]: Component } = {}; | |||
let childrenBucket: { | |||
[key: string]: { | |||
children: Component[]; | |||
page: number; | |||
total: number; | |||
}; | |||
} = {}; | |||
let breadcrumbsBucket: { [key: string]: Breadcrumb[] } = {}; | |||
export function addComponent(component: Component): void { | |||
bucket[component.key] = component; | |||
} | |||
export function getComponent(componentKey) { | |||
export function getComponent(componentKey: string): Component { | |||
return bucket[componentKey]; | |||
} | |||
export function addComponentChildren(componentKey, children, total, page) { | |||
export function addComponentChildren( | |||
componentKey: string, | |||
children: Component[], | |||
total: number, | |||
page: number | |||
): void { | |||
childrenBucket[componentKey] = { children, total, page }; | |||
} | |||
export function getComponentChildren(componentKey) { | |||
export function getComponentChildren( | |||
componentKey: string | |||
): { | |||
children: Component[]; | |||
page: number; | |||
total: number; | |||
} { | |||
return childrenBucket[componentKey]; | |||
} | |||
export function addComponentBreadcrumbs(componentKey, breadcrumbs) { | |||
export function addComponentBreadcrumbs(componentKey: string, breadcrumbs: Breadcrumb[]): void { | |||
breadcrumbsBucket[componentKey] = breadcrumbs; | |||
} | |||
export function getComponentBreadcrumbs(componentKey) { | |||
export function getComponentBreadcrumbs(componentKey: string): Breadcrumb[] { | |||
return breadcrumbsBucket[componentKey]; | |||
} | |||
export function clearBucket() { | |||
export function clearBucket(): void { | |||
bucket = {}; | |||
childrenBucket = {}; | |||
breadcrumbsBucket = {}; |
@@ -17,11 +17,12 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import classNames from 'classnames'; | |||
import React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import Helmet from 'react-helmet'; | |||
import Components from './Components'; | |||
import Breadcrumbs from './Breadcrumbs'; | |||
import { Component as CodeComponent } from '../types'; | |||
import SourceViewer from './../../../components/SourceViewer/SourceViewer'; | |||
import Search from './Search'; | |||
import ListFooter from '../../../components/controls/ListFooter'; | |||
@@ -32,19 +33,36 @@ import { | |||
parseError | |||
} from '../utils'; | |||
import { addComponent, addComponentBreadcrumbs, clearBucket } from '../bucket'; | |||
import { getBranchName } from '../../../helpers/branches'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import '../code.css'; | |||
import { Component, Branch } from '../../../app/types'; | |||
export default class App extends React.PureComponent { | |||
state = { | |||
interface Props { | |||
branch: Branch; | |||
component: Component; | |||
location: { query: { [x: string]: string } }; | |||
} | |||
interface State { | |||
baseComponent?: CodeComponent; | |||
breadcrumbs: Array<CodeComponent>; | |||
components?: Array<CodeComponent>; | |||
error?: string; | |||
loading: boolean; | |||
page: number; | |||
searchResults?: Array<CodeComponent>; | |||
sourceViewer?: CodeComponent; | |||
total: number; | |||
} | |||
export default class App extends React.PureComponent<Props, State> { | |||
mounted: boolean; | |||
state: State = { | |||
loading: true, | |||
baseComponent: null, | |||
components: null, | |||
breadcrumbs: [], | |||
total: 0, | |||
page: 0, | |||
sourceViewer: null, | |||
error: null | |||
page: 0 | |||
}; | |||
componentDidMount() { | |||
@@ -52,8 +70,8 @@ export default class App extends React.PureComponent { | |||
this.handleComponentChange(); | |||
} | |||
componentDidUpdate(prevProps) { | |||
if (prevProps.component !== this.props.component) { | |||
componentDidUpdate(prevProps: Props) { | |||
if (prevProps.component !== this.props.component || prevProps.branch !== this.props.branch) { | |||
this.handleComponentChange(); | |||
} else if (prevProps.location !== this.props.location) { | |||
this.handleUpdate(); | |||
@@ -66,31 +84,31 @@ export default class App extends React.PureComponent { | |||
} | |||
handleComponentChange() { | |||
const { component } = this.props; | |||
const { branch, component } = this.props; | |||
// we already know component's breadcrumbs, | |||
addComponentBreadcrumbs(component.key, component.breadcrumbs); | |||
this.setState({ loading: true }); | |||
const isPortfolio = ['VW', 'SVW'].includes(component.qualifier); | |||
retrieveComponentChildren(component.key, isPortfolio, component.branch) | |||
.then(r => { | |||
addComponent(r.baseComponent); | |||
retrieveComponentChildren(component.key, isPortfolio, getBranchName(branch)) | |||
.then(() => { | |||
addComponent(component); | |||
this.handleUpdate(); | |||
}) | |||
.catch(e => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
parseError(e).then(this.handleError.bind(this)); | |||
parseError(e).then(this.handleError); | |||
} | |||
}); | |||
} | |||
loadComponent(componentKey) { | |||
loadComponent(componentKey: string) { | |||
this.setState({ loading: true }); | |||
const isPortfolio = ['VW', 'SVW'].includes(this.props.component.qualifier); | |||
retrieveComponent(componentKey, isPortfolio, this.props.component.branch) | |||
retrieveComponent(componentKey, isPortfolio, getBranchName(this.props.branch)) | |||
.then(r => { | |||
if (this.mounted) { | |||
if (['FIL', 'UTS'].includes(r.component.qualifier)) { | |||
@@ -98,7 +116,7 @@ export default class App extends React.PureComponent { | |||
loading: false, | |||
sourceViewer: r.component, | |||
breadcrumbs: r.breadcrumbs, | |||
searchResults: null | |||
searchResults: undefined | |||
}); | |||
} else { | |||
this.setState({ | |||
@@ -108,8 +126,8 @@ export default class App extends React.PureComponent { | |||
breadcrumbs: r.breadcrumbs, | |||
total: r.total, | |||
page: r.page, | |||
sourceViewer: null, | |||
searchResults: null | |||
sourceViewer: undefined, | |||
searchResults: undefined | |||
}); | |||
} | |||
} | |||
@@ -117,7 +135,7 @@ export default class App extends React.PureComponent { | |||
.catch(e => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
parseError(e).then(this.handleError.bind(this)); | |||
parseError(e).then(this.handleError); | |||
} | |||
}); | |||
} | |||
@@ -131,13 +149,16 @@ export default class App extends React.PureComponent { | |||
} | |||
handleLoadMore = () => { | |||
const { baseComponent, page } = this.state; | |||
const { baseComponent, components, page } = this.state; | |||
if (!baseComponent || !components) { | |||
return; | |||
} | |||
const isPortfolio = ['VW', 'SVW'].includes(this.props.component.qualifier); | |||
loadMoreChildren(baseComponent.key, page + 1, isPortfolio, this.props.component.branch) | |||
loadMoreChildren(baseComponent.key, page + 1, isPortfolio, getBranchName(this.props.branch)) | |||
.then(r => { | |||
if (this.mounted) { | |||
this.setState({ | |||
components: [...this.state.components, ...r.components], | |||
components: [...components, ...r.components], | |||
page: r.page, | |||
total: r.total | |||
}); | |||
@@ -151,14 +172,14 @@ export default class App extends React.PureComponent { | |||
}); | |||
}; | |||
handleError = error => { | |||
handleError = (error: string) => { | |||
if (this.mounted) { | |||
this.setState({ error }); | |||
} | |||
}; | |||
render() { | |||
const { component, location } = this.props; | |||
const { branch, component, location } = this.props; | |||
const { | |||
loading, | |||
error, | |||
@@ -168,10 +189,9 @@ export default class App extends React.PureComponent { | |||
total, | |||
sourceViewer | |||
} = this.state; | |||
const branchName = getBranchName(branch); | |||
const shouldShowSourceViewer = !!sourceViewer; | |||
const shouldShowComponents = !shouldShowSourceViewer && components; | |||
const shouldShowBreadcrumbs = Array.isArray(breadcrumbs) && breadcrumbs.length > 1; | |||
const shouldShowBreadcrumbs = breadcrumbs.length > 1; | |||
const componentsClassName = classNames('spacer-top', { 'new-loading': loading }); | |||
@@ -184,27 +204,35 @@ export default class App extends React.PureComponent { | |||
{error} | |||
</div>} | |||
<Search location={location} component={component} onError={this.handleError} /> | |||
<Search | |||
branch={branchName} | |||
component={component} | |||
location={location} | |||
onError={this.handleError} | |||
/> | |||
<div className="code-components"> | |||
{shouldShowBreadcrumbs && | |||
<Breadcrumbs rootComponent={component} breadcrumbs={breadcrumbs} />} | |||
<Breadcrumbs branch={branchName} breadcrumbs={breadcrumbs} rootComponent={component} />} | |||
{shouldShowComponents && | |||
{sourceViewer == undefined && | |||
components != undefined && | |||
<div className={componentsClassName}> | |||
<Components | |||
rootComponent={component} | |||
baseComponent={baseComponent} | |||
branch={branchName} | |||
components={components} | |||
rootComponent={component} | |||
/> | |||
</div>} | |||
{shouldShowComponents && | |||
{sourceViewer == undefined && | |||
components != undefined && | |||
<ListFooter count={components.length} total={total} loadMore={this.handleLoadMore} />} | |||
{shouldShowSourceViewer && | |||
{sourceViewer != undefined && | |||
<div className="spacer-top"> | |||
<SourceViewer branch={component.branch} component={sourceViewer.key} /> | |||
<SourceViewer branch={branchName} component={sourceViewer.key} /> | |||
</div>} | |||
</div> | |||
</div> |
@@ -17,18 +17,26 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import Breadcrumb from './Breadcrumb'; | |||
import * as React from 'react'; | |||
import ComponentName from './ComponentName'; | |||
import { Component } from '../types'; | |||
export default function Breadcrumbs({ rootComponent, breadcrumbs }) { | |||
interface Props { | |||
branch?: string; | |||
breadcrumbs: Component[]; | |||
rootComponent: Component; | |||
} | |||
export default function Breadcrumbs({ branch, breadcrumbs, rootComponent }: Props) { | |||
return ( | |||
<ul className="code-breadcrumbs"> | |||
{breadcrumbs.map((component, index) => | |||
<li key={component.key}> | |||
<Breadcrumb | |||
rootComponent={rootComponent} | |||
component={component} | |||
<ComponentName | |||
branch={branch} | |||
canBrowse={index < breadcrumbs.length - 1} | |||
component={component} | |||
rootComponent={rootComponent} | |||
/> | |||
</li> | |||
)} |
@@ -17,18 +17,29 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import classNames from 'classnames'; | |||
import React from 'react'; | |||
import ReactDOM from 'react-dom'; | |||
import * as classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import ComponentName from './ComponentName'; | |||
import ComponentMeasure from './ComponentMeasure'; | |||
import ComponentDetach from './ComponentDetach'; | |||
import ComponentPin from './ComponentPin'; | |||
import { Component as IComponent } from '../types'; | |||
const TOP_OFFSET = 200; | |||
const BOTTOM_OFFSET = 10; | |||
export default class Component extends React.PureComponent { | |||
interface Props { | |||
branch?: string; | |||
canBrowse?: boolean; | |||
component: IComponent; | |||
previous?: IComponent; | |||
rootComponent: IComponent; | |||
selected?: boolean; | |||
} | |||
export default class Component extends React.PureComponent<Props> { | |||
node: HTMLElement; | |||
componentDidMount() { | |||
this.handleUpdate(); | |||
} | |||
@@ -49,8 +60,7 @@ export default class Component extends React.PureComponent { | |||
} | |||
handleScroll() { | |||
const node = ReactDOM.findDOMNode(this); | |||
const position = node.getBoundingClientRect(); | |||
const position = this.node.getBoundingClientRect(); | |||
const { top, bottom } = position; | |||
if (bottom > window.innerHeight - BOTTOM_OFFSET) { | |||
window.scrollTo(0, bottom - window.innerHeight + window.scrollY + BOTTOM_OFFSET); | |||
@@ -60,7 +70,14 @@ export default class Component extends React.PureComponent { | |||
} | |||
render() { | |||
const { component, rootComponent, selected, previous, canBrowse } = this.props; | |||
const { | |||
branch, | |||
component, | |||
rootComponent, | |||
selected = false, | |||
previous, | |||
canBrowse = false | |||
} = this.props; | |||
const isPortfolio = ['VW', 'SVW'].includes(rootComponent.qualifier); | |||
const isApplication = rootComponent.qualifier === 'APP'; | |||
@@ -70,10 +87,10 @@ export default class Component extends React.PureComponent { | |||
switch (component.qualifier) { | |||
case 'FIL': | |||
case 'UTS': | |||
componentAction = <ComponentPin branch={rootComponent.branch} component={component} />; | |||
componentAction = <ComponentPin branch={branch} component={component} />; | |||
break; | |||
default: | |||
componentAction = <ComponentDetach branch={rootComponent.branch} component={component} />; | |||
componentAction = <ComponentDetach branch={branch} component={component} />; | |||
} | |||
} | |||
@@ -93,10 +110,10 @@ export default class Component extends React.PureComponent { | |||
{ metric: 'code_smells', type: 'SHORT_INT' }, | |||
{ metric: 'coverage', type: 'PERCENT' }, | |||
{ metric: 'duplicated_lines_density', type: 'PERCENT' } | |||
].filter(Boolean); | |||
].filter(Boolean) as Array<{ metric: string; type: string }>; | |||
return ( | |||
<tr className={classNames({ selected })}> | |||
<tr className={classNames({ selected })} ref={node => (this.node = node as HTMLElement)}> | |||
<td className="thin nowrap"> | |||
<span className="spacer-right"> | |||
{componentAction} | |||
@@ -104,6 +121,7 @@ export default class Component extends React.PureComponent { | |||
</td> | |||
<td className="code-name-cell"> | |||
<ComponentName | |||
branch={branch} | |||
component={component} | |||
rootComponent={rootComponent} | |||
previous={previous} |
@@ -17,11 +17,17 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { Component } from '../types'; | |||
export default function ComponentDetach({ component, branch }) { | |||
interface Props { | |||
branch?: string; | |||
component: Component; | |||
} | |||
export default function ComponentDetach({ component, branch }: Props) { | |||
return ( | |||
<Link | |||
to={{ pathname: '/dashboard', query: { branch, id: component.refKey || component.key } }} |
@@ -17,10 +17,17 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import * as React from 'react'; | |||
import Measure from '../../../components/measure/Measure'; | |||
import { Component } from '../types'; | |||
const ComponentMeasure = ({ component, metricKey, metricType }) => { | |||
interface Props { | |||
component: Component; | |||
metricKey: string; | |||
metricType: string; | |||
} | |||
export default function ComponentMeasure({ component, metricKey, metricType }: Props) { | |||
const isProject = component.qualifier === 'TRK'; | |||
const isReleasability = metricKey === 'releasability_rating'; | |||
@@ -35,9 +42,10 @@ const ComponentMeasure = ({ component, metricKey, metricType }) => { | |||
return <span />; | |||
} | |||
// TODO | |||
const AnyMeasure = Measure as any; | |||
return ( | |||
<Measure measure={{ ...measure, metric: { key: finalMetricKey, type: finalMetricType } }} /> | |||
<AnyMeasure measure={{ ...measure, metric: { key: finalMetricKey, type: finalMetricType } }} /> | |||
); | |||
}; | |||
export default ComponentMeasure; | |||
} |
@@ -17,12 +17,13 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
import Truncated from './Truncated'; | |||
import QualifierIcon from '../../../components/shared/QualifierIcon'; | |||
import { Component } from '../types'; | |||
function getTooltip(component) { | |||
function getTooltip(component: Component) { | |||
const isFile = component.qualifier === 'FIL' || component.qualifier === 'UTS'; | |||
if (isFile && component.path) { | |||
return component.path + '\n\n' + component.key; | |||
@@ -31,7 +32,7 @@ function getTooltip(component) { | |||
} | |||
} | |||
function mostCommitPrefix(strings) { | |||
function mostCommitPrefix(strings: string[]) { | |||
const sortedStrings = strings.slice(0).sort(); | |||
const firstString = sortedStrings[0]; | |||
const firstStringLength = firstString.length; | |||
@@ -46,9 +47,21 @@ function mostCommitPrefix(strings) { | |||
return prefix.substr(0, prefix.length - lastPrefixPart.length); | |||
} | |||
const ComponentName = ({ component, rootComponent, previous, canBrowse }) => { | |||
interface Props { | |||
branch?: string; | |||
canBrowse?: boolean; | |||
component: Component; | |||
previous?: Component; | |||
rootComponent: Component; | |||
} | |||
export default function ComponentName(props: Props) { | |||
const { branch, component, rootComponent, previous, canBrowse = false } = props; | |||
const areBothDirs = component.qualifier === 'DIR' && previous && previous.qualifier === 'DIR'; | |||
const prefix = areBothDirs ? mostCommitPrefix([component.name + '/', previous.name + '/']) : ''; | |||
const prefix = | |||
areBothDirs && previous != undefined | |||
? mostCommitPrefix([component.name + '/', previous.name + '/']) | |||
: ''; | |||
const name = prefix | |||
? <span> | |||
<span style={{ color: '#777' }}> | |||
@@ -71,7 +84,7 @@ const ComponentName = ({ component, rootComponent, previous, canBrowse }) => { | |||
</Link> | |||
); | |||
} else if (canBrowse) { | |||
const query = { id: rootComponent.key, branch: rootComponent.branch }; | |||
const query = { id: rootComponent.key, branch }; | |||
if (component.key !== rootComponent.key) { | |||
Object.assign(query, { selected: component.key }); | |||
} | |||
@@ -93,6 +106,4 @@ const ComponentName = ({ component, rootComponent, previous, canBrowse }) => { | |||
{inner} | |||
</Truncated> | |||
); | |||
}; | |||
export default ComponentName; | |||
} |
@@ -17,14 +17,20 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import * as React from 'react'; | |||
import Workspace from '../../../components/workspace/main'; | |||
import PinIcon from '../../../components/shared/pin-icon'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { Component } from '../types'; | |||
const ComponentPin = ({ branch, component }) => { | |||
const handleClick = e => { | |||
e.preventDefault(); | |||
interface Props { | |||
branch?: string; | |||
component: Component; | |||
} | |||
export default function ComponentPin({ branch, component }: Props) { | |||
const handleClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
Workspace.openComponent({ branch, key: component.key }); | |||
}; | |||
@@ -37,6 +43,4 @@ const ComponentPin = ({ branch, component }) => { | |||
<PinIcon /> | |||
</a> | |||
); | |||
}; | |||
export default ComponentPin; | |||
} |
@@ -17,36 +17,48 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import * as React from 'react'; | |||
import Component from './Component'; | |||
import ComponentsEmpty from './ComponentsEmpty'; | |||
import ComponentsHeader from './ComponentsHeader'; | |||
import { Component as IComponent } from '../types'; | |||
export default function Components({ rootComponent, baseComponent, components, selected }) { | |||
interface Props { | |||
baseComponent?: IComponent; | |||
branch?: string; | |||
components: IComponent[]; | |||
rootComponent: IComponent; | |||
selected?: IComponent; | |||
} | |||
export default function Components(props: Props) { | |||
const { baseComponent, branch, components, rootComponent, selected } = props; | |||
return ( | |||
<table className="data zebra"> | |||
<ComponentsHeader baseComponent={baseComponent} rootComponent={rootComponent} /> | |||
{baseComponent && | |||
<tbody> | |||
<Component | |||
branch={branch} | |||
component={baseComponent} | |||
key={baseComponent.key} | |||
rootComponent={rootComponent} | |||
component={baseComponent} | |||
/> | |||
<tr className="blank"> | |||
<td colSpan="8"> </td> | |||
<td colSpan={8}> </td> | |||
</tr> | |||
</tbody>} | |||
<tbody> | |||
{components.length | |||
? components.map((component, index, list) => | |||
<Component | |||
branch={branch} | |||
canBrowse={true} | |||
component={component} | |||
key={component.key} | |||
previous={index > 0 ? list[index - 1] : undefined} | |||
rootComponent={rootComponent} | |||
component={component} | |||
selected={component === selected} | |||
previous={index > 0 ? list[index - 1] : null} | |||
canBrowse={true} | |||
/> | |||
) | |||
: <ComponentsEmpty />} |
@@ -17,16 +17,16 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import * as React from 'react'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export default function ComponentsEmpty() { | |||
return ( | |||
<tr> | |||
<td colSpan="2"> | |||
<td colSpan={2}> | |||
{translate('no_results')} | |||
</td> | |||
<td colSpan="6" /> | |||
<td colSpan={6} /> | |||
</tr> | |||
); | |||
} |
@@ -17,10 +17,16 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import * as React from 'react'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { Component } from '../types'; | |||
const ComponentsHeader = ({ baseComponent, rootComponent }) => { | |||
interface Props { | |||
baseComponent?: Component; | |||
rootComponent: Component; | |||
} | |||
export default function ComponentsHeader({ baseComponent, rootComponent }: Props) { | |||
const isPortfolio = rootComponent.qualifier === 'VW' || rootComponent.qualifier === 'SVW'; | |||
const isApplication = rootComponent.qualifier === 'APP'; | |||
@@ -40,7 +46,7 @@ const ComponentsHeader = ({ baseComponent, rootComponent }) => { | |||
translate('metric', 'code_smells', 'name'), | |||
translate('metric', 'coverage', 'name'), | |||
translate('metric', 'duplicated_lines_density', 'short_name') | |||
].filter(Boolean); | |||
].filter(Boolean) as string[]; | |||
return ( | |||
<thead> | |||
@@ -55,6 +61,4 @@ const ComponentsHeader = ({ baseComponent, rootComponent }) => { | |||
</tr> | |||
</thead> | |||
); | |||
}; | |||
export default ComponentsHeader; | |||
} |
@@ -17,32 +17,42 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import PropTypes from 'prop-types'; | |||
import classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import * as PropTypes from 'prop-types'; | |||
import * as classNames from 'classnames'; | |||
import { debounce } from 'lodash'; | |||
import Components from './Components'; | |||
import { getTree } from '../../../api/components'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { parseError } from '../utils'; | |||
import { getComponentUrl } from '../../../helpers/urls'; | |||
import { getProjectUrl } from '../../../helpers/urls'; | |||
import { Component } from '../types'; | |||
interface Props { | |||
branch?: string; | |||
component: Component; | |||
location: {}; | |||
onError: (error: string) => void; | |||
} | |||
interface State { | |||
query: string; | |||
loading: boolean; | |||
results?: Component[]; | |||
selectedIndex?: number; | |||
} | |||
export default class Search extends React.PureComponent<Props, State> { | |||
input: HTMLInputElement; | |||
mounted: boolean; | |||
export default class Search extends React.PureComponent { | |||
static contextTypes = { | |||
router: PropTypes.object.isRequired | |||
}; | |||
static propTypes = { | |||
component: PropTypes.object.isRequired, | |||
location: PropTypes.object.isRequired, | |||
onError: PropTypes.func.isRequired | |||
}; | |||
state = { | |||
state: State = { | |||
query: '', | |||
loading: false, | |||
results: null, | |||
selectedIndex: null | |||
loading: false | |||
}; | |||
componentWillMount() { | |||
@@ -51,17 +61,17 @@ export default class Search extends React.PureComponent { | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.refs.input.focus(); | |||
this.input.focus(); | |||
} | |||
componentWillReceiveProps(nextProps) { | |||
componentWillReceiveProps(nextProps: Props) { | |||
// if the url has change, reset the current state | |||
if (nextProps.location !== this.props.location) { | |||
this.setState({ | |||
query: '', | |||
loading: false, | |||
results: null, | |||
selectedIndex: null | |||
results: undefined, | |||
selectedIndex: undefined | |||
}); | |||
} | |||
} | |||
@@ -70,8 +80,8 @@ export default class Search extends React.PureComponent { | |||
this.mounted = false; | |||
} | |||
checkInputValue(query) { | |||
return this.refs.input.value === query; | |||
checkInputValue(query: string) { | |||
return this.input.value === query; | |||
} | |||
handleSelectNext() { | |||
@@ -89,27 +99,23 @@ export default class Search extends React.PureComponent { | |||
} | |||
handleSelectCurrent() { | |||
const { component } = this.props; | |||
const { branch, component } = this.props; | |||
const { results, selectedIndex } = this.state; | |||
if (results != null && selectedIndex != null) { | |||
const selected = results[selectedIndex]; | |||
if (selected.refKey) { | |||
window.location = getComponentUrl(selected.refKey); | |||
this.context.router.push(getProjectUrl(selected.refKey)); | |||
} else { | |||
this.context.router.push({ | |||
pathname: '/code', | |||
query: { | |||
branch: component.branch, | |||
id: component.key, | |||
selected: selected.key | |||
} | |||
query: { branch, id: component.key, selected: selected.key } | |||
}); | |||
} | |||
} | |||
} | |||
handleKeyDown(e) { | |||
handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) { | |||
switch (e.keyCode) { | |||
case 13: | |||
e.preventDefault(); | |||
@@ -127,27 +133,22 @@ export default class Search extends React.PureComponent { | |||
} | |||
} | |||
handleSearch = query => { | |||
handleSearch = (query: string) => { | |||
// first time check if value has changed due to debounce | |||
if (this.mounted && this.checkInputValue(query)) { | |||
const { component, onError } = this.props; | |||
const { branch, component, onError } = this.props; | |||
this.setState({ loading: true }); | |||
const isPortfolio = ['VW', 'SVW', 'APP'].includes(component.qualifier); | |||
const qualifiers = isPortfolio ? 'SVW,TRK' : 'BRC,UTS,FIL'; | |||
getTree(component.key, { | |||
branch: component.branch, | |||
q: query, | |||
s: 'qualifier,name', | |||
qualifiers | |||
}) | |||
getTree(component.key, { branch, q: query, s: 'qualifier,name', qualifiers }) | |||
.then(r => { | |||
// second time check if value has change due to api request | |||
if (this.mounted && this.checkInputValue(query)) { | |||
this.setState({ | |||
results: r.components, | |||
selectedIndex: r.components.length > 0 ? 0 : null, | |||
selectedIndex: r.components.length > 0 ? 0 : undefined, | |||
loading: false | |||
}); | |||
} | |||
@@ -162,30 +163,30 @@ export default class Search extends React.PureComponent { | |||
} | |||
}; | |||
handleQueryChange(query) { | |||
handleQueryChange(query: string) { | |||
this.setState({ query }); | |||
if (query.length < 3) { | |||
this.setState({ results: null }); | |||
this.setState({ results: undefined }); | |||
} else { | |||
this.handleSearch(query); | |||
} | |||
} | |||
handleInputChange(e) { | |||
const query = e.target.value; | |||
handleInputChange(event: React.SyntheticEvent<HTMLInputElement>) { | |||
const query = event.currentTarget.value; | |||
this.handleQueryChange(query); | |||
} | |||
handleSubmit(e) { | |||
e.preventDefault(); | |||
const query = this.refs.input.value; | |||
handleSubmit(event: React.SyntheticEvent<HTMLFormElement>) { | |||
event.preventDefault(); | |||
const query = this.input.value; | |||
this.handleQueryChange(query); | |||
} | |||
render() { | |||
const { component } = this.props; | |||
const { query, loading, selectedIndex, results } = this.state; | |||
const selected = selectedIndex != null && results != null ? results[selectedIndex] : null; | |||
const selected = selectedIndex != null && results != null ? results[selectedIndex] : undefined; | |||
const containerClassName = classNames('code-search', { | |||
'code-search-with-results': results != null | |||
}); | |||
@@ -201,7 +202,7 @@ export default class Search extends React.PureComponent { | |||
</button> | |||
<input | |||
ref="input" | |||
ref={node => (this.input = node as HTMLInputElement)} | |||
onKeyDown={this.handleKeyDown.bind(this)} | |||
onChange={this.handleInputChange.bind(this)} | |||
value={query} | |||
@@ -209,7 +210,7 @@ export default class Search extends React.PureComponent { | |||
type="search" | |||
name="q" | |||
placeholder={translate('search_verb')} | |||
maxLength="100" | |||
maxLength={100} | |||
autoComplete="off" | |||
/> | |||
@@ -221,7 +222,12 @@ export default class Search extends React.PureComponent { | |||
</form> | |||
{results != null && | |||
<Components rootComponent={component} components={results} selected={selected} />} | |||
<Components | |||
branch={this.props.branch} | |||
components={results} | |||
rootComponent={component} | |||
selected={selected} | |||
/>} | |||
</div> | |||
); | |||
} |
@@ -17,9 +17,14 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import * as React from 'react'; | |||
export default function Truncated({ children, title }) { | |||
interface Props { | |||
children: React.ReactNode; | |||
title: string; | |||
} | |||
export default function Truncated({ children, title }: Props) { | |||
return ( | |||
<span className="code-truncated" title={title}> | |||
{children} |
@@ -1,7 +1,7 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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 | |||
@@ -17,11 +17,25 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import ComponentName from './ComponentName'; | |||
interface Measure { | |||
metric: string; | |||
value: string; | |||
periods?: Period[]; | |||
} | |||
interface Period { | |||
index: number; | |||
value: string; | |||
} | |||
export interface Component extends Breadcrumb { | |||
measures?: Measure[]; | |||
path?: string; | |||
refKey?: string; | |||
} | |||
export default function Breadcrumb({ rootComponent, component, canBrowse }) { | |||
return ( | |||
<ComponentName rootComponent={rootComponent} component={component} canBrowse={canBrowse} /> | |||
); | |||
export interface Breadcrumb { | |||
key: string; | |||
name: string; | |||
qualifier: string; | |||
} |
@@ -26,6 +26,7 @@ import { | |||
addComponentBreadcrumbs, | |||
getComponentBreadcrumbs | |||
} from './bucket'; | |||
import { Breadcrumb, Component } from './types'; | |||
import { getChildren, getComponent, getBreadcrumbs } from '../../api/components'; | |||
import { translate } from '../../helpers/l10n'; | |||
@@ -50,7 +51,11 @@ const PORTFOLIO_METRICS = [ | |||
const PAGE_SIZE = 100; | |||
function requestChildren(componentKey, metrics, page) { | |||
function requestChildren( | |||
componentKey: string, | |||
metrics: string[], | |||
page: number | |||
): Promise<Component[]> { | |||
return getChildren(componentKey, metrics, { p: page, ps: PAGE_SIZE }).then(r => { | |||
if (r.paging.total > r.paging.pageSize * r.paging.pageIndex) { | |||
return requestChildren(componentKey, metrics, page + 1).then(moreComponents => { | |||
@@ -61,14 +66,24 @@ function requestChildren(componentKey, metrics, page) { | |||
}); | |||
} | |||
function requestAllChildren(componentKey, metrics) { | |||
function requestAllChildren(componentKey: string, metrics: string[]): Promise<Component[]> { | |||
return requestChildren(componentKey, metrics, 1); | |||
} | |||
function expandRootDir(metrics) { | |||
interface Children { | |||
components: Component[]; | |||
page: number; | |||
total: number; | |||
} | |||
interface ExpandRootDirFunc { | |||
(children: Children): Promise<Children>; | |||
} | |||
function expandRootDir(metrics: string[]): ExpandRootDirFunc { | |||
return function({ components, total, ...other }) { | |||
const rootDir = components.find( | |||
component => component.qualifier === 'DIR' && component.name === '/' | |||
(component: Component) => component.qualifier === 'DIR' && component.name === '/' | |||
); | |||
if (rootDir) { | |||
return requestAllChildren(rootDir.key, metrics).then(rootDirComponents => { | |||
@@ -77,31 +92,30 @@ function expandRootDir(metrics) { | |||
return { components: nextComponents, total: nextTotal, ...other }; | |||
}); | |||
} else { | |||
return { components, total, ...other }; | |||
return Promise.resolve({ components, total, ...other }); | |||
} | |||
}; | |||
} | |||
function prepareChildren(r) { | |||
function prepareChildren(r: any): Children { | |||
return { | |||
components: r.components, | |||
total: r.paging.total, | |||
page: r.paging.pageIndex, | |||
baseComponent: r.baseComponent | |||
page: r.paging.pageIndex | |||
}; | |||
} | |||
function skipRootDir(breadcrumbs) { | |||
function skipRootDir(breadcrumbs: Component[]) { | |||
return breadcrumbs.filter(component => { | |||
return !(component.qualifier === 'DIR' && component.name === '/'); | |||
}); | |||
} | |||
function storeChildrenBase(children) { | |||
function storeChildrenBase(children: Component[]) { | |||
children.forEach(addComponent); | |||
} | |||
function storeChildrenBreadcrumbs(parentComponentKey, children) { | |||
function storeChildrenBreadcrumbs(parentComponentKey: string, children: Breadcrumb[]) { | |||
const parentBreadcrumbs = getComponentBreadcrumbs(parentComponentKey); | |||
if (parentBreadcrumbs) { | |||
children.forEach(child => { | |||
@@ -111,16 +125,11 @@ function storeChildrenBreadcrumbs(parentComponentKey, children) { | |||
} | |||
} | |||
function getMetrics(isPortfolio) { | |||
function getMetrics(isPortfolio: boolean) { | |||
return isPortfolio ? PORTFOLIO_METRICS : METRICS; | |||
} | |||
/** | |||
* @param {string} componentKey | |||
* @param {boolean} isPortfolio | |||
* @returns {Promise} | |||
*/ | |||
function retrieveComponentBase(componentKey, isPortfolio, branch) { | |||
function retrieveComponentBase(componentKey: string, isPortfolio: boolean, branch?: string) { | |||
const existing = getComponentFromBucket(componentKey); | |||
if (existing) { | |||
return Promise.resolve(existing); | |||
@@ -134,12 +143,11 @@ function retrieveComponentBase(componentKey, isPortfolio, branch) { | |||
}); | |||
} | |||
/** | |||
* @param {string} componentKey | |||
* @param {boolean} isPortfolio | |||
* @returns {Promise} | |||
*/ | |||
export function retrieveComponentChildren(componentKey, isPortfolio, branch) { | |||
export function retrieveComponentChildren( | |||
componentKey: string, | |||
isPortfolio: boolean, | |||
branch?: string | |||
): Promise<{ components: Component[]; page: number; total: number }> { | |||
const existing = getComponentChildren(componentKey); | |||
if (existing) { | |||
return Promise.resolve({ | |||
@@ -162,7 +170,10 @@ export function retrieveComponentChildren(componentKey, isPortfolio, branch) { | |||
}); | |||
} | |||
function retrieveComponentBreadcrumbs(componentKey, branch) { | |||
function retrieveComponentBreadcrumbs( | |||
componentKey: string, | |||
branch?: string | |||
): Promise<Breadcrumb[]> { | |||
const existing = getComponentBreadcrumbs(componentKey); | |||
if (existing) { | |||
return Promise.resolve(existing); | |||
@@ -174,12 +185,17 @@ function retrieveComponentBreadcrumbs(componentKey, branch) { | |||
}); | |||
} | |||
/** | |||
* @param {string} componentKey | |||
* @param {boolean} isPortfolio | |||
* @returns {Promise} | |||
*/ | |||
export function retrieveComponent(componentKey, isPortfolio, branch) { | |||
export function retrieveComponent( | |||
componentKey: string, | |||
isPortfolio: boolean, | |||
branch?: string | |||
): Promise<{ | |||
breadcrumbs: Component[]; | |||
component: Component; | |||
components: Component[]; | |||
page: number; | |||
total: number; | |||
}> { | |||
return Promise.all([ | |||
retrieveComponentBase(componentKey, isPortfolio, branch), | |||
retrieveComponentChildren(componentKey, isPortfolio, branch), | |||
@@ -195,7 +211,12 @@ export function retrieveComponent(componentKey, isPortfolio, branch) { | |||
}); | |||
} | |||
export function loadMoreChildren(componentKey, page, isPortfolio, branch) { | |||
export function loadMoreChildren( | |||
componentKey: string, | |||
page: number, | |||
isPortfolio: boolean, | |||
branch?: string | |||
): Promise<Children> { | |||
const metrics = getMetrics(isPortfolio); | |||
return getChildren(componentKey, metrics, { branch, ps: PAGE_SIZE, p: page }) | |||
@@ -209,18 +230,14 @@ export function loadMoreChildren(componentKey, page, isPortfolio, branch) { | |||
}); | |||
} | |||
/** | |||
* Parse response of failed request | |||
* @param {Error} error | |||
* @returns {Promise} | |||
*/ | |||
export function parseError(error) { | |||
/** Parse response of failed request */ | |||
export function parseError(error: { response: Response }): Promise<string> { | |||
const DEFAULT_MESSAGE = translate('default_error_message'); | |||
try { | |||
return error.response | |||
.json() | |||
.then(r => r.errors.map(error => error.msg).join('. ')) | |||
.then(r => r.errors.map((error: any) => error.msg).join('. ')) | |||
.catch(() => DEFAULT_MESSAGE); | |||
} catch (ex) { | |||
return Promise.resolve(DEFAULT_MESSAGE); |
@@ -25,6 +25,7 @@ import MeasureContentContainer from './MeasureContentContainer'; | |||
import MeasureOverviewContainer from './MeasureOverviewContainer'; | |||
import Sidebar from '../sidebar/Sidebar'; | |||
import { hasBubbleChart, parseQuery, serializeQuery } from '../utils'; | |||
import { getBranchName } from '../../../helpers/branches'; | |||
import { translate } from '../../../helpers/l10n'; | |||
/*:: import type { Component, Query, Period } from '../types'; */ | |||
/*:: import type { RawQuery } from '../../../helpers/query'; */ | |||
@@ -33,12 +34,14 @@ import { translate } from '../../../helpers/l10n'; | |||
import '../style.css'; | |||
/*:: type Props = {| | |||
branch: {}, | |||
component: Component, | |||
currentUser: { isLoggedIn: boolean }, | |||
location: { pathname: string, query: RawQuery }, | |||
fetchMeasures: ( | |||
component: string, | |||
metricsKey: Array<string> | |||
metricsKey: Array<string>, | |||
branch: string | null | |||
) => Promise<{ component: Component, measures: Array<MeasureEnhanced>, leakPeriod: ?Period }>, | |||
fetchMetrics: () => void, | |||
metrics: { [string]: Metric }, | |||
@@ -81,6 +84,7 @@ export default class App extends React.PureComponent { | |||
componentWillReceiveProps(nextProps /*: Props */) { | |||
if ( | |||
nextProps.branch !== this.props.branch || | |||
nextProps.component.key !== this.props.component.key || | |||
nextProps.metrics !== this.props.metrics | |||
) { | |||
@@ -97,12 +101,12 @@ export default class App extends React.PureComponent { | |||
} | |||
} | |||
fetchMeasures = ({ component, fetchMeasures, metrics, metricsKey } /*: Props */) => { | |||
fetchMeasures = ({ branch, component, fetchMeasures, metrics, metricsKey } /*: Props */) => { | |||
this.setState({ loading: true }); | |||
const filteredKeys = metricsKey.filter( | |||
key => !metrics[key].hidden && !['DATA', 'DISTRIB'].includes(metrics[key].type) | |||
); | |||
fetchMeasures(component.key, filteredKeys).then( | |||
fetchMeasures(component.key, filteredKeys, getBranchName(branch)).then( | |||
({ measures, leakPeriod }) => { | |||
if (this.mounted) { | |||
this.setState({ | |||
@@ -125,6 +129,7 @@ export default class App extends React.PureComponent { | |||
pathname: this.props.location.pathname, | |||
query: { | |||
...query, | |||
branch: getBranchName(this.props.branch), | |||
id: this.props.component.key | |||
} | |||
}); | |||
@@ -135,7 +140,7 @@ export default class App extends React.PureComponent { | |||
if (isLoading) { | |||
return <i className="spinner spinner-margin" />; | |||
} | |||
const { component, fetchMeasures, metrics } = this.props; | |||
const { branch, component, fetchMeasures, metrics } = this.props; | |||
const { leakPeriod } = this.state; | |||
const query = parseQuery(this.props.location.query); | |||
const metric = metrics[query.metric]; | |||
@@ -159,6 +164,7 @@ export default class App extends React.PureComponent { | |||
{metric != null && | |||
<MeasureContentContainer | |||
branch={branch} | |||
className="layout-page-main" | |||
currentUser={this.props.currentUser} | |||
rootComponent={component} | |||
@@ -174,6 +180,7 @@ export default class App extends React.PureComponent { | |||
{metric == null && | |||
hasBubbleChart(query.metric) && | |||
<MeasureOverviewContainer | |||
branch={branch} | |||
className="layout-page-main" | |||
rootComponent={component} | |||
currentUser={this.props.currentUser} |
@@ -47,15 +47,19 @@ function banQualityGate(component /*: Component */) /*: Array<Measure> */ { | |||
return component.measures.filter(measure => !bannedMetrics.includes(measure.metric)); | |||
} | |||
const fetchMeasures = (component /*: string */, metricsKey /*: Array<string> */) => ( | |||
dispatch, | |||
getState | |||
) => { | |||
const fetchMeasures = ( | |||
component /*: string */, | |||
metricsKey /*: Array<string> */, | |||
branch /*: string | null */ | |||
) => (dispatch, getState) => { | |||
if (metricsKey.length <= 0) { | |||
return Promise.resolve({ component: {}, measures: [], leakPeriod: null }); | |||
} | |||
return getMeasuresAndMeta(component, metricsKey, { additionalFields: 'periods' }).then(r => { | |||
return getMeasuresAndMeta(component, metricsKey, { | |||
additionalFields: 'periods', | |||
branch | |||
}).then(r => { | |||
const measures = banQualityGate(r.component).map(measure => | |||
enhanceMeasure(measure, getMetrics(getState())) | |||
); |
@@ -22,10 +22,12 @@ import React from 'react'; | |||
import key from 'keymaster'; | |||
import Breadcrumb from './Breadcrumb'; | |||
import { getBreadcrumbs } from '../../../api/components'; | |||
import { getBranchName } from '../../../helpers/branches'; | |||
/*:: import type { Component } from '../types'; */ | |||
/*:: type Props = {| | |||
backToFirst: boolean, | |||
branch: {}, | |||
className?: string, | |||
component: Component, | |||
handleSelect: string => void, | |||
@@ -75,7 +77,7 @@ export default class Breadcrumbs extends React.PureComponent { | |||
key.unbind('left', 'measures-files'); | |||
} | |||
fetchBreadcrumbs = ({ component, rootComponent } /*: Props */) => { | |||
fetchBreadcrumbs = ({ branch, component, rootComponent } /*: Props */) => { | |||
const isRoot = component.key === rootComponent.key; | |||
if (isRoot) { | |||
if (this.mounted) { | |||
@@ -83,7 +85,7 @@ export default class Breadcrumbs extends React.PureComponent { | |||
} | |||
return; | |||
} | |||
getBreadcrumbs(component.key).then(breadcrumbs => { | |||
getBreadcrumbs(component.key, getBranchName(branch)).then(breadcrumbs => { | |||
if (this.mounted) { | |||
this.setState({ breadcrumbs }); | |||
} |
@@ -32,6 +32,7 @@ import TreeMapView from '../drilldown/TreeMapView'; | |||
import { getComponentTree } from '../../../api/components'; | |||
import { complementary } from '../config/complementary'; | |||
import { enhanceComponent, isFileType, isViewType } from '../utils'; | |||
import { getBranchName } from '../../../helpers/branches'; | |||
import { getProjectUrl } from '../../../helpers/urls'; | |||
import { isDiffMetric } from '../../../helpers/measures'; | |||
import { parseDate } from '../../../helpers/dates'; | |||
@@ -43,6 +44,7 @@ import { parseDate } from '../../../helpers/dates'; | |||
// https://github.com/facebook/flow/issues/3147 | |||
// router: { push: ({ pathname: string, query?: RawQuery }) => void } | |||
/*:: type Props = {| | |||
branch: {}, | |||
className?: string, | |||
component: Component, | |||
currentUser: { isLoggedIn: boolean }, | |||
@@ -86,7 +88,11 @@ export default class MeasureContent extends React.PureComponent { | |||
} | |||
componentWillReceiveProps(nextProps /*: Props */) { | |||
if (nextProps.component !== this.props.component || nextProps.metric !== this.props.metric) { | |||
if ( | |||
nextProps.branch !== this.props.branch || | |||
nextProps.component !== this.props.component || | |||
nextProps.metric !== this.props.metric | |||
) { | |||
this.fetchComponents(nextProps); | |||
} | |||
} | |||
@@ -110,7 +116,10 @@ export default class MeasureContent extends React.PureComponent { | |||
) => { | |||
const strategy = view === 'list' ? 'leaves' : 'children'; | |||
const metricKeys = [metric.key]; | |||
const opts /*: Object */ = { metricSortFilter: 'withMeasuresOnly' }; | |||
const opts /*: Object */ = { | |||
branch: getBranchName(this.props.branch), | |||
metricSortFilter: 'withMeasuresOnly' | |||
}; | |||
const isDiff = isDiffMetric(metric.key); | |||
if (isDiff) { | |||
opts.metricPeriodSort = 1; | |||
@@ -215,7 +224,7 @@ export default class MeasureContent extends React.PureComponent { | |||
onSelectComponent = (componentKey /*: string */) => this.setState({ selected: componentKey }); | |||
renderCode() { | |||
const { component, leakPeriod } = this.props; | |||
const { branch, component, leakPeriod } = this.props; | |||
const leakPeriodDate = | |||
isDiffMetric(this.props.metric.key) && leakPeriod != null ? parseDate(leakPeriod.date) : null; | |||
@@ -232,7 +241,11 @@ export default class MeasureContent extends React.PureComponent { | |||
} | |||
return ( | |||
<div className="measure-details-viewer"> | |||
<SourceViewer component={component.key} filterLine={filterLine} /> | |||
<SourceViewer | |||
branch={getBranchName(branch)} | |||
component={component.key} | |||
filterLine={filterLine} | |||
/> | |||
</div> | |||
); | |||
} | |||
@@ -244,6 +257,7 @@ export default class MeasureContent extends React.PureComponent { | |||
const selectedIdx = this.getSelectedIndex(); | |||
return ( | |||
<FilesView | |||
branch={this.props.branch} | |||
components={this.state.components} | |||
fetchMore={this.fetchMoreComponents} | |||
handleOpen={this.onOpenComponent} | |||
@@ -260,6 +274,7 @@ export default class MeasureContent extends React.PureComponent { | |||
if (view === 'treemap') { | |||
return ( | |||
<TreeMapView | |||
branch={this.props.branch} | |||
components={this.state.components} | |||
handleSelect={this.onOpenComponent} | |||
metric={metric} | |||
@@ -272,7 +287,7 @@ export default class MeasureContent extends React.PureComponent { | |||
} | |||
render() { | |||
const { component, currentUser, measure, metric, rootComponent, view } = this.props; | |||
const { branch, component, currentUser, measure, metric, rootComponent, view } = this.props; | |||
const isLoggedIn = currentUser && currentUser.isLoggedIn; | |||
const isFile = isFileType(component); | |||
const selectedIdx = this.getSelectedIndex(); | |||
@@ -286,6 +301,7 @@ export default class MeasureContent extends React.PureComponent { | |||
<div className="layout-page-main-inner"> | |||
<Breadcrumbs | |||
backToFirst={view === 'list'} | |||
branch={branch} | |||
className="measure-breadcrumbs spacer-right text-ellipsis" | |||
component={component} | |||
handleSelect={this.onOpenComponent} |
@@ -20,18 +20,21 @@ | |||
// @flow | |||
import React from 'react'; | |||
import MeasureContent from './MeasureContent'; | |||
import { getBranchName } from '../../../helpers/branches'; | |||
/*:: import type { Component, Period, Query } from '../types'; */ | |||
/*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */ | |||
/*:: import type { Metric } from '../../../store/metrics/actions'; */ | |||
/*:: import type { RawQuery } from '../../../helpers/query'; */ | |||
/*:: type Props = {| | |||
branch: {}, | |||
className?: string, | |||
currentUser: { isLoggedIn: boolean }, | |||
rootComponent: Component, | |||
fetchMeasures: ( | |||
component: string, | |||
metricsKey: Array<string> | |||
metricsKey: Array<string>, | |||
branch: string | null | |||
) => Promise<{ component: Component, measures: Array<MeasureEnhanced> }>, | |||
leakPeriod?: Period, | |||
metric: Metric, | |||
@@ -87,7 +90,7 @@ export default class MeasureContentContainer extends React.PureComponent { | |||
this.mounted = false; | |||
} | |||
fetchMeasure = ({ rootComponent, fetchMeasures, metric, selected } /*: Props */) => { | |||
fetchMeasure = ({ branch, rootComponent, fetchMeasures, metric, selected } /*: Props */) => { | |||
this.updateLoading({ measure: true }); | |||
const metricKeys = [metric.key]; | |||
@@ -99,7 +102,7 @@ export default class MeasureContentContainer extends React.PureComponent { | |||
metricKeys.push('file_complexity_distribution'); | |||
} | |||
fetchMeasures(selected || rootComponent.key, metricKeys).then( | |||
fetchMeasures(selected || rootComponent.key, metricKeys, getBranchName(branch)).then( | |||
({ component, measures }) => { | |||
if (this.mounted) { | |||
const measure = measures.find(measure => measure.metric.key === metric.key); | |||
@@ -132,6 +135,7 @@ export default class MeasureContentContainer extends React.PureComponent { | |||
return ( | |||
<MeasureContent | |||
branch={this.props.branch} | |||
className={this.props.className} | |||
component={this.state.component} | |||
currentUser={this.props.currentUser} |
@@ -26,11 +26,13 @@ import MeasureFavoriteContainer from './MeasureFavoriteContainer'; | |||
import PageActions from './PageActions'; | |||
import SourceViewer from '../../../components/SourceViewer/SourceViewer'; | |||
import { getComponentLeaves } from '../../../api/components'; | |||
import { getBranchName } from '../../../helpers/branches'; | |||
import { enhanceComponent, getBubbleMetrics, isFileType } from '../utils'; | |||
/*:: import type { Component, ComponentEnhanced, Paging, Period } from '../types'; */ | |||
/*:: import type { Metric } from '../../../store/metrics/actions'; */ | |||
/*:: type Props = {| | |||
branch: {}, | |||
className?: string, | |||
component: Component, | |||
currentUser: { isLoggedIn: boolean }, | |||
@@ -78,7 +80,7 @@ export default class MeasureOverview extends React.PureComponent { | |||
} | |||
fetchComponents = (props /*: Props */) => { | |||
const { component, domain, metrics } = props; | |||
const { branch, component, domain, metrics } = props; | |||
if (isFileType(component)) { | |||
return this.setState({ components: [], paging: null }); | |||
} | |||
@@ -88,6 +90,7 @@ export default class MeasureOverview extends React.PureComponent { | |||
metricsKey.push(colors.map(metric => metric.key)); | |||
} | |||
const options = { | |||
branch: getBranchName(branch), | |||
s: 'metric', | |||
metricSort: size.key, | |||
asc: false, | |||
@@ -112,11 +115,11 @@ export default class MeasureOverview extends React.PureComponent { | |||
}; | |||
renderContent() { | |||
const { component } = this.props; | |||
const { branch, component } = this.props; | |||
if (isFileType(component)) { | |||
return ( | |||
<div className="measure-details-viewer"> | |||
<SourceViewer component={component.key} /> | |||
<SourceViewer branch={getBranchName(branch)} component={component.key} /> | |||
</div> | |||
); | |||
} | |||
@@ -133,7 +136,7 @@ export default class MeasureOverview extends React.PureComponent { | |||
} | |||
render() { | |||
const { component, currentUser, leakPeriod, rootComponent } = this.props; | |||
const { branch, component, currentUser, leakPeriod, rootComponent } = this.props; | |||
const isLoggedIn = currentUser && currentUser.isLoggedIn; | |||
const isFile = isFileType(component); | |||
return ( | |||
@@ -143,6 +146,7 @@ export default class MeasureOverview extends React.PureComponent { | |||
<div className="layout-page-main-inner"> | |||
<Breadcrumbs | |||
backToFirst={true} | |||
branch={branch} | |||
className="measure-breadcrumbs spacer-right text-ellipsis" | |||
component={component} | |||
handleSelect={this.props.updateSelected} |
@@ -21,6 +21,7 @@ | |||
import React from 'react'; | |||
import MeasureOverview from './MeasureOverview'; | |||
import { getComponentShow } from '../../../api/components'; | |||
import { getBranchName } from '../../../helpers/branches'; | |||
import { getProjectUrl } from '../../../helpers/urls'; | |||
import { isViewType } from '../utils'; | |||
/*:: import type { Component, Period, Query } from '../types'; */ | |||
@@ -28,6 +29,7 @@ import { isViewType } from '../utils'; | |||
/*:: import type { Metric } from '../../../store/metrics/actions'; */ | |||
/*:: type Props = {| | |||
branch: {}, | |||
className?: string, | |||
rootComponent: Component, | |||
currentUser: { isLoggedIn: boolean }, | |||
@@ -80,14 +82,14 @@ export default class MeasureOverviewContainer extends React.PureComponent { | |||
this.mounted = false; | |||
} | |||
fetchComponent = ({ rootComponent, selected } /*: Props */) => { | |||
fetchComponent = ({ branch, rootComponent, selected } /*: Props */) => { | |||
if (!selected || rootComponent.key === selected) { | |||
this.setState({ component: rootComponent }); | |||
this.updateLoading({ component: false }); | |||
return; | |||
} | |||
this.updateLoading({ component: true }); | |||
getComponentShow(selected).then( | |||
getComponentShow(selected, getBranchName(branch)).then( | |||
({ component }) => { | |||
if (this.mounted) { | |||
this.setState({ component }); | |||
@@ -121,6 +123,7 @@ export default class MeasureOverviewContainer extends React.PureComponent { | |||
return ( | |||
<MeasureOverview | |||
branch={this.props.branch} | |||
className={this.props.className} | |||
component={this.state.component} | |||
currentUser={this.props.currentUser} |
@@ -34,6 +34,7 @@ jest.mock('../../../../api/components', () => ({ | |||
it('should display correctly for the list view', () => { | |||
const wrapper = mount( | |||
<Breadcrumbs | |||
branch={{ isMain: true }} | |||
component={{ key: 'bar', name: 'Bar' }} | |||
handleSelect={() => {}} | |||
rootComponent={{ key: 'foo', name: 'Foo' }} | |||
@@ -46,6 +47,7 @@ it('should display correctly for the list view', () => { | |||
it('should display only the root component', () => { | |||
const wrapper = mount( | |||
<Breadcrumbs | |||
branch={{ isMain: true }} | |||
component={{ key: 'foo', name: 'Foo' }} | |||
handleSelect={() => {}} | |||
rootComponent={{ key: 'foo', name: 'Foo' }} | |||
@@ -58,6 +60,7 @@ it('should display only the root component', () => { | |||
it.only('should load the breadcrumb from the api', () => { | |||
const wrapper = mount( | |||
<Breadcrumbs | |||
branch={{ isMain: true }} | |||
component={{ key: 'bar', name: 'Bar' }} | |||
handleSelect={() => {}} | |||
rootComponent={{ key: 'foo', name: 'Foo' }} |
@@ -148,6 +148,7 @@ exports[`should render correctly 1`] = ` | |||
Object { | |||
"pathname": "/project/activity", | |||
"query": Object { | |||
"branch": undefined, | |||
"custom_metrics": "reliability_rating", | |||
"graph": "custom", | |||
"id": "foo", |
@@ -20,11 +20,13 @@ | |||
// @flow | |||
import React from 'react'; | |||
import QualifierIcon from '../../../components/icons-components/QualifierIcon'; | |||
import { getBranchName } from '../../../helpers/branches'; | |||
import { splitPath } from '../../../helpers/path'; | |||
import { getComponentUrl } from '../../../helpers/urls'; | |||
/*:: import type { ComponentEnhanced } from '../types'; */ | |||
/*:: type Props = { | |||
branch: {}, | |||
component: ComponentEnhanced, | |||
onClick: string => void | |||
}; */ | |||
@@ -66,22 +68,22 @@ export default class ComponentCell extends React.PureComponent { | |||
} | |||
render() { | |||
const { component } = this.props; | |||
const { branch, component } = this.props; | |||
return ( | |||
<td className="measure-details-component-cell"> | |||
<div className="text-ellipsis"> | |||
{component.refId == null | |||
{component.refKey == null | |||
? <a | |||
id={'component-measures-component-link-' + component.key} | |||
className="link-no-underline" | |||
href={getComponentUrl(component.key)} | |||
href={getComponentUrl(component.key, getBranchName(branch))} | |||
onClick={this.handleClick}> | |||
{this.renderInner()} | |||
</a> | |||
: <a | |||
id={'component-measures-component-link-' + component.key} | |||
className="link-no-underline" | |||
href={getComponentUrl(component.refKey || component.key)}> | |||
href={getComponentUrl(component.refKey, getBranchName(branch))}> | |||
<span className="big-spacer-right"> | |||
<i className="icon-detach" /> | |||
</span> |
@@ -27,6 +27,7 @@ import { getLocalizedMetricName } from '../../../helpers/l10n'; | |||
/*:: import type { Metric } from '../../../store/metrics/actions'; */ | |||
/*:: type Props = {| | |||
branch: {}, | |||
components: Array<ComponentEnhanced>, | |||
onClick: string => void, | |||
metric: Metric, | |||
@@ -35,7 +36,7 @@ import { getLocalizedMetricName } from '../../../helpers/l10n'; | |||
|}; */ | |||
export default function ComponentsList( | |||
{ components, onClick, metrics, metric, selectedComponent } /*: Props */ | |||
{ branch, components, onClick, metrics, metric, selectedComponent } /*: Props */ | |||
) { | |||
if (!components.length) { | |||
return <EmptyResult />; | |||
@@ -67,6 +68,7 @@ export default function ComponentsList( | |||
{components.map(component => | |||
<ComponentsListRow | |||
key={component.id} | |||
branch={branch} | |||
component={component} | |||
otherMetrics={otherMetrics} | |||
isSelected={component.key === selectedComponent} |
@@ -26,6 +26,7 @@ import MeasureCell from './MeasureCell'; | |||
/*:: import type { Metric } from '../../../store/metrics/actions'; */ | |||
/*:: type Props = {| | |||
branch: {}, | |||
component: ComponentEnhanced, | |||
isSelected: boolean, | |||
onClick: string => void, | |||
@@ -34,7 +35,7 @@ import MeasureCell from './MeasureCell'; | |||
|}; */ | |||
export default function ComponentsListRow(props /*: Props */) { | |||
const { component } = props; | |||
const { branch, component } = props; | |||
const otherMeasures = props.otherMetrics.map(metric => { | |||
const measure = component.measures.find(measure => measure.metric.key === metric.key); | |||
return { ...measure, metric }; | |||
@@ -44,7 +45,7 @@ export default function ComponentsListRow(props /*: Props */) { | |||
}); | |||
return ( | |||
<tr className={rowClass}> | |||
<ComponentCell component={component} onClick={props.onClick} /> | |||
<ComponentCell branch={branch} component={component} onClick={props.onClick} /> | |||
<MeasureCell component={component} metric={props.metric} /> | |||
@@ -28,6 +28,7 @@ import { scrollToElement } from '../../../helpers/scrolling'; | |||
/*:: import type { Metric } from '../../../store/metrics/actions'; */ | |||
/*:: type Props = {| | |||
branch: {}, | |||
components: Array<ComponentEnhanced>, | |||
fetchMore: () => void, | |||
handleSelect: string => void, | |||
@@ -117,6 +118,7 @@ export default class ListView extends React.PureComponent { | |||
return ( | |||
<div ref={elem => (this.listContainer = elem)}> | |||
<ComponentsList | |||
branch={this.props.branch} | |||
components={this.props.components} | |||
metrics={this.props.metrics} | |||
metric={this.props.metric} |
@@ -26,6 +26,7 @@ import ColorGradientLegend from '../../../components/charts/ColorGradientLegend' | |||
import EmptyResult from './EmptyResult'; | |||
import QualifierIcon from '../../../components/icons-components/QualifierIcon'; | |||
import TreeMap from '../../../components/charts/TreeMap'; | |||
import { getBranchName } from '../../../helpers/branches'; | |||
import { translate, translateWithParameters, getLocalizedMetricName } from '../../../helpers/l10n'; | |||
import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; | |||
import { getComponentUrl } from '../../../helpers/urls'; | |||
@@ -34,6 +35,7 @@ import { getComponentUrl } from '../../../helpers/urls'; | |||
/*:: import type { TreeMapItem } from '../../../components/charts/TreeMap'; */ | |||
/*:: type Props = {| | |||
branch: {}, | |||
components: Array<ComponentEnhanced>, | |||
handleSelect: string => void, | |||
metric: Metric | |||
@@ -62,7 +64,7 @@ export default class TreeMapView extends React.PureComponent { | |||
} | |||
} | |||
getTreemapComponents = ({ components, metric } /*: Props */) => { | |||
getTreemapComponents = ({ branch, components, metric } /*: Props */) => { | |||
const colorScale = this.getColorScale(metric); | |||
return components | |||
.map(component => { | |||
@@ -93,7 +95,7 @@ export default class TreeMapView extends React.PureComponent { | |||
sizeValue | |||
), | |||
label: component.name, | |||
link: getComponentUrl(component.refKey || component.key) | |||
link: getComponentUrl(component.refKey || component.key, getBranchName(branch)) | |||
}; | |||
}) | |||
.filter(Boolean); |
@@ -55,6 +55,7 @@ import { | |||
} from '../utils'; */ | |||
import ListFooter from '../../../components/controls/ListFooter'; | |||
import EmptySearch from '../../../components/common/EmptySearch'; | |||
import { getBranchName } from '../../../helpers/branches'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { scrollToElement } from '../../../helpers/scrolling'; | |||
/*:: import type { Issue } from '../../../components/issue/types'; */ | |||
@@ -173,6 +174,7 @@ export default class App extends React.PureComponent { | |||
const { query: prevQuery } = prevProps.location; | |||
if ( | |||
prevProps.component !== this.props.component || | |||
prevProps.branch !== this.props.branch || | |||
!areQueriesEqual(prevQuery, query) || | |||
areMyIssuesSelected(prevQuery) !== areMyIssuesSelected(query) | |||
) { | |||
@@ -308,7 +310,7 @@ export default class App extends React.PureComponent { | |||
pathname: this.props.location.pathname, | |||
query: { | |||
...serializeQuery(this.state.query), | |||
branch: this.props.branch && this.props.branch.name, | |||
branch: this.props.branch && getBranchName(this.props.branch), | |||
id: this.props.component && this.props.component.key, | |||
myIssues: this.state.myIssues ? 'true' : undefined, | |||
open: issue | |||
@@ -327,7 +329,7 @@ export default class App extends React.PureComponent { | |||
pathname: this.props.location.pathname, | |||
query: { | |||
...serializeQuery(this.state.query), | |||
branch: this.props.branch && this.props.branch.name, | |||
branch: this.props.branch && getBranchName(this.props.branch), | |||
id: this.props.component && this.props.component.key, | |||
myIssues: this.state.myIssues ? 'true' : undefined, | |||
open: undefined | |||
@@ -363,7 +365,7 @@ export default class App extends React.PureComponent { | |||
: undefined; | |||
const parameters = { | |||
branch: this.props.branch && this.props.branch.name, | |||
branch: this.props.branch && getBranchName(this.props.branch), | |||
componentKeys: component && component.key, | |||
s: 'FILE_LINE', | |||
...serializeQuery(query), | |||
@@ -554,7 +556,7 @@ export default class App extends React.PureComponent { | |||
pathname: this.props.location.pathname, | |||
query: { | |||
...serializeQuery({ ...this.state.query, ...changes }), | |||
branch: this.props.branch && this.props.branch.name, | |||
branch: this.props.branch && getBranchName(this.props.branch), | |||
id: this.props.component && this.props.component.key, | |||
myIssues: this.state.myIssues ? 'true' : undefined | |||
} | |||
@@ -570,7 +572,7 @@ export default class App extends React.PureComponent { | |||
pathname: this.props.location.pathname, | |||
query: { | |||
...serializeQuery({ ...this.state.query, assigned: true, assignees: [] }), | |||
branch: this.props.branch && this.props.branch.name, | |||
branch: this.props.branch && getBranchName(this.props.branch), | |||
id: this.props.component && this.props.component.key, | |||
myIssues: myIssues ? 'true' : undefined | |||
} | |||
@@ -597,7 +599,7 @@ export default class App extends React.PureComponent { | |||
pathname: this.props.location.pathname, | |||
query: { | |||
...DEFAULT_QUERY, | |||
branch: this.props.branch && this.props.branch.name, | |||
branch: this.props.branch && getBranchName(this.props.branch), | |||
id: this.props.component && this.props.component.key, | |||
myIssues: this.state.myIssues ? 'true' : undefined | |||
} | |||
@@ -890,7 +892,7 @@ export default class App extends React.PureComponent { | |||
<div> | |||
{openIssue | |||
? <IssuesSourceViewer | |||
branch={this.props.branch} | |||
branch={this.props.branch && getBranchName(this.props.branch)} | |||
component={component} | |||
openIssue={openIssue} | |||
loadIssues={this.fetchIssuesForComponent} |
@@ -26,7 +26,7 @@ import { scrollToElement } from '../../../helpers/scrolling'; | |||
/*:: | |||
type Props = {| | |||
branch?: { name: string }, | |||
branch?: string, | |||
component: Component, | |||
loadIssues: (string, number, number) => Promise<*>, | |||
onIssueChange: Issue => void, | |||
@@ -86,7 +86,7 @@ export default class IssuesSourceViewer extends React.PureComponent { | |||
<div ref={node => (this.node = node)}> | |||
<SourceViewer | |||
aroundLine={openIssue.textRange ? openIssue.textRange.endLine : undefined} | |||
branch={this.props.branch && this.props.branch.name} | |||
branch={this.props.branch} | |||
component={openIssue.component} | |||
displayAllIssues={true} | |||
highlightedLocations={locations} |
@@ -28,7 +28,7 @@ import SourceViewer from '../../../components/SourceViewer/SourceViewer'; | |||
/*:: | |||
type Props = { | |||
branch: {}, | |||
branch: { name: string }, | |||
component: { | |||
analysisDate?: string, | |||
id: string, | |||
@@ -84,6 +84,12 @@ export default class App extends React.PureComponent { | |||
return <EmptyOverview component={component} />; | |||
} | |||
return <OverviewApp component={component} onComponentChange={this.props.onComponentChange} />; | |||
return ( | |||
<OverviewApp | |||
branch={this.props.branch} | |||
component={component} | |||
onComponentChange={this.props.onComponentChange} | |||
/> | |||
); | |||
} | |||
} |
@@ -36,11 +36,13 @@ import { getLeakPeriod } from '../../../helpers/periods'; | |||
import { getCustomGraph, getGraph } from '../../../helpers/storage'; | |||
import { METRICS, HISTORY_METRICS_LIST } from '../utils'; | |||
import { DEFAULT_GRAPH, getDisplayedHistoryMetrics } from '../../projectActivity/utils'; | |||
import { getBranchName } from '../../../helpers/branches'; | |||
/*:: import type { Component, History, MeasuresList, Period } from '../types'; */ | |||
import '../styles.css'; | |||
/*:: | |||
type Props = { | |||
branch: { name: string }, | |||
component: Component, | |||
onComponentChange: {} => void | |||
}; | |||
@@ -70,14 +72,12 @@ export default class OverviewApp extends React.PureComponent { | |||
if (domElement) { | |||
domElement.classList.add('dashboard-page'); | |||
} | |||
this.loadMeasures(this.props.component.key).then(() => this.loadHistory(this.props.component)); | |||
this.loadMeasures().then(this.loadHistory); | |||
} | |||
componentDidUpdate(prevProps /*: Props */) { | |||
if (this.props.component.key !== prevProps.component.key) { | |||
this.loadMeasures(this.props.component.key).then(() => | |||
this.loadHistory(this.props.component) | |||
); | |||
if (this.props.component.key !== prevProps.component.key || this.props.branch !== prevProps.branch) { | |||
this.loadMeasures().then(this.loadHistory); | |||
} | |||
} | |||
@@ -89,11 +89,13 @@ export default class OverviewApp extends React.PureComponent { | |||
} | |||
} | |||
loadMeasures(componentKey /*: string */) { | |||
loadMeasures() { | |||
const { branch, component } = this.props; | |||
this.setState({ loading: true }); | |||
return getMeasuresAndMeta(componentKey, METRICS, { | |||
additionalFields: 'metrics,periods' | |||
return getMeasuresAndMeta(component.key, METRICS, { | |||
additionalFields: 'metrics,periods', | |||
branch: getBranchName(branch) | |||
}).then( | |||
r => { | |||
if (this.mounted) { | |||
@@ -113,14 +115,18 @@ export default class OverviewApp extends React.PureComponent { | |||
); | |||
} | |||
loadHistory(component /*: Component */) { | |||
loadHistory = () => { | |||
const { branch, component } = this.props; | |||
let graphMetrics = getDisplayedHistoryMetrics(getGraph(), getCustomGraph()); | |||
if (!graphMetrics || graphMetrics.length <= 0) { | |||
graphMetrics = getDisplayedHistoryMetrics(DEFAULT_GRAPH, []); | |||
} | |||
const metrics = uniq(HISTORY_METRICS_LIST.concat(graphMetrics)); | |||
return getAllTimeMachineData(component.key, metrics).then(r => { | |||
return getAllTimeMachineData(component.key, metrics, { | |||
branch: getBranchName(branch) | |||
}).then(r => { | |||
if (this.mounted) { | |||
const history /*: History */ = {}; | |||
r.measures.forEach(measure => { | |||
@@ -134,7 +140,7 @@ export default class OverviewApp extends React.PureComponent { | |||
this.setState({ history, historyStartDate }); | |||
} | |||
}, throwGlobalError); | |||
} | |||
}; | |||
getApplicationLeakPeriod = () => | |||
this.state.measures.find(measure => measure.metric.key === 'new_bugs') ? { index: 1 } : null; | |||
@@ -148,7 +154,7 @@ export default class OverviewApp extends React.PureComponent { | |||
} | |||
render() { | |||
const { component } = this.props; | |||
const { branch, component } = this.props; | |||
const { loading, measures, periods, history, historyStartDate } = this.state; | |||
if (loading) { | |||
@@ -157,7 +163,7 @@ export default class OverviewApp extends React.PureComponent { | |||
const leakPeriod = | |||
component.qualifier === 'APP' ? this.getApplicationLeakPeriod() : getLeakPeriod(periods); | |||
const domainProps = { component, measures, leakPeriod, history, historyStartDate }; | |||
const domainProps = { branch, component, measures, leakPeriod, history, historyStartDate }; | |||
return ( | |||
<div className="page page-limited"> | |||
@@ -165,7 +171,7 @@ export default class OverviewApp extends React.PureComponent { | |||
<div className="overview-main page-main"> | |||
{component.qualifier === 'APP' | |||
? <ApplicationQualityGate component={component} /> | |||
: <QualityGate component={component} measures={measures} />} | |||
: <QualityGate branch={branch} component={component} measures={measures} />} | |||
<div className="overview-domains-list"> | |||
<BugsAndVulnerabilities {...domainProps} /> | |||
@@ -177,6 +183,7 @@ export default class OverviewApp extends React.PureComponent { | |||
<div className="page-sidebar-fixed"> | |||
<Meta | |||
branch={branch} | |||
component={component} | |||
history={history} | |||
measures={measures} |
@@ -24,12 +24,14 @@ import Analysis from './Analysis'; | |||
import PreviewGraph from './PreviewGraph'; | |||
import { getMetrics } from '../../../api/metrics'; | |||
import { getProjectActivity } from '../../../api/projectActivity'; | |||
import { getBranchName } from '../../../helpers/branches'; | |||
import { translate } from '../../../helpers/l10n'; | |||
/*:: import type { Analysis as AnalysisType } from '../../projectActivity/types'; */ | |||
/*:: import type { History, Metric } from '../types'; */ | |||
/*:: | |||
type Props = { | |||
branch: {}, | |||
history: ?History, | |||
project: string, | |||
qualifier: string, | |||
@@ -70,7 +72,11 @@ export default class AnalysesList extends React.PureComponent { | |||
fetchData() { | |||
this.setState({ loading: true }); | |||
Promise.all([ | |||
getProjectActivity({ project: this.props.project, ps: PAGE_SIZE }), | |||
getProjectActivity({ | |||
branch: getBranchName(this.props.branch), | |||
project: this.props.project, | |||
ps: PAGE_SIZE | |||
}), | |||
getMetrics() | |||
]).then(response => { | |||
if (this.mounted) { | |||
@@ -111,6 +117,7 @@ export default class AnalysesList extends React.PureComponent { | |||
</h4> | |||
<PreviewGraph | |||
branch={this.props.branch} | |||
history={this.props.history} | |||
project={this.props.project} | |||
metrics={this.state.metrics} | |||
@@ -120,7 +127,11 @@ export default class AnalysesList extends React.PureComponent { | |||
{this.renderList(analyses)} | |||
<div className="spacer-top small"> | |||
<Link to={{ pathname: '/project/activity', query: { id: this.props.project } }}> | |||
<Link | |||
to={{ | |||
pathname: '/project/activity', | |||
query: { id: this.props.project, branch: getBranchName(this.props.branch) } | |||
}}> | |||
{translate('show_more')} | |||
</Link> | |||
</div> |
@@ -31,6 +31,7 @@ import { | |||
hasHistoryDataValue, | |||
splitSeriesInGraphs | |||
} from '../../projectActivity/utils'; | |||
import { getBranchName } from '../../../helpers/branches'; | |||
import { getCustomGraph, getGraph } from '../../../helpers/storage'; | |||
import { formatMeasure, getShortType } from '../../../helpers/measures'; | |||
import { translate } from '../../../helpers/l10n'; | |||
@@ -39,6 +40,7 @@ import { translate } from '../../../helpers/l10n'; | |||
/*:: | |||
type Props = { | |||
branch: {}, | |||
history: ?History, | |||
metrics: Array<Metric>, | |||
project: string, | |||
@@ -137,7 +139,10 @@ export default class PreviewGraph extends React.PureComponent { | |||
}; | |||
handleClick = () => { | |||
this.props.router.push({ pathname: '/project/activity', query: { id: this.props.project } }); | |||
this.props.router.push({ | |||
pathname: '/project/activity', | |||
query: { id: this.props.project, branch: getBranchName(this.props.branch) } | |||
}); | |||
}; | |||
updateTooltip = ( |
@@ -26,12 +26,14 @@ import BugIcon from '../../../components/icons-components/BugIcon'; | |||
import LeakPeriodLegend from '../components/LeakPeriodLegend'; | |||
import VulnerabilityIcon from '../../../components/icons-components/VulnerabilityIcon'; | |||
import { getMetricName } from '../helpers/metrics'; | |||
import { getBranchName } from '../../../helpers/branches'; | |||
import { getComponentDrilldownUrl } from '../../../helpers/urls'; | |||
import { translate } from '../../../helpers/l10n'; | |||
class BugsAndVulnerabilities extends React.PureComponent { | |||
renderHeader() { | |||
const { component } = this.props; | |||
const { branch, component } = this.props; | |||
const branchName = getBranchName(branch); | |||
return ( | |||
<div className="overview-card-header"> | |||
@@ -41,7 +43,7 @@ class BugsAndVulnerabilities extends React.PureComponent { | |||
</span> | |||
<Link | |||
className="button button-small button-compact spacer-left text-text-bottom" | |||
to={getComponentDrilldownUrl(component.key, 'Reliability')}> | |||
to={getComponentDrilldownUrl(component.key, 'Reliability', branchName)}> | |||
<BubblesIcon size={14} /> | |||
</Link> | |||
<span className="big-spacer-left"> | |||
@@ -49,7 +51,7 @@ class BugsAndVulnerabilities extends React.PureComponent { | |||
</span> | |||
<Link | |||
className="button button-small button-compact spacer-left text-text-bottom" | |||
to={getComponentDrilldownUrl(component.key, 'Security')}> | |||
to={getComponentDrilldownUrl(component.key, 'Security', branchName)}> | |||
<BubblesIcon size={14} /> | |||
</Link> | |||
</div> |
@@ -28,6 +28,7 @@ import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; | |||
import { getComponentIssuesUrl } from '../../../helpers/urls'; | |||
import CodeSmellIcon from '../../../components/icons-components/CodeSmellIcon'; | |||
import { getBranchName } from '../../../helpers/branches'; | |||
class CodeSmells extends React.PureComponent { | |||
renderHeader() { | |||
@@ -35,10 +36,15 @@ class CodeSmells extends React.PureComponent { | |||
} | |||
renderDebt(metric, type) { | |||
const { measures, component } = this.props; | |||
const { branch, measures, component } = this.props; | |||
const measure = measures.find(measure => measure.metric.key === metric); | |||
const value = this.props.getValue(measure); | |||
const params = { resolved: 'false', facetMode: 'effort', types: type }; | |||
const params = { | |||
branch: getBranchName(branch), | |||
resolved: 'false', | |||
facetMode: 'effort', | |||
types: type | |||
}; | |||
if (isDiffMetric(metric)) { | |||
Object.assign(params, { sinceLeakPeriod: 'true' }); |
@@ -20,6 +20,7 @@ | |||
import React from 'react'; | |||
import enhance from './enhance'; | |||
import { DrilldownLink } from '../../../components/shared/drilldown-link'; | |||
import { getBranchName } from '../../../helpers/branches'; | |||
import { getMetricName } from '../helpers/metrics'; | |||
import { formatMeasure, getPeriodValue } from '../../../helpers/measures'; | |||
import { translate } from '../../../helpers/l10n'; | |||
@@ -55,7 +56,7 @@ class Coverage extends React.PureComponent { | |||
} | |||
renderCoverage() { | |||
const { component } = this.props; | |||
const { branch, component } = this.props; | |||
const metric = 'coverage'; | |||
const coverage = this.getCoverage(); | |||
@@ -67,7 +68,7 @@ class Coverage extends React.PureComponent { | |||
<div className="display-inline-block text-middle"> | |||
<div className="overview-domain-measure-value"> | |||
<DrilldownLink component={component.key} metric={metric}> | |||
<DrilldownLink branch={getBranchName(branch)} component={component.key} metric={metric}> | |||
<span className="js-overview-main-coverage"> | |||
{formatMeasure(coverage, 'PERCENT')} | |||
</span> | |||
@@ -84,7 +85,8 @@ class Coverage extends React.PureComponent { | |||
} | |||
renderNewCoverage() { | |||
const { component, leakPeriod } = this.props; | |||
const { branch, component, leakPeriod } = this.props; | |||
const branchName = getBranchName(branch); | |||
const newCoverageMeasure = this.getNewCoverageMeasure(); | |||
const newLinesToCover = this.getNewLinesToCover(); | |||
@@ -98,7 +100,10 @@ class Coverage extends React.PureComponent { | |||
const formattedValue = | |||
newCoverageValue != null | |||
? <div> | |||
<DrilldownLink component={component.key} metric={newCoverageMeasure.metric.key}> | |||
<DrilldownLink | |||
branch={branchName} | |||
component={component.key} | |||
metric={newCoverageMeasure.metric.key}> | |||
<span className="js-overview-main-new-coverage"> | |||
{formatMeasure(newCoverageValue, 'PERCENT')} | |||
</span> | |||
@@ -111,6 +116,7 @@ class Coverage extends React.PureComponent { | |||
{translate('overview.coverage_on')} | |||
<br /> | |||
<DrilldownLink | |||
branch={branchName} | |||
className="spacer-right overview-domain-secondary-measure-value" | |||
component={component.key} | |||
metric={newLinesToCover.metric.key}> |
@@ -20,6 +20,7 @@ | |||
import React from 'react'; | |||
import enhance from './enhance'; | |||
import { DrilldownLink } from '../../../components/shared/drilldown-link'; | |||
import { getBranchName } from '../../../helpers/branches'; | |||
import { getMetricName } from '../helpers/metrics'; | |||
import { formatMeasure, getPeriodValue } from '../../../helpers/measures'; | |||
import { translate } from '../../../helpers/l10n'; | |||
@@ -39,7 +40,7 @@ class Duplications extends React.PureComponent { | |||
} | |||
renderDuplications() { | |||
const { component, measures } = this.props; | |||
const { branch, component, measures } = this.props; | |||
const measure = measures.find(measure => measure.metric.key === 'duplicated_lines_density'); | |||
const duplications = Number(measure.value); | |||
@@ -51,7 +52,10 @@ class Duplications extends React.PureComponent { | |||
<div className="display-inline-block text-middle"> | |||
<div className="overview-domain-measure-value"> | |||
<DrilldownLink component={component.key} metric="duplicated_lines_density"> | |||
<DrilldownLink | |||
branch={getBranchName(branch)} | |||
component={component.key} | |||
metric="duplicated_lines_density"> | |||
{formatMeasure(duplications, 'PERCENT')} | |||
</DrilldownLink> | |||
</div> | |||
@@ -66,7 +70,8 @@ class Duplications extends React.PureComponent { | |||
} | |||
renderNewDuplications() { | |||
const { component, measures, leakPeriod } = this.props; | |||
const { branch, component, measures, leakPeriod } = this.props; | |||
const branchName = getBranchName(branch); | |||
const newDuplicationsMeasure = measures.find( | |||
measure => measure.metric.key === 'new_duplicated_lines_density' | |||
); | |||
@@ -82,7 +87,10 @@ class Duplications extends React.PureComponent { | |||
const formattedValue = | |||
newDuplicationsValue != null | |||
? <div> | |||
<DrilldownLink component={component.key} metric={newDuplicationsMeasure.metric.key}> | |||
<DrilldownLink | |||
branch={branchName} | |||
component={component.key} | |||
metric={newDuplicationsMeasure.metric.key}> | |||
<span className="js-overview-main-new-duplications"> | |||
{formatMeasure(newDuplicationsValue, 'PERCENT')} | |||
</span> | |||
@@ -95,6 +103,7 @@ class Duplications extends React.PureComponent { | |||
{translate('overview.duplications_on')} | |||
<br /> | |||
<DrilldownLink | |||
branch={branchName} | |||
className="spacer-right overview-domain-secondary-measure-value" | |||
component={component.key} | |||
metric={newLinesMeasure.metric.key}> |
@@ -26,6 +26,7 @@ import HistoryIcon from '../../../components/icons-components/HistoryIcon'; | |||
import Rating from './../../../components/ui/Rating'; | |||
import Timeline from '../components/Timeline'; | |||
import Tooltip from '../../../components/controls/Tooltip'; | |||
import { getBranchName } from '../../../helpers/branches'; | |||
import { | |||
formatMeasure, | |||
formatMeasureVariation, | |||
@@ -59,7 +60,7 @@ export default function enhance(ComposedComponent) { | |||
}; | |||
renderHeader = (domain, label) => { | |||
const { component } = this.props; | |||
const { branch, component } = this.props; | |||
return ( | |||
<div className="overview-card-header"> | |||
<div className="overview-title"> | |||
@@ -68,7 +69,7 @@ export default function enhance(ComposedComponent) { | |||
</span> | |||
<Link | |||
className="button button-small button-compact spacer-left text-text-bottom" | |||
to={getComponentDrilldownUrl(component.key, domain)}> | |||
to={getComponentDrilldownUrl(component.key, domain, getBranchName(branch))}> | |||
<BubblesIcon size={14} /> | |||
</Link> | |||
</div> | |||
@@ -77,7 +78,7 @@ export default function enhance(ComposedComponent) { | |||
}; | |||
renderMeasure = metricKey => { | |||
const { measures, component } = this.props; | |||
const { branch, measures, component } = this.props; | |||
const measure = measures.find(measure => measure.metric.key === metricKey); | |||
if (measure == null) { | |||
@@ -87,7 +88,10 @@ export default function enhance(ComposedComponent) { | |||
return ( | |||
<div className="overview-domain-measure"> | |||
<div className="overview-domain-measure-value"> | |||
<DrilldownLink component={component.key} metric={metricKey}> | |||
<DrilldownLink | |||
branch={getBranchName(branch)} | |||
component={component.key} | |||
metric={metricKey}> | |||
<span className="js-overview-main-tests"> | |||
{formatMeasure(measure.value, getShortType(measure.metric.type))} | |||
</span> | |||
@@ -125,7 +129,7 @@ export default function enhance(ComposedComponent) { | |||
}; | |||
renderRating = metricKey => { | |||
const { component, measures } = this.props; | |||
const { branch, component, measures } = this.props; | |||
const measure = measures.find(measure => measure.metric.key === metricKey); | |||
if (!measure) { | |||
return null; | |||
@@ -136,6 +140,7 @@ export default function enhance(ComposedComponent) { | |||
<Tooltip overlay={title} placement="top"> | |||
<div className="overview-domain-measure-sup"> | |||
<DrilldownLink | |||
branch={getBranchName(branch)} | |||
className="link-no-underline" | |||
component={component.key} | |||
metric={metricKey}> | |||
@@ -147,10 +152,11 @@ export default function enhance(ComposedComponent) { | |||
}; | |||
renderIssues = (metric, type) => { | |||
const { measures, component } = this.props; | |||
const { branch, measures, component } = this.props; | |||
const measure = measures.find(measure => measure.metric.key === metric); | |||
const value = this.getValue(measure); | |||
const params = { | |||
branch: getBranchName(branch), | |||
resolved: 'false', | |||
types: type | |||
}; | |||
@@ -182,7 +188,11 @@ export default function enhance(ComposedComponent) { | |||
return ( | |||
<Link | |||
className={linkClass} | |||
to={getComponentMeasureHistory(this.props.component.key, metricKey)}> | |||
to={getComponentMeasureHistory( | |||
this.props.component.key, | |||
metricKey, | |||
getBranchName(this.props.branch) | |||
)}> | |||
<HistoryIcon /> | |||
</Link> | |||
); |
@@ -31,6 +31,7 @@ import MetaTags from './MetaTags'; | |||
import { areThereCustomOrganizations } from '../../../store/rootReducer'; | |||
const Meta = ({ | |||
branch, | |||
component, | |||
history, | |||
measures, | |||
@@ -58,12 +59,13 @@ const Meta = ({ | |||
{description} | |||
</div>} | |||
<MetaSize component={component} measures={measures} /> | |||
<MetaSize branch={branch} component={component} measures={measures} /> | |||
{isProject && <MetaTags component={component} onComponentChange={onComponentChange} />} | |||
{(isProject || isApplication) && | |||
<AnalysesList | |||
branch={branch} | |||
project={component.key} | |||
qualifier={component.qualifier} | |||
history={history} |
@@ -36,7 +36,7 @@ export default class MetaLinks extends React.PureComponent { | |||
} | |||
componentDidUpdate(prevProps) { | |||
if (prevProps.component !== this.props.component) { | |||
if (prevProps.component.key !== this.props.component.key) { | |||
this.loadLinks(); | |||
} | |||
} |
@@ -23,12 +23,14 @@ import classNames from 'classnames'; | |||
import { DrilldownLink } from '../../../components/shared/drilldown-link'; | |||
import LanguageDistribution from '../../../components/charts/LanguageDistribution'; | |||
import SizeRating from '../../../components/ui/SizeRating'; | |||
import { getBranchName } from '../../../helpers/branches'; | |||
import { formatMeasure } from '../../../helpers/measures'; | |||
import { getMetricName } from '../helpers/metrics'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export default class MetaSize extends React.PureComponent { | |||
static propTypes = { | |||
branch: PropTypes.object.isRequired, | |||
component: PropTypes.object.isRequired, | |||
measures: PropTypes.array.isRequired | |||
}; | |||
@@ -42,7 +44,10 @@ export default class MetaSize extends React.PureComponent { | |||
<span className="spacer-right"> | |||
<SizeRating value={ncloc.value} /> | |||
</span> | |||
<DrilldownLink component={this.props.component.key} metric="ncloc"> | |||
<DrilldownLink | |||
branch={getBranchName(this.props.branch)} | |||
component={this.props.component.key} | |||
metric="ncloc"> | |||
{formatMeasure(ncloc.value, 'SHORT_INT')} | |||
</DrilldownLink> | |||
<div className="overview-domain-measure-label text-muted"> | |||
@@ -69,7 +74,10 @@ export default class MetaSize extends React.PureComponent { | |||
? <div | |||
id="overview-projects" | |||
className="overview-meta-size-ncloc is-half-width bordered-left"> | |||
<DrilldownLink component={this.props.component.key} metric="projects"> | |||
<DrilldownLink | |||
branch={getBranchName(this.props.branch)} | |||
component={this.props.component.key} | |||
metric="projects"> | |||
{formatMeasure(projects.value, 'SHORT_INT')} | |||
</DrilldownLink> | |||
<div className="overview-domain-measure-label text-muted"> |
@@ -35,12 +35,13 @@ function isProject(component /*: Component */) { | |||
/*:: | |||
type Props = { | |||
branch: { name: string }, | |||
component: Component, | |||
measures: MeasuresList | |||
}; | |||
*/ | |||
export default function QualityGate({ component, measures } /*: Props */) { | |||
export default function QualityGate({ branch, component, measures } /*: Props */) { | |||
const statusMeasure = measures.find(measure => measure.metric.key === 'alert_status'); | |||
const detailsMeasure = measures.find(measure => measure.metric.key === 'quality_gate_details'); | |||
@@ -63,7 +64,7 @@ export default function QualityGate({ component, measures } /*: Props */) { | |||
</h2> | |||
{conditions.length > 0 && | |||
<QualityGateConditions component={component} conditions={conditions} />} | |||
<QualityGateConditions branch={branch} component={component} conditions={conditions} />} | |||
</div> | |||
); | |||
} |
@@ -24,6 +24,7 @@ import { Link } from 'react-router'; | |||
import { DrilldownLink } from '../../../components/shared/drilldown-link'; | |||
import Measure from '../../../components/measure/Measure'; | |||
import IssueTypeIcon from '../../../components/ui/IssueTypeIcon'; | |||
import { getBranchName } from '../../../helpers/branches'; | |||
import { getPeriodValue, isDiffMetric, formatMeasure } from '../../../helpers/measures'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getComponentIssuesUrl } from '../../../helpers/urls'; | |||
@@ -32,6 +33,7 @@ import { getComponentIssuesUrl } from '../../../helpers/urls'; | |||
export default class QualityGateCondition extends React.PureComponent { | |||
/*:: props: { | |||
branch: { name: string }, | |||
component: Component, | |||
condition: { | |||
level: string, | |||
@@ -52,16 +54,17 @@ export default class QualityGateCondition extends React.PureComponent { | |||
} | |||
} | |||
getIssuesUrl(sinceLeakPeriod /*: boolean */, customQuery /*: {} */) { | |||
getIssuesUrl = (sinceLeakPeriod /*: boolean */, customQuery /*: {} */) => { | |||
const query /*: Object */ = { | |||
resolved: 'false', | |||
branch: getBranchName(this.props.branch), | |||
...customQuery | |||
}; | |||
if (sinceLeakPeriod) { | |||
Object.assign(query, { sinceLeakPeriod: 'true' }); | |||
} | |||
return getComponentIssuesUrl(this.props.component.key, query); | |||
} | |||
}; | |||
getUrlForCodeSmells(sinceLeakPeriod /*: boolean */) { | |||
return this.getIssuesUrl(sinceLeakPeriod, { types: 'CODE_SMELL' }); | |||
@@ -91,7 +94,7 @@ export default class QualityGateCondition extends React.PureComponent { | |||
} | |||
wrapWithLink(children /*: React.Element<*> */) { | |||
const { component, condition } = this.props; | |||
const { branch, component, condition } = this.props; | |||
const className = classNames( | |||
'overview-quality-gate-condition', | |||
@@ -115,6 +118,7 @@ export default class QualityGateCondition extends React.PureComponent { | |||
{children} | |||
</Link> | |||
: <DrilldownLink | |||
branch={getBranchName(branch)} | |||
className={className} | |||
component={component.key} | |||
metric={condition.measure.metric.key} |
@@ -22,6 +22,7 @@ import { sortBy } from 'lodash'; | |||
import QualityGateCondition from './QualityGateCondition'; | |||
import { ComponentType, ConditionsListType } from '../propTypes'; | |||
import { getMeasuresAndMeta } from '../../../api/measures'; | |||
import { getBranchName } from '../../../helpers/branches'; | |||
import { enhanceMeasuresWithMetrics } from '../../../helpers/measures'; | |||
const LEVEL_ORDER = ['ERROR', 'WARN']; | |||
@@ -35,6 +36,7 @@ function enhanceConditions(conditions, measures) { | |||
export default class QualityGateConditions extends React.PureComponent { | |||
static propTypes = { | |||
// branch | |||
component: ComponentType.isRequired, | |||
conditions: ConditionsListType.isRequired | |||
}; | |||
@@ -50,6 +52,7 @@ export default class QualityGateConditions extends React.PureComponent { | |||
componentDidUpdate(prevProps) { | |||
if ( | |||
prevProps.branch !== this.props.branch || | |||
prevProps.conditions !== this.props.conditions || | |||
prevProps.component !== this.props.component | |||
) { | |||
@@ -62,11 +65,14 @@ export default class QualityGateConditions extends React.PureComponent { | |||
} | |||
loadFailedMeasures() { | |||
const { component, conditions } = this.props; | |||
const { branch, component, conditions } = this.props; | |||
const failedConditions = conditions.filter(c => c.level !== 'OK'); | |||
if (failedConditions.length > 0) { | |||
const metrics = failedConditions.map(condition => condition.metric); | |||
getMeasuresAndMeta(component.key, metrics, { additionalFields: 'metrics' }).then(r => { | |||
getMeasuresAndMeta(component.key, metrics, { | |||
additionalFields: 'metrics', | |||
branch: getBranchName(branch) | |||
}).then(r => { | |||
if (this.mounted) { | |||
const measures = enhanceMeasuresWithMetrics(r.component.measures, r.metrics); | |||
this.setState({ | |||
@@ -81,7 +87,7 @@ export default class QualityGateConditions extends React.PureComponent { | |||
} | |||
render() { | |||
const { component } = this.props; | |||
const { branch, component } = this.props; | |||
const { loading, conditions } = this.state; | |||
if (loading) { | |||
@@ -101,6 +107,7 @@ export default class QualityGateConditions extends React.PureComponent { | |||
{sortedConditions.map(condition => | |||
<QualityGateCondition | |||
key={condition.measure.metric.key} | |||
branch={branch} | |||
component={component} | |||
condition={condition} | |||
/> |
@@ -56,7 +56,13 @@ it('open_issues', () => { | |||
op: 'GT' | |||
}; | |||
expect( | |||
shallow(<QualityGateCondition component={{ key: 'abcd-key' }} condition={condition} />) | |||
shallow( | |||
<QualityGateCondition | |||
branch={{ isMain: true }} | |||
component={{ key: 'abcd-key' }} | |||
condition={condition} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
@@ -79,28 +85,52 @@ it('new_open_issues', () => { | |||
period: 1 | |||
}; | |||
expect( | |||
shallow(<QualityGateCondition component={{ key: 'abcd-key' }} condition={condition} />) | |||
shallow( | |||
<QualityGateCondition | |||
branch={{ isMain: true }} | |||
component={{ key: 'abcd-key' }} | |||
condition={condition} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
it('reliability_rating', () => { | |||
const condition = mockRatingCondition('reliability_rating'); | |||
expect( | |||
shallow(<QualityGateCondition component={{ key: 'abcd-key' }} condition={condition} />) | |||
shallow( | |||
<QualityGateCondition | |||
branch={{ isMain: true }} | |||
component={{ key: 'abcd-key' }} | |||
condition={condition} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
it('security_rating', () => { | |||
const condition = mockRatingCondition('security_rating'); | |||
expect( | |||
shallow(<QualityGateCondition component={{ key: 'abcd-key' }} condition={condition} />) | |||
shallow( | |||
<QualityGateCondition | |||
branch={{ isMain: true }} | |||
component={{ key: 'abcd-key' }} | |||
condition={condition} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
it('sqale_rating', () => { | |||
const condition = mockRatingCondition('sqale_rating'); | |||
expect( | |||
shallow(<QualityGateCondition component={{ key: 'abcd-key' }} condition={condition} />) | |||
shallow( | |||
<QualityGateCondition | |||
branch={{ isMain: true }} | |||
component={{ key: 'abcd-key' }} | |||
condition={condition} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
@@ -109,7 +139,13 @@ it('new_reliability_rating', () => { | |||
condition.period = 1; | |||
condition.measure.periods = periods; | |||
expect( | |||
shallow(<QualityGateCondition component={{ key: 'abcd-key' }} condition={condition} />) | |||
shallow( | |||
<QualityGateCondition | |||
branch={{ isMain: true }} | |||
component={{ key: 'abcd-key' }} | |||
condition={condition} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
@@ -118,7 +154,13 @@ it('new_security_rating', () => { | |||
condition.period = 1; | |||
condition.measure.periods = periods; | |||
expect( | |||
shallow(<QualityGateCondition component={{ key: 'abcd-key' }} condition={condition} />) | |||
shallow( | |||
<QualityGateCondition | |||
branch={{ isMain: true }} | |||
component={{ key: 'abcd-key' }} | |||
condition={condition} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
@@ -127,14 +169,24 @@ it('new_maintainability_rating', () => { | |||
condition.period = 1; | |||
condition.measure.periods = periods; | |||
expect( | |||
shallow(<QualityGateCondition component={{ key: 'abcd-key' }} condition={condition} />) | |||
shallow( | |||
<QualityGateCondition | |||
branch={{ isMain: true }} | |||
component={{ key: 'abcd-key' }} | |||
condition={condition} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should be able to correctly decide how much decimals to show', () => { | |||
const condition = mockRatingCondition('new_maintainability_rating'); | |||
const instance = shallow( | |||
<QualityGateCondition component={{ key: 'abcd-key' }} condition={condition} /> | |||
<QualityGateCondition | |||
branch={{ isMain: true }} | |||
component={{ key: 'abcd-key' }} | |||
condition={condition} | |||
/> | |||
).instance(); | |||
expect(instance.getDecimalsNumber(85, 80)).toBe(undefined); | |||
expect(instance.getDecimalsNumber(85, 85)).toBe(undefined); | |||
@@ -144,3 +196,16 @@ it('should be able to correctly decide how much decimals to show', () => { | |||
expect(instance.getDecimalsNumber(85, 85.0000000000000954)).toBe('00000000000009'.length); | |||
expect(instance.getDecimalsNumber(85, 85.00000000000000009)).toBe(undefined); | |||
}); | |||
it('should work with branch', () => { | |||
const condition = mockRatingCondition('new_maintainability_rating'); | |||
expect( | |||
shallow( | |||
<QualityGateCondition | |||
branch={{ isMain: false, name: 'feature' }} | |||
component={{ key: 'abcd-key' }} | |||
condition={condition} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); |
@@ -9,6 +9,7 @@ exports[`renders 1`] = ` | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "foo", | |||
}, | |||
} |
@@ -9,6 +9,7 @@ exports[`new_maintainability_rating 1`] = ` | |||
Object { | |||
"pathname": "/project/issues", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "abcd-key", | |||
"resolved": "false", | |||
"sinceLeakPeriod": "true", | |||
@@ -131,6 +132,7 @@ exports[`new_reliability_rating 1`] = ` | |||
Object { | |||
"pathname": "/project/issues", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "abcd-key", | |||
"resolved": "false", | |||
"severities": "BLOCKER,CRITICAL,MAJOR,MINOR", | |||
@@ -198,6 +200,7 @@ exports[`new_security_rating 1`] = ` | |||
Object { | |||
"pathname": "/project/issues", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "abcd-key", | |||
"resolved": "false", | |||
"severities": "BLOCKER,CRITICAL,MAJOR,MINOR", | |||
@@ -315,6 +318,7 @@ exports[`reliability_rating 1`] = ` | |||
Object { | |||
"pathname": "/project/issues", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "abcd-key", | |||
"resolved": "false", | |||
"severities": "BLOCKER,CRITICAL,MAJOR,MINOR", | |||
@@ -375,6 +379,7 @@ exports[`security_rating 1`] = ` | |||
Object { | |||
"pathname": "/project/issues", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "abcd-key", | |||
"resolved": "false", | |||
"severities": "BLOCKER,CRITICAL,MAJOR,MINOR", | |||
@@ -426,6 +431,67 @@ exports[`security_rating 1`] = ` | |||
</Link> | |||
`; | |||
exports[`should work with branch 1`] = ` | |||
<Link | |||
className="overview-quality-gate-condition overview-quality-gate-condition-error" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/project/issues", | |||
"query": Object { | |||
"branch": "feature", | |||
"id": "abcd-key", | |||
"resolved": "false", | |||
"sinceLeakPeriod": "true", | |||
"types": "CODE_SMELL", | |||
}, | |||
} | |||
} | |||
> | |||
<div | |||
className="overview-quality-gate-condition-container" | |||
> | |||
<div | |||
className="overview-quality-gate-condition-value" | |||
> | |||
<Measure | |||
decimals={null} | |||
measure={ | |||
Object { | |||
"leak": "3", | |||
"metric": Object { | |||
"key": "new_maintainability_rating", | |||
"name": "new_maintainability_rating", | |||
"type": "RATING", | |||
}, | |||
"value": "3", | |||
} | |||
} | |||
/> | |||
</div> | |||
<div> | |||
<div | |||
className="overview-quality-gate-condition-metric" | |||
> | |||
<IssueTypeIcon | |||
className="little-spacer-right" | |||
query="new_maintainability_rating" | |||
/> | |||
new_maintainability_rating | |||
</div> | |||
<div | |||
className="overview-quality-gate-threshold" | |||
> | |||
quality_gates.operator.GT.rating | |||
A | |||
</div> | |||
</div> | |||
</div> | |||
</Link> | |||
`; | |||
exports[`sqale_rating 1`] = ` | |||
<Link | |||
className="overview-quality-gate-condition overview-quality-gate-condition-error" | |||
@@ -435,6 +501,7 @@ exports[`sqale_rating 1`] = ` | |||
Object { | |||
"pathname": "/project/issues", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "abcd-key", | |||
"resolved": "false", | |||
"types": "CODE_SMELL", |
@@ -26,6 +26,7 @@ import { getAllTimeMachineData } from '../../../api/time-machine'; | |||
import { getMetrics } from '../../../api/metrics'; | |||
import * as api from '../../../api/projectActivity'; | |||
import * as actions from '../actions'; | |||
import { getBranchName } from '../../../helpers/branches'; | |||
import { parseDate } from '../../../helpers/dates'; | |||
import { getCustomGraph, getGraph } from '../../../helpers/storage'; | |||
import { | |||
@@ -42,6 +43,7 @@ import { | |||
/*:: | |||
type Props = { | |||
branch: {}, | |||
location: { pathname: string, query: RawQuery }, | |||
component: { | |||
configuration?: { showHistory: boolean }, | |||
@@ -93,7 +95,7 @@ export default class ProjectActivityAppContainer extends React.PureComponent { | |||
} | |||
this.context.router.replace({ | |||
pathname: props.location.pathname, | |||
query: serializeUrlQuery(newQuery) | |||
query: { ...serializeUrlQuery(newQuery), branch: getBranchName(props.branch) } | |||
}); | |||
} | |||
} | |||
@@ -167,7 +169,7 @@ export default class ProjectActivityAppContainer extends React.PureComponent { | |||
[string]: string | |||
} */ | |||
) => { | |||
const parameters = { project, p, ps }; | |||
const parameters = { project, p, ps, branch: getBranchName(this.props.branch) }; | |||
return api | |||
.getProjectActivity({ ...parameters, ...additional }) | |||
.then(({ analyses, paging }) => ({ | |||
@@ -180,7 +182,9 @@ export default class ProjectActivityAppContainer extends React.PureComponent { | |||
if (metrics.length <= 0) { | |||
return Promise.resolve([]); | |||
} | |||
return getAllTimeMachineData(this.props.component.key, metrics).then( | |||
return getAllTimeMachineData(this.props.component.key, metrics, { | |||
branch: getBranchName(this.props.branch) | |||
}).then( | |||
({ measures }) => | |||
measures.map(measure => ({ | |||
metric: measure.metric, | |||
@@ -281,6 +285,7 @@ export default class ProjectActivityAppContainer extends React.PureComponent { | |||
pathname: this.props.location.pathname, | |||
query: { | |||
...query, | |||
branch: getBranchName(this.props.branch), | |||
id: this.props.component.key | |||
} | |||
}); |
@@ -63,6 +63,7 @@ const DEFAULT_PROPS = { | |||
addVersion: () => {}, | |||
analyses: ANALYSES, | |||
analysesLoading: false, | |||
branch: { isMain: true }, | |||
changeEvent: () => {}, | |||
deleteAnalysis: () => {}, | |||
deleteEvent: () => {}, |
@@ -419,7 +419,7 @@ export default class SourceViewerBase extends React.PureComponent { | |||
}; | |||
loadDuplications = (line /*: SourceLine */) => { | |||
getDuplications(this.props.component).then(r => { | |||
getDuplications(this.props.component, this.props.branch).then(r => { | |||
if (this.mounted) { | |||
this.setState( | |||
{ | |||
@@ -440,13 +440,22 @@ export default class SourceViewerBase extends React.PureComponent { | |||
}; | |||
showMeasures = () => { | |||
const measuresOverlay = new MeasuresOverlay({ component: this.state.component, large: true }); | |||
const measuresOverlay = new MeasuresOverlay({ | |||
branch: this.props.branch, | |||
component: this.state.component, | |||
large: true | |||
}); | |||
measuresOverlay.render(); | |||
}; | |||
handleCoverageClick = (line /*: SourceLine */, element /*: HTMLElement */) => { | |||
getTests(this.props.component, line.line).then(tests => { | |||
const popup = new CoveragePopupView({ line, tests, triggerEl: element }); | |||
getTests(this.props.component, line.line, this.props.branch).then(tests => { | |||
const popup = new CoveragePopupView({ | |||
line, | |||
tests, | |||
triggerEl: element, | |||
branch: this.props.branch | |||
}); | |||
popup.render(); | |||
}); | |||
}; | |||
@@ -477,7 +486,8 @@ export default class SourceViewerBase extends React.PureComponent { | |||
inRemovedComponent, | |||
component: this.state.component, | |||
files: this.state.duplicatedFiles, | |||
triggerEl: element | |||
triggerEl: element, | |||
branch: this.props.branch | |||
}); | |||
popup.render(); | |||
} | |||
@@ -500,7 +510,8 @@ export default class SourceViewerBase extends React.PureComponent { | |||
const popup = new LineActionsPopupView({ | |||
line, | |||
triggerEl: element, | |||
component: this.state.component | |||
component: this.state.component, | |||
branch: this.props.branch | |||
}); | |||
popup.render(); | |||
} | |||
@@ -637,7 +648,11 @@ export default class SourceViewerBase extends React.PureComponent { | |||
return ( | |||
<div className={className} ref={node => (this.node = node)}> | |||
<SourceViewerHeader component={this.state.component} showMeasures={this.showMeasures} /> | |||
<SourceViewerHeader | |||
branch={this.props.branch} | |||
component={this.state.component} | |||
showMeasures={this.showMeasures} | |||
/> | |||
{notAccessible && | |||
<div className="alert alert-warning spacer-top"> | |||
{translate('code_viewer.no_source_code_displayed_due_to_security')} |
@@ -29,6 +29,7 @@ import { formatMeasure } from '../../helpers/measures'; | |||
export default class SourceViewerHeader extends React.PureComponent { | |||
/*:: props: { | |||
branch?: string, | |||
component: { | |||
canMarkAsFavorite: boolean, | |||
key: string, | |||
@@ -60,7 +61,7 @@ export default class SourceViewerHeader extends React.PureComponent { | |||
e.preventDefault(); | |||
const { key } = this.props.component; | |||
const Workspace = require('../workspace/main').default; | |||
Workspace.openComponent({ key }); | |||
Workspace.openComponent({ key, branch: this.props.branch }); | |||
}; | |||
render() { | |||
@@ -78,8 +79,11 @@ export default class SourceViewerHeader extends React.PureComponent { | |||
const isUnitTest = q === 'UTS'; | |||
// TODO check if source viewer is displayed inside workspace | |||
const workspace = false; | |||
const rawSourcesLink = | |||
let rawSourcesLink = | |||
window.baseUrl + `/api/sources/raw?key=${encodeURIComponent(this.props.component.key)}`; | |||
if (this.props.branch) { | |||
rawSourcesLink += `&branch=${encodeURIComponent(this.props.branch)}`; | |||
} | |||
// TODO favorite | |||
return ( | |||
@@ -87,14 +91,14 @@ export default class SourceViewerHeader extends React.PureComponent { | |||
<div className="source-viewer-header-component"> | |||
<div className="component-name"> | |||
<div className="component-name-parent"> | |||
<Link to={getProjectUrl(project)} className="link-with-icon"> | |||
<Link to={getProjectUrl(project, this.props.branch)} className="link-with-icon"> | |||
<QualifierIcon qualifier="TRK" /> <span>{projectName}</span> | |||
</Link> | |||
</div> | |||
{subProject != null && | |||
<div className="component-name-parent"> | |||
<Link to={getProjectUrl(subProject)} className="link-with-icon"> | |||
<Link to={getProjectUrl(subProject, this.props.branch)} className="link-with-icon"> | |||
<QualifierIcon qualifier="BRC" /> <span>{subProjectName}</span> | |||
</Link> | |||
</div>} | |||
@@ -124,7 +128,10 @@ export default class SourceViewerHeader extends React.PureComponent { | |||
<Link | |||
className="js-new-window" | |||
target="_blank" | |||
to={{ pathname: '/component', query: { id: this.props.component.key } }}> | |||
to={{ | |||
pathname: '/component', | |||
query: { branch: this.props.branch, id: this.props.component.key } | |||
}}> | |||
{translate('component_viewer.new_window')} | |||
</Link> | |||
</li> | |||
@@ -166,7 +173,11 @@ export default class SourceViewerHeader extends React.PureComponent { | |||
<div className="source-viewer-header-measure"> | |||
<span className="source-viewer-header-measure-value"> | |||
<Link | |||
to={getComponentIssuesUrl(project, { resolved: 'false', fileUuids: uuid })} | |||
to={getComponentIssuesUrl(project, { | |||
resolved: 'false', | |||
fileUuids: uuid, | |||
branch: this.props.branch | |||
})} | |||
className="source-viewer-header-external-link" | |||
target="_blank"> | |||
{measures.issues != null ? formatMeasure(measures.issues, 'SHORT_INT') : 0}{' '} |
@@ -38,7 +38,7 @@ export default Popup.extend({ | |||
e.stopPropagation(); | |||
const key = $(e.currentTarget).data('key'); | |||
const Workspace = require('../../workspace/main').default; | |||
Workspace.openComponent({ key }); | |||
Workspace.openComponent({ key, branch: this.options.branch }); | |||
}, | |||
serializeData() { |
@@ -34,7 +34,7 @@ export default Popup.extend({ | |||
const key = $(e.currentTarget).data('key'); | |||
const line = $(e.currentTarget).data('line'); | |||
const Workspace = require('../../workspace/main').default; | |||
Workspace.openComponent({ key, line }); | |||
Workspace.openComponent({ key, line, branch: this.options.branch }); | |||
}, | |||
serializeData() { |
@@ -24,9 +24,12 @@ export default Popup.extend({ | |||
template: Template, | |||
serializeData() { | |||
const { component, line } = this.options; | |||
return { | |||
permalink: window.baseUrl + `/component?id=${encodeURIComponent(component.key)}&line=${line}` | |||
}; | |||
const { component, line, branch } = this.options; | |||
let permalink = | |||
window.baseUrl + `/component?id=${encodeURIComponent(component.key)}&line=${line}`; | |||
if (branch) { | |||
permalink += `&branch=${encodeURIComponent(branch)}`; | |||
} | |||
return { permalink }; | |||
} | |||
}); |
@@ -141,7 +141,11 @@ export default ModalView.extend({ | |||
.filter(metric => metric.type !== 'DATA' && !metric.hidden) | |||
.map(metric => metric.key); | |||
return getMeasures(this.options.component.key, metricsToRequest).then(measures => { | |||
return getMeasures( | |||
this.options.component.key, | |||
metricsToRequest, | |||
this.options.branch | |||
).then(measures => { | |||
let nextMeasures = this.options.component.measures || {}; | |||
measures.forEach(measure => { | |||
const metric = metrics.find(metric => metric.key === measure.metric); | |||
@@ -160,6 +164,7 @@ export default ModalView.extend({ | |||
return new Promise(resolve => { | |||
const url = window.baseUrl + '/api/issues/search'; | |||
const options = { | |||
branch: this.options.branch, | |||
componentKeys: this.options.component.key, | |||
resolved: false, | |||
ps: 1, | |||
@@ -191,7 +196,7 @@ export default ModalView.extend({ | |||
requestTests() { | |||
return new Promise(resolve => { | |||
const url = window.baseUrl + '/api/tests/list'; | |||
const options = { testFileKey: this.options.component.key }; | |||
const options = { branch: this.options.branch, testFileKey: this.options.component.key }; | |||
$.get(url, options).done(data => { | |||
this.tests = data.tests; |
@@ -18,28 +18,21 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import ShortLivingBranchIcon from './ShortLivingBranchIcon'; | |||
import LongLivingBranchIcon from './LongLivingBranchIcon'; | |||
// import PullRequestIcon from './PullRequestIcon'; | |||
import { Branch } from '../../app/types'; | |||
import { isShortLivingBranch } from '../../helpers/branches'; | |||
interface Props { | |||
branch: Branch; | |||
className?: string; | |||
color?: string; | |||
size?: number; | |||
} | |||
export default function BranchIcon({ className, color = '#4b9fd5', size = 14 }: Props) { | |||
/* eslint-disable max-len */ | |||
return ( | |||
<svg | |||
xmlns="http://www.w3.org/2000/svg" | |||
className={className} | |||
height={size} | |||
width={size} | |||
viewBox="0 0 16 16"> | |||
<g transform="matrix(0.0416667,0,0,0.0416667,2.98284,-1.32102)"> | |||
<path | |||
d="M72,368C72,361.333 69.667,355.667 65,351C60.333,346.333 54.667,344 48,344C41.333,344 35.667,346.333 31,351C26.333,355.667 24,361.333 24,368C24,374.667 26.333,380.333 31,385C35.667,389.667 41.333,392 48,392C54.667,392 60.333,389.667 65,385C69.667,380.333 72,374.667 72,368ZM72,80C72,73.333 69.667,67.667 65,63C60.333,58.333 54.667,56 48,56C41.333,56 35.667,58.333 31,63C26.333,67.667 24,73.333 24,80C24,86.667 26.333,92.333 31,97C35.667,101.667 41.333,104 48,104C54.667,104 60.333,101.667 65,97C69.667,92.333 72,86.667 72,80ZM232,112C232,105.333 229.667,99.667 225,95C220.333,90.333 214.667,88 208,88C201.333,88 195.667,90.333 191,95C186.333,99.667 184,105.333 184,112C184,118.667 186.333,124.333 191,129C195.667,133.667 201.333,136 208,136C214.667,136 220.333,133.667 225,129C229.667,124.333 232,118.667 232,112ZM256,112C256,120.667 253.833,128.708 249.5,136.125C245.167,143.542 239.333,149.333 232,153.5C231.667,201.333 212.833,235.833 175.5,257C164.167,263.333 147.25,270.083 124.75,277.25C103.417,283.917 89.292,289.833 82.375,295C75.458,300.167 72,308.5 72,320L72,326.5C79.333,330.667 85.167,336.458 89.5,343.875C93.833,351.292 96,359.333 96,368C96,381.333 91.333,392.667 82,402C72.667,411.333 61.333,416 48,416C34.667,416 23.333,411.333 14,402C4.667,392.667 0,381.333 0,368C0,359.333 2.167,351.292 6.5,343.875C10.833,336.458 16.667,330.667 24,326.5L24,121.5C16.667,117.333 10.833,111.542 6.5,104.125C2.167,96.708 0,88.667 0,80C0,66.667 4.667,55.333 14,46C23.333,36.667 34.667,32 48,32C61.333,32 72.667,36.667 82,46C91.333,55.333 96,66.667 96,80C96,88.667 93.833,96.708 89.5,104.125C85.167,111.542 79.333,117.333 72,121.5L72,245.75C81,241.417 93.833,236.667 110.5,231.5C119.667,228.667 126.958,226.208 132.375,224.125C137.792,222.042 143.667,219.458 150,216.375C156.333,213.292 161.25,210 164.75,206.5C168.25,203 171.625,198.75 174.875,193.75C178.125,188.75 180.458,182.958 181.875,176.375C183.292,169.792 184,162.167 184,153.5C176.667,149.333 170.833,143.542 166.5,136.125C162.167,128.708 160,120.667 160,112C160,98.667 164.667,87.333 174,78C183.333,68.667 194.667,64 208,64C221.333,64 232.667,68.667 242,78C251.333,87.333 256,98.667 256,112Z" | |||
style={{ fill: color, fillRule: 'nonzero' }} | |||
/> | |||
</g> | |||
</svg> | |||
); | |||
export default function BranchIcon({ branch, ...props }: Props) { | |||
return isShortLivingBranch(branch) | |||
? <ShortLivingBranchIcon {...props} /> | |||
: <LongLivingBranchIcon {...props} />; | |||
} |
@@ -0,0 +1,45 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 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'; | |||
interface Props { | |||
className?: string; | |||
color?: string; | |||
size?: number; | |||
} | |||
export default function LongLivingBranchIcon({ className, color = '#4b9fd5', size = 16 }: Props) { | |||
/* eslint-disable max-len */ | |||
return ( | |||
<svg | |||
xmlns="http://www.w3.org/2000/svg" | |||
className={className} | |||
height={size} | |||
width={size} | |||
viewBox="0 0 16 16"> | |||
<g transform="translate(5, 0)"> | |||
<path | |||
style={{ fill: color }} | |||
d="M4.5 8c0-.9-.6-1.7-1.5-1.9V4c.9-.2 1.5-1 1.5-1.9 0-1.1-.9-2-2-2s-2 .9-2 2C.5 3 1.1 3.8 2 4v2.1C1.1 6.3.5 7.1.5 8s.6 1.7 1.5 2v2.1c-.9.2-1.5 1-1.5 1.9 0 1.1.9 2 2 2s2-.9 2-2c0-.9-.6-1.7-1.5-1.9V10c.9-.3 1.5-1 1.5-2zm-3-5.9c0-.6.4-1 1-1s1 .4 1 1-.4 1-1 1-1-.5-1-1zm0 5.9c0-.6.4-1 1-1s1 .4 1 1-.4 1-1 1-1-.4-1-1zm2 6c0 .6-.4 1-1 1s-1-.4-1-1 .4-1 1-1 1 .5 1 1z" | |||
/> | |||
</g> | |||
</svg> | |||
); | |||
} |
@@ -0,0 +1,43 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 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'; | |||
interface Props { | |||
className?: string; | |||
color?: string; | |||
size?: number; | |||
} | |||
export default function PullRequestIcon({ className, color = '#4b9fd5', size = 16 }: Props) { | |||
/* eslint-disable max-len */ | |||
return ( | |||
<svg | |||
xmlns="http://www.w3.org/2000/svg" | |||
className={className} | |||
height={size} | |||
width={size} | |||
viewBox="0 0 16 16"> | |||
<path | |||
style={{ fill: color }} | |||
d="M3 11.9V4.1c.9-.2 1.5-1 1.5-1.9 0-1.1-.9-2-2-2s-2 .9-2 2c0 .9.6 1.7 1.5 1.9v7.8c-.9.2-1.5 1-1.5 1.9 0 1.1.9 2 2 2s2-.9 2-2c0-.9-.6-1.6-1.5-1.9zM1.5 2.2c0-.6.4-1 1-1s1 .4 1 1-.4 1-1 1-1-.4-1-1zm1 12.7c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1-.4 1-1 1zM14 11.9V5.5c0-.1-.2-3.1-5.1-3.5L10.1.8 9.5.1 6.9 2.6l2.6 2.5.7-.7L8.8 3c4 .2 4.2 2.4 4.2 2.5v6.4c-.9.2-1.5 1-1.5 1.9 0 1.1.9 2 2 2s2-.9 2-2c0-.9-.6-1.6-1.5-1.9zm-.5 3c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1-.4 1-1 1z" | |||
/> | |||
</svg> | |||
); | |||
} |
@@ -0,0 +1,45 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 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'; | |||
interface Props { | |||
className?: string; | |||
color?: string; | |||
size?: number; | |||
} | |||
export default function ShortLivingBranchIcon({ className, color = '#4b9fd5', size = 16 }: Props) { | |||
/* eslint-disable max-len */ | |||
return ( | |||
<svg | |||
xmlns="http://www.w3.org/2000/svg" | |||
className={className} | |||
height={size} | |||
width={size} | |||
viewBox="0 0 16 16"> | |||
<g transform="translate(3, 0)"> | |||
<path | |||
style={{ fill: color }} | |||
d="M9.5 6.5c0-1.1-.9-2-2-2s-2 .9-2 2c0 .8.5 1.5 1.2 1.8-.3.6-.7 1.1-1.2 1.4-.9.5-1.9.5-2.5.4V4c.9-.2 1.5-1 1.5-1.9 0-1.1-.9-2-2-2s-2 .9-2 2C.5 3 1.1 3.8 2 4v8c-.9.2-1.5 1-1.5 1.9 0 1.1.9 2 2 2s2-.9 2-2c0-.9-.6-1.7-1.5-1.9v-1c.2 0 .5.1.7.1.7 0 1.5-.1 2.2-.6.8-.5 1.4-1.2 1.7-2.1 1.1 0 1.9-.9 1.9-1.9zm-8-4.4c0-.6.4-1 1-1s1 .4 1 1-.4 1-1 1-1-.5-1-1zm2 11.9c0 .6-.4 1-1 1s-1-.4-1-1 .4-1 1-1 1 .4 1 1zm4-6.5c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1-.4 1-1 1z" | |||
/> | |||
</g> | |||
</svg> | |||
); | |||
} |
@@ -49,6 +49,7 @@ const ISSUE_MEASURES = [ | |||
export class DrilldownLink extends React.PureComponent { | |||
static propTypes = { | |||
branch: PropTypes.string, | |||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]), | |||
className: PropTypes.string, | |||
component: PropTypes.string.isRequired, | |||
@@ -118,7 +119,10 @@ export class DrilldownLink extends React.PureComponent { | |||
}; | |||
renderIssuesLink = () => { | |||
const url = getComponentIssuesUrl(this.props.component, this.propsToIssueParams()); | |||
const url = getComponentIssuesUrl(this.props.component, { | |||
...this.propsToIssueParams(), | |||
branch: this.props.branch | |||
}); | |||
return ( | |||
<Link to={url} className={this.props.className}> | |||
@@ -132,7 +136,11 @@ export class DrilldownLink extends React.PureComponent { | |||
return this.renderIssuesLink(); | |||
} | |||
const url = getComponentDrilldownUrl(this.props.component, this.props.metric); | |||
const url = getComponentDrilldownUrl( | |||
this.props.component, | |||
this.props.metric, | |||
this.props.branch | |||
); | |||
return ( | |||
<Link to={url} className={this.props.className}> | |||
{this.props.children} |
@@ -0,0 +1,71 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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 { sortBranchesAsTree } from '../branches'; | |||
import { MainBranch, BranchType, ShortLivingBranch, LongLivingBranch } from '../../app/types'; | |||
describe('#sortBranchesAsTree', () => { | |||
it('sorts main branch and short-living branches', () => { | |||
const main = mainBranch(); | |||
const foo = shortLivingBranch('foo', 'master'); | |||
const bar = shortLivingBranch('bar', 'master'); | |||
expect(sortBranchesAsTree([main, foo, bar])).toEqual([main, bar, foo]); | |||
}); | |||
it('sorts main branch and long-living branches', () => { | |||
const main = mainBranch(); | |||
const foo = longLivingBranch('foo'); | |||
const bar = longLivingBranch('bar'); | |||
expect(sortBranchesAsTree([main, foo, bar])).toEqual([main, bar, foo]); | |||
}); | |||
it('sorts all types of branches', () => { | |||
const main = mainBranch(); | |||
const shortFoo = shortLivingBranch('shortFoo', 'master'); | |||
const shortBar = shortLivingBranch('shortBar', 'longBaz'); | |||
const shortPre = shortLivingBranch('shortPre', 'shortFoo'); | |||
const longBaz = longLivingBranch('longBaz'); | |||
const longQux = longLivingBranch('longQux'); | |||
const longQwe = longLivingBranch('longQwe'); | |||
// - main - main | |||
// - shortFoo - shortFoo | |||
// - shortPre - shortPre | |||
// - longBaz ----> - longBaz | |||
// - shortBar - shortBar | |||
// - longQwe - longQwe | |||
// - longQux - longQux | |||
expect( | |||
sortBranchesAsTree([main, shortFoo, shortBar, shortPre, longBaz, longQux, longQwe]) | |||
).toEqual([main, shortFoo, shortPre, longBaz, shortBar, longQux, longQwe]); | |||
}); | |||
}); | |||
function mainBranch(): MainBranch { | |||
return { isMain: true, name: 'master' }; | |||
} | |||
function shortLivingBranch(name: string, mergeBranch: string): ShortLivingBranch { | |||
const status = { bugs: 0, codeSmells: 0, vulnerabilities: 0 }; | |||
return { isMain: false, mergeBranch, name, status, type: BranchType.SHORT }; | |||
} | |||
function longLivingBranch(name: string): LongLivingBranch { | |||
const status = { qualityGateStatus: 'OK' }; | |||
return { isMain: false, name, status, type: BranchType.LONG }; | |||
} |
@@ -17,20 +17,55 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { Branch, BranchType, ShortLivingBranch } from '../app/types'; | |||
import { sortBy } from 'lodash'; | |||
import { Branch, BranchType, ShortLivingBranch, LongLivingBranch } from '../app/types'; | |||
export const MAIN_BRANCH: Branch = { | |||
isMain: true, | |||
name: undefined, | |||
type: BranchType.LONG | |||
}; | |||
export function isShortLivingBranch(branch: Branch | null): branch is ShortLivingBranch { | |||
return branch != null && !branch.isMain && branch.type === BranchType.SHORT; | |||
} | |||
const MAIN_BRANCH_DISPLAY_NAME = 'master'; | |||
export function isLongLivingBranch(branch: Branch | null): branch is LongLivingBranch { | |||
return branch != null && !branch.isMain && branch.type === BranchType.LONG; | |||
} | |||
export function isShortLivingBranch(branch: Branch | null): branch is ShortLivingBranch { | |||
return branch != null && branch.type === BranchType.SHORT; | |||
export function getBranchName(branch: Branch): string | undefined { | |||
return branch.isMain ? undefined : branch.name; | |||
} | |||
export function getBranchDisplayName(branch: Branch): string { | |||
return branch.isMain ? MAIN_BRANCH_DISPLAY_NAME : branch.name; | |||
export function sortBranchesAsTree(branches: Branch[]): Branch[] { | |||
const result: Branch[] = []; | |||
const shortLivingBranches = branches.filter(isShortLivingBranch); | |||
// main branch is always first | |||
const mainBranch = branches.find(branch => branch.isMain); | |||
if (mainBranch) { | |||
result.push(mainBranch, ...getNestedShortLivingBranches(mainBranch.name)); | |||
} | |||
// the all long-living branches | |||
sortBy(branches.filter(isLongLivingBranch), 'name').forEach(longLivingBranch => { | |||
result.push(longLivingBranch, ...getNestedShortLivingBranches(longLivingBranch.name)); | |||
}); | |||
// finally all orhpan branches | |||
result.push(...shortLivingBranches.filter(branch => branch.isOrphan)); | |||
return result; | |||
/** Get all short-living branches (possibly nested) which should be merged to a given branch */ | |||
function getNestedShortLivingBranches(mergeBranch: string): ShortLivingBranch[] { | |||
const found: ShortLivingBranch[] = shortLivingBranches.filter( | |||
branch => branch.mergeBranch === mergeBranch | |||
); | |||
let i = 0; | |||
while (i < found.length) { | |||
const current = found[i]; | |||
found.push(...shortLivingBranches.filter(branch => branch.mergeBranch === current.name)); | |||
i++; | |||
} | |||
return sortBy(found, 'name'); | |||
} | |||
} |
@@ -63,12 +63,14 @@ export function elementKeydown(element: ShallowWrapper, keyCode: number): void { | |||
}); | |||
} | |||
export function doAsync(fn: Function): Promise<void> { | |||
export function doAsync(fn?: Function): Promise<void> { | |||
return new Promise(resolve => { | |||
setTimeout(() => { | |||
fn(); | |||
setImmediate(() => { | |||
if (fn) { | |||
fn(); | |||
} | |||
resolve(); | |||
}, 0); | |||
}); | |||
}); | |||
} | |||
@@ -23,7 +23,7 @@ import { getProfilePath } from '../apps/quality-profiles/utils'; | |||
import { Branch } from '../app/types'; | |||
interface Query { | |||
[x: string]: string; | |||
[x: string]: string | undefined; | |||
} | |||
interface Location { | |||
@@ -34,12 +34,15 @@ interface Location { | |||
/** | |||
* Generate URL for a component's home page | |||
*/ | |||
export function getComponentUrl(componentKey: string): string { | |||
return (window as any).baseUrl + '/dashboard?id=' + encodeURIComponent(componentKey); | |||
export function getComponentUrl(componentKey: string, branch?: string): string { | |||
const branchQuery = branch ? `&branch=${encodeURIComponent(branch)}` : ''; | |||
return ( | |||
(window as any).baseUrl + '/dashboard?id=' + encodeURIComponent(componentKey) + branchQuery | |||
); | |||
} | |||
export function getProjectUrl(key: string): Location { | |||
return { pathname: '/dashboard', query: { id: key } }; | |||
export function getProjectUrl(key: string, branch?: string): Location { | |||
return { pathname: '/dashboard', query: { id: key, branch } }; | |||
} | |||
export function getProjectBranchUrl(key: string, branch: Branch) { | |||
@@ -48,6 +51,8 @@ export function getProjectBranchUrl(key: string, branch: Branch) { | |||
pathname: '/project/issues', | |||
query: { branch: branch.name, id: key, resolved: 'false' } | |||
}; | |||
} else if (!branch.isMain) { | |||
return { pathname: '/dashboard', query: { branch: branch.name, id: key } }; | |||
} else { | |||
return { pathname: '/dashboard', query: { id: key } }; | |||
} | |||
@@ -75,17 +80,21 @@ export function getComponentIssuesUrlAsString(componentKey: string, query?: Quer | |||
/** | |||
* Generate URL for a component's drilldown page | |||
*/ | |||
export function getComponentDrilldownUrl(componentKey: string, metric: string): Location { | |||
return { pathname: '/component_measures', query: { id: componentKey, metric } }; | |||
export function getComponentDrilldownUrl(componentKey: string, metric: string, branch?: string) { | |||
return { pathname: '/component_measures', query: { id: componentKey, metric, branch } }; | |||
} | |||
/** | |||
* Generate URL for a component's measure history | |||
*/ | |||
export function getComponentMeasureHistory(componentKey: string, metric: string): Location { | |||
export function getComponentMeasureHistory( | |||
componentKey: string, | |||
metric: string, | |||
branch?: string | |||
): Location { | |||
return { | |||
pathname: '/project/activity', | |||
query: { id: componentKey, graph: 'custom', custom_metrics: metric } | |||
query: { id: componentKey, graph: 'custom', custom_metrics: metric, branch } | |||
}; | |||
} | |||
@@ -988,7 +988,8 @@ dependencies.not_used=Not used | |||
#------------------------------------------------------------------------------ | |||
dashboard.no_dashboard=No dashboard | |||
dashboard.project_not_found=The requested project does not exist. Either it has never been analyzed successfully or it has been deleted. | |||
dashboard.project_not_found=The requested project does not exist. | |||
dashboard.project_not_found.2=Either it has never been analyzed successfully or it has been deleted. | |||
#------------------------------------------------------------------------------ |