diff options
author | Stas Vilchik <stas.vilchik@sonarsource.com> | 2017-08-24 11:53:47 +0200 |
---|---|---|
committer | Janos Gyerik <janos.gyerik@sonarsource.com> | 2017-09-12 11:34:49 +0200 |
commit | 404d315b077c84da3c31b0c0f4dde852d918c8d1 (patch) | |
tree | 176840ffdfb472a469848539dfbb803372c94b93 /server/sonar-web | |
parent | 9f5272cac177f024354dd72da2cda2160c5440af (diff) | |
download | sonarqube-404d315b077c84da3c31b0c0f4dde852d918c8d1.tar.gz sonarqube-404d315b077c84da3c31b0c0f4dde852d918c8d1.zip |
SONAR-9736 Build UI for long-living branches (#2390)
Diffstat (limited to 'server/sonar-web')
95 files changed, 2093 insertions, 753 deletions
diff --git a/server/sonar-web/src/main/js/api/branches.ts b/server/sonar-web/src/main/js/api/branches.ts index 5c597385c85..6435c575aae 100644 --- a/server/sonar-web/src/main/js/api/branches.ts +++ b/server/sonar-web/src/main/js/api/branches.ts @@ -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 - ); -} diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts index fd46c34d559..361713b815d 100644 --- a/server/sonar-web/src/main/js/api/components.ts +++ b/server/sonar-web/src/main/js/api/components.ts @@ -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); } diff --git a/server/sonar-web/src/main/js/api/measures.ts b/server/sonar-web/src/main/js/api/measures.ts index 5f6ff93f40f..fd4f5259d2f 100644 --- a/server/sonar-web/src/main/js/api/measures.ts +++ b/server/sonar-web/src/main/js/api/measures.ts @@ -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); } diff --git a/server/sonar-web/src/main/js/api/projectActivity.ts b/server/sonar-web/src/main/js/api/projectActivity.ts index b2dddd8c890..a01fd36dec3 100644 --- a/server/sonar-web/src/main/js/api/projectActivity.ts +++ b/server/sonar-web/src/main/js/api/projectActivity.ts @@ -30,6 +30,7 @@ interface GetProjectActivityResponse { } export function getProjectActivity(data: { + branch?: string; project: string; category?: string; p?: number; diff --git a/server/sonar-web/src/main/js/app/components/ProjectContainer.tsx b/server/sonar-web/src/main/js/app/components/ProjectContainer.tsx index ef19629d7e3..f7ae6453557 100644 --- a/server/sonar-web/src/main/js/app/components/ProjectContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/ProjectContainer.tsx @@ -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} diff --git a/server/sonar-web/src/main/js/app/components/ProjectContainerNotFound.tsx b/server/sonar-web/src/main/js/app/components/ProjectContainerNotFound.tsx new file mode 100644 index 00000000000..1b5c4daadab --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/ProjectContainerNotFound.tsx @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx index 5400c85f212..beb6580feb8 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx @@ -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'); + }); +}); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.css b/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.css index b52fc694772..74278d67573 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.css +++ b/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.css @@ -1,4 +1,6 @@ .branch-status { + min-width: 64px; + text-align: right; } .branch-status-indicator { diff --git a/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.tsx b/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.tsx index bac5d7e295f..9a7937deaba 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.tsx @@ -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} />; + } } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx index 9729aea4cd6..189ace21d11 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx @@ -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} /> diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx index 1ac3fa296ec..fbcbd1db06c 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx @@ -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> ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx index 6efe8e25b18..261abe6dd2b 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx @@ -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> ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx index 44821562e28..9d218df274f 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx @@ -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} /> diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx index 15c6087e4ec..f0edfde9eab 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx @@ -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; } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx index fae37abdabd..00c4db25561 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx @@ -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} /> diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/BranchStatus-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/BranchStatus-test.tsx index e932d89cc4c..be779854e94 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/BranchStatus-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/BranchStatus-test.tsx @@ -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(); + } +}); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx index 8b138e9aa90..5d2d86f9d24 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx @@ -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); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx index c6ef473552c..a4eb5bfbf82 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx @@ -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 diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx index ba32de85805..2e1d56e2115 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx @@ -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 diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx index dc3ce7d17e8..2b6574a85ed 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx @@ -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(); }); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx index 766d5aff111..0a94ecf0cfe 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/BranchStatus-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/BranchStatus-test.tsx.snap index ab40c58e93c..1f4ccfc4484 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/BranchStatus-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/BranchStatus-test.tsx.snap @@ -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" > diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap index 3ead4391274..977dd6433c3 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap @@ -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> `; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap index dc05a411296..d80344beadd 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap @@ -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> `; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap index e579bfadf0c..c1aed6ae1b6 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap @@ -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, diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap index fac7593c0f9..ab9cc70c6b1 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap @@ -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", }, } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap new file mode 100644 index 00000000000..4b296afc2bf --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap index f09e0116d75..a99f95bc210 100644 --- a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap +++ b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap @@ -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", }, } diff --git a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.js.snap b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.js.snap index 9d298246118..d7a5a2f3e76 100644 --- a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.js.snap +++ b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.js.snap @@ -22,6 +22,7 @@ exports[`should match snapshot 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "foo", }, } diff --git a/server/sonar-web/src/main/js/apps/code/bucket.js b/server/sonar-web/src/main/js/apps/code/bucket.ts index 7b235e0f14b..fd052c55002 100644 --- a/server/sonar-web/src/main/js/apps/code/bucket.js +++ b/server/sonar-web/src/main/js/apps/code/bucket.ts @@ -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 = {}; diff --git a/server/sonar-web/src/main/js/apps/code/components/App.js b/server/sonar-web/src/main/js/apps/code/components/App.tsx index c59cac6bade..0df83e23dde 100644 --- a/server/sonar-web/src/main/js/apps/code/components/App.js +++ b/server/sonar-web/src/main/js/apps/code/components/App.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.js b/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.tsx index a6f5a0aedfa..3c8feee7b2b 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.js +++ b/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.tsx @@ -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> )} diff --git a/server/sonar-web/src/main/js/apps/code/components/Component.js b/server/sonar-web/src/main/js/apps/code/components/Component.tsx index 4fe40dfa618..5294779fd66 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Component.js +++ b/server/sonar-web/src/main/js/apps/code/components/Component.tsx @@ -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} diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentDetach.js b/server/sonar-web/src/main/js/apps/code/components/ComponentDetach.tsx index 30fccdfa7bf..0a237c54bb8 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentDetach.js +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentDetach.tsx @@ -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 } }} diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.js b/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx index b0e2db4adc1..c765ff3eb59 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.js +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx @@ -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; +} diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentName.js b/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx index 921aa3f724c..f6f2474ff42 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentName.js +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx @@ -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; +} diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentPin.js b/server/sonar-web/src/main/js/apps/code/components/ComponentPin.tsx index 641017207d7..785ce1640b2 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentPin.js +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentPin.tsx @@ -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; +} diff --git a/server/sonar-web/src/main/js/apps/code/components/Components.js b/server/sonar-web/src/main/js/apps/code/components/Components.tsx index 8e1451ff79e..89decb3012c 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Components.js +++ b/server/sonar-web/src/main/js/apps/code/components/Components.tsx @@ -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 />} diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.js b/server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.tsx index 02169f172db..8ea0b1a11b6 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.js +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.tsx @@ -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> ); } diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.js b/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.tsx index a2977ae960b..4717af5957f 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.js +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.tsx @@ -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; +} diff --git a/server/sonar-web/src/main/js/apps/code/components/Search.js b/server/sonar-web/src/main/js/apps/code/components/Search.tsx index 4d772e7a3b1..a66320d85d9 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Search.js +++ b/server/sonar-web/src/main/js/apps/code/components/Search.tsx @@ -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> ); } diff --git a/server/sonar-web/src/main/js/apps/code/components/Truncated.js b/server/sonar-web/src/main/js/apps/code/components/Truncated.tsx index c6e380859db..5409db09ea5 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Truncated.js +++ b/server/sonar-web/src/main/js/apps/code/components/Truncated.tsx @@ -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} diff --git a/server/sonar-web/src/main/js/apps/code/components/Breadcrumb.js b/server/sonar-web/src/main/js/apps/code/types.ts index 3e0ce74f384..3bf912f0a35 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Breadcrumb.js +++ b/server/sonar-web/src/main/js/apps/code/types.ts @@ -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; } diff --git a/server/sonar-web/src/main/js/apps/code/utils.js b/server/sonar-web/src/main/js/apps/code/utils.ts index 52975be78ec..4904ed4c50f 100644 --- a/server/sonar-web/src/main/js/apps/code/utils.js +++ b/server/sonar-web/src/main/js/apps/code/utils.ts @@ -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); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/App.js b/server/sonar-web/src/main/js/apps/component-measures/components/App.js index d75feac2ca2..e6545eea8ea 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/App.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/App.js @@ -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} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js b/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js index f823961bb6f..896d6870e23 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js @@ -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())) ); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js index 0a5920f55c9..7de031dee8a 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js @@ -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 }); } diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js index c5dedfe59b6..64411b028c1 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js @@ -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} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js index 5c4fbdea60e..faf2ee60d5f 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js @@ -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} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.js index 56ac23bdf58..8543558ebc4 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.js @@ -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} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.js index 345ccc78dc3..02c07128e45 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.js @@ -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} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumbs-test.js b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumbs-test.js index 22018da2057..a08a96339b4 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumbs-test.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumbs-test.js @@ -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' }} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap index fae4fa473aa..e9ff866768e 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap @@ -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", diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.js index d38768555a9..a8f042e7bf0 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.js @@ -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> diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.js index 2d28cb63de7..53175f19e06 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.js @@ -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} diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js index bc38573e3ad..9e2a1e7ed3d 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js @@ -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} /> diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js index b1f8aebad51..6fe0bb181b0 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js @@ -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} diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js index 5bfa9a7873f..9d1b3ff6d1a 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js @@ -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); diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.js b/server/sonar-web/src/main/js/apps/issues/components/App.js index 509a69dddce..87191bace08 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/App.js +++ b/server/sonar-web/src/main/js/apps/issues/components/App.js @@ -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} diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js b/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js index 6561fd547fa..eed561cee25 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js @@ -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} diff --git a/server/sonar-web/src/main/js/apps/overview/components/App.js b/server/sonar-web/src/main/js/apps/overview/components/App.js index aa36f8402aa..794aae352ca 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/App.js +++ b/server/sonar-web/src/main/js/apps/overview/components/App.js @@ -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} + /> + ); } } diff --git a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js index 9e28bf48386..907bc8c95c1 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js +++ b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js @@ -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} diff --git a/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js b/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js index e3549293bfd..937dd653952 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js +++ b/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js @@ -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> diff --git a/server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js b/server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js index 03c890da665..0c52daceb1a 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js +++ b/server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js @@ -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 = ( diff --git a/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.js b/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.js index 7311941cc3a..05d2f89d658 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.js +++ b/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.js @@ -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> diff --git a/server/sonar-web/src/main/js/apps/overview/main/CodeSmells.js b/server/sonar-web/src/main/js/apps/overview/main/CodeSmells.js index 01e98995e21..a9ab208ea17 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/CodeSmells.js +++ b/server/sonar-web/src/main/js/apps/overview/main/CodeSmells.js @@ -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' }); diff --git a/server/sonar-web/src/main/js/apps/overview/main/Coverage.js b/server/sonar-web/src/main/js/apps/overview/main/Coverage.js index 0fcf8dd1668..90cac0105d4 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/Coverage.js +++ b/server/sonar-web/src/main/js/apps/overview/main/Coverage.js @@ -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}> diff --git a/server/sonar-web/src/main/js/apps/overview/main/Duplications.js b/server/sonar-web/src/main/js/apps/overview/main/Duplications.js index 9d549b18a9f..acad0d75090 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/Duplications.js +++ b/server/sonar-web/src/main/js/apps/overview/main/Duplications.js @@ -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}> diff --git a/server/sonar-web/src/main/js/apps/overview/main/enhance.js b/server/sonar-web/src/main/js/apps/overview/main/enhance.js index 7d0b11432b5..a6ef18f6f8b 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/enhance.js +++ b/server/sonar-web/src/main/js/apps/overview/main/enhance.js @@ -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> ); diff --git a/server/sonar-web/src/main/js/apps/overview/meta/Meta.js b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js index a79b44dbd41..8bf9ed046ba 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/Meta.js +++ b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js @@ -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} diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaLinks.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaLinks.js index 4982632cc80..c62fd9d3979 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaLinks.js +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaLinks.js @@ -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(); } } diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js index fe153ca52fd..12c6172fde3 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaSize.js @@ -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"> diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js index cb5795332f9..bb4e7041003 100644 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js @@ -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> ); } diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.js index 66ea85d9289..ddf81063fd6 100644 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.js +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.js @@ -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} diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateConditions.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateConditions.js index d3edece78d7..02fbf9c5e5f 100644 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateConditions.js +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateConditions.js @@ -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} /> diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/QualityGateCondition-test.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/QualityGateCondition-test.js index 449ceff035b..1eee087a42f 100644 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/QualityGateCondition-test.js +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/QualityGateCondition-test.js @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.js.snap b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.js.snap index c887cc5062e..aac6af124c6 100644 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.js.snap +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.js.snap @@ -9,6 +9,7 @@ exports[`renders 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "foo", }, } diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap index cf6049b83cb..2b4d6d19c62 100644 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap @@ -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", diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js index de7cde52b7a..19db0c40c33 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js @@ -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 } }); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-test.js index 2dcf1dd4d61..850526fe3d9 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-test.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-test.js @@ -63,6 +63,7 @@ const DEFAULT_PROPS = { addVersion: () => {}, analyses: ANALYSES, analysesLoading: false, + branch: { isMain: true }, changeEvent: () => {}, deleteAnalysis: () => {}, deleteEvent: () => {}, diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js index 8c72221dd3c..9f41c7a8a8e 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js @@ -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')} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js index 9391f3dedf9..991ec78a765 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js @@ -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}{' '} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js index 3206805adc1..4100e0a5439 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js @@ -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() { diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js index fec8023fc77..e5fc6ec87f2 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js @@ -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() { diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/line-actions-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/popups/line-actions-popup.js index 1f3cc3edeb2..46c9c81428d 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/popups/line-actions-popup.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/popups/line-actions-popup.js @@ -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 }; } }); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js b/server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js index 23190cd383a..d9d7eb25c31 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js @@ -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; diff --git a/server/sonar-web/src/main/js/components/icons-components/BranchIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/BranchIcon.tsx index 420d94ebd6a..a1b1a363b3d 100644 --- a/server/sonar-web/src/main/js/components/icons-components/BranchIcon.tsx +++ b/server/sonar-web/src/main/js/components/icons-components/BranchIcon.tsx @@ -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} />; } diff --git a/server/sonar-web/src/main/js/components/icons-components/LongLivingBranchIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/LongLivingBranchIcon.tsx new file mode 100644 index 00000000000..bba5d0de101 --- /dev/null +++ b/server/sonar-web/src/main/js/components/icons-components/LongLivingBranchIcon.tsx @@ -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> + ); +} diff --git a/server/sonar-web/src/main/js/components/icons-components/PullRequestIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/PullRequestIcon.tsx new file mode 100644 index 00000000000..e3432619cb8 --- /dev/null +++ b/server/sonar-web/src/main/js/components/icons-components/PullRequestIcon.tsx @@ -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> + ); +} diff --git a/server/sonar-web/src/main/js/components/icons-components/ShortLivingBranchIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/ShortLivingBranchIcon.tsx new file mode 100644 index 00000000000..f1eaa7a3cfc --- /dev/null +++ b/server/sonar-web/src/main/js/components/icons-components/ShortLivingBranchIcon.tsx @@ -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> + ); +} diff --git a/server/sonar-web/src/main/js/components/shared/drilldown-link.js b/server/sonar-web/src/main/js/components/shared/drilldown-link.js index 13b0afeb0c3..87aadeea190 100644 --- a/server/sonar-web/src/main/js/components/shared/drilldown-link.js +++ b/server/sonar-web/src/main/js/components/shared/drilldown-link.js @@ -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} diff --git a/server/sonar-web/src/main/js/helpers/__tests__/branches-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/branches-test.ts new file mode 100644 index 00000000000..640b5a05d4e --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/__tests__/branches-test.ts @@ -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 }; +} diff --git a/server/sonar-web/src/main/js/helpers/branches.ts b/server/sonar-web/src/main/js/helpers/branches.ts index f447020e58d..d9eb71449a9 100644 --- a/server/sonar-web/src/main/js/helpers/branches.ts +++ b/server/sonar-web/src/main/js/helpers/branches.ts @@ -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'); + } } diff --git a/server/sonar-web/src/main/js/helpers/testUtils.ts b/server/sonar-web/src/main/js/helpers/testUtils.ts index 6675a6791b1..507fbc44ffb 100644 --- a/server/sonar-web/src/main/js/helpers/testUtils.ts +++ b/server/sonar-web/src/main/js/helpers/testUtils.ts @@ -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); + }); }); } diff --git a/server/sonar-web/src/main/js/helpers/urls.ts b/server/sonar-web/src/main/js/helpers/urls.ts index 4f7f62e72fc..378d89bb0f9 100644 --- a/server/sonar-web/src/main/js/helpers/urls.ts +++ b/server/sonar-web/src/main/js/helpers/urls.ts @@ -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 } }; } |