aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStas Vilchik <stas.vilchik@sonarsource.com>2017-09-25 11:43:24 +0200
committerStas Vilchik <stas.vilchik@sonarsource.com>2017-09-25 13:40:46 +0200
commit3fe5d569ca87b58e1cfbb9253abe6a8c5c912230 (patch)
tree23b8f180e2cb136326e67c1864de758ab659fe7c
parent3882435f10d70ef47f4b189b9d032119335e5130 (diff)
downloadsonarqube-3fe5d569ca87b58e1cfbb9253abe6a8c5c912230.tar.gz
sonarqube-3fe5d569ca87b58e1cfbb9253abe6a8c5c912230.zip
revert changes of branches administration
-rw-r--r--server/sonar-web/src/main/js/app/components/ComponentContainer.tsx2
-rw-r--r--server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx17
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx2
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx2
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx23
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx129
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx21
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx6
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx11
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx11
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx20
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap7
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap51
-rw-r--r--server/sonar-web/src/main/js/app/utils/startReactApp.js2
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx60
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx139
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/DeleteBranchModal.tsx (renamed from server/sonar-web/src/main/js/app/components/nav/component/DeleteBranchModal.tsx)7
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx (renamed from server/sonar-web/src/main/js/app/components/nav/component/RenameBranchModal.tsx)7
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx34
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx65
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/DeleteBranchModal-test.tsx (renamed from server/sonar-web/src/main/js/app/components/nav/component/__tests__/DeleteBranchModal-test.tsx)8
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/RenameBranchModal-test.tsx (renamed from server/sonar-web/src/main/js/app/components/nav/component/__tests__/RenameBranchModal-test.tsx)8
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap73
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap100
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/DeleteBranchModal-test.tsx.snap (renamed from server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/DeleteBranchModal-test.tsx.snap)0
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/RenameBranchModal-test.tsx.snap (renamed from server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/RenameBranchModal-test.tsx.snap)0
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/routes.ts30
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/App.js2
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties5
29 files changed, 661 insertions, 181 deletions
diff --git a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
index a1d01845343..5c4287ae231 100644
--- a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
+++ b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
@@ -137,7 +137,6 @@ export default class ComponentContainer extends React.PureComponent<Props, State
currentBranch={branch}
component={component}
location={this.props.location}
- onBranchesChange={this.handleBranchesChange}
/>
)}
{loading ? (
@@ -149,6 +148,7 @@ export default class ComponentContainer extends React.PureComponent<Props, State
branch,
branches,
component,
+ onBranchesChange: this.handleBranchesChange,
onComponentChange: this.handleComponentChange
})
)}
diff --git a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
index 01ff6e3a4fa..105931959f9 100644
--- a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
@@ -106,3 +106,20 @@ it("doesn't load branches portfolio", () => {
expect(wrapper.find(Inner).exists()).toBeTruthy();
});
});
+
+it('updates branches on change', () => {
+ (getBranches as jest.Mock<any>).mockImplementation(() => Promise.resolve([]));
+ const wrapper = shallow(
+ <ComponentContainer location={{ query: { id: 'portfolioKey' } }}>
+ <Inner />
+ </ComponentContainer>
+ );
+ (wrapper.instance() as ComponentContainer).mounted = true;
+ wrapper.setState({
+ branches: [{ isMain: true }],
+ component: { breadcrumbs: [{ key: 'projectKey', name: 'project', qualifier: 'TRK' }] },
+ loading: false
+ });
+ (wrapper.find(Inner).prop('onBranchesChange') as Function)();
+ expect(getBranches).toBeCalledWith('projectKey');
+});
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 5d78833af2d..5b9a509fdd8 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
@@ -35,7 +35,6 @@ interface Props {
currentBranch?: Branch;
component: Component;
location: {};
- onBranchesChange: () => void;
}
interface State {
@@ -106,7 +105,6 @@ export default class ComponentNav extends React.PureComponent<Props, State> {
currentBranch={this.props.currentBranch}
// to close dropdown on any location change
location={this.props.location}
- onBranchesChange={this.props.onBranchesChange}
/>
)}
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 21010a1ec69..7215a3ab825 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
@@ -35,7 +35,6 @@ interface Props {
component: Component;
currentBranch: Branch;
location?: any;
- onBranchesChange: () => void;
}
interface State {
@@ -128,7 +127,6 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State
canAdmin={configuration && configuration.showSettings}
component={this.props.component}
currentBranch={this.props.currentBranch}
- onBranchesChange={this.props.onBranchesChange}
onClose={this.closeDropdown}
/>
) : null;
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 f4ea43b0bc6..21d66918b1f 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,6 +19,7 @@
*/
import * as React from 'react';
import * as PropTypes from 'prop-types';
+import { Link } from 'react-router';
import ComponentNavBranchesMenuItem from './ComponentNavBranchesMenuItem';
import { Branch, Component } from '../../../types';
import {
@@ -35,7 +36,6 @@ interface Props {
canAdmin?: boolean;
component: Component;
currentBranch: Branch;
- onBranchesChange: () => void;
onClose: () => void;
}
@@ -66,9 +66,7 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
);
handleClickOutside = (event: Event) => {
- // do not close when rename or delete branch modal is open
- const modal = document.querySelector('.modal');
- if (!modal && (!this.node || !this.node.contains(event.target as HTMLElement))) {
+ if (!this.node || !this.node.contains(event.target as HTMLElement)) {
this.props.onClose();
}
};
@@ -194,10 +192,8 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
menu.push(
<ComponentNavBranchesMenuItem
branch={branch}
- canAdmin={this.props.canAdmin}
component={this.props.component}
key={branch.name}
- onBranchesChange={this.props.onBranchesChange}
onSelect={this.handleSelect}
selected={branch.name === selected}
/>
@@ -208,10 +204,25 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
};
render() {
+ const { component } = this.props;
+ const showManageLink =
+ component.qualifier === 'TRK' &&
+ component.configuration &&
+ component.configuration.showSettings;
+
return (
<div className="dropdown-menu dropdown-menu-shadow" ref={node => (this.node = node)}>
{this.renderSearch()}
{this.renderBranchesList()}
+ {showManageLink && (
+ <div className="dropdown-bottom-hint text-right">
+ <Link
+ className="text-muted"
+ to={{ pathname: '/project/branches', query: { id: component.key } }}>
+ {translate('branches.manage')}
+ </Link>
+ </div>
+ )}
</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 a1e02a1aa8f..46f9a02957e 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
@@ -20,125 +20,48 @@
import * as React from 'react';
import { Link } from 'react-router';
import * as classNames from 'classnames';
-import DeleteBranchModal from './DeleteBranchModal';
-import RenameBranchModal from './RenameBranchModal';
import BranchStatus from '../../../../components/common/BranchStatus';
import { Branch, Component } from '../../../types';
import BranchIcon from '../../../../components/icons-components/BranchIcon';
-import ChangeIcon from '../../../../components/icons-components/ChangeIcon';
-import DeleteIcon from '../../../../components/icons-components/DeleteIcon';
import { isShortLivingBranch } from '../../../../helpers/branches';
import { translate } from '../../../../helpers/l10n';
import { getProjectBranchUrl } from '../../../../helpers/urls';
export interface Props {
branch: Branch;
- canAdmin?: boolean;
component: Component;
- onBranchesChange: () => void;
onSelect: (branch: Branch) => void;
selected: boolean;
}
-interface State {
- deleteBranchModal: boolean;
- renameBranchModal: boolean;
-}
-
-export default class ComponentNavBranchesMenuItem extends React.PureComponent<Props, State> {
- state: State = { deleteBranchModal: false, renameBranchModal: false };
-
- handleMouseEnter = () => {
- this.props.onSelect(this.props.branch);
- };
-
- handleDeleteBranchClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
- event.preventDefault();
- event.currentTarget.blur();
- this.setState({ deleteBranchModal: true });
- };
-
- handleDeleteBranchClose = () => {
- this.setState({ deleteBranchModal: false });
- };
-
- handleBranchDelete = () => {
- this.props.onBranchesChange();
- this.setState({ deleteBranchModal: false });
- };
-
- handleRenameBranchClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
- event.preventDefault();
- event.currentTarget.blur();
- this.setState({ renameBranchModal: true });
+export default function ComponentNavBranchesMenuItem({ branch, ...props }: Props) {
+ const handleMouseEnter = () => {
+ props.onSelect(branch);
};
- handleRenameBranchClose = () => {
- this.setState({ renameBranchModal: false });
- };
-
- handleBranchRename = () => {
- this.props.onBranchesChange();
- this.setState({ renameBranchModal: false });
- };
-
- render() {
- const { branch } = this.props;
- return (
- <li key={branch.name} onMouseEnter={this.handleMouseEnter}>
- <Link
- className={classNames('navbar-context-meta-branch-menu-item', {
- active: this.props.selected
- })}
- to={getProjectBranchUrl(this.props.component.key, branch)}>
- <div className="navbar-context-meta-branch-menu-item-name">
- <BranchIcon
- branch={branch}
- className={classNames('little-spacer-right', {
- 'big-spacer-left': isShortLivingBranch(branch) && !branch.isOrphan
- })}
- />
- {branch.name}
- {branch.isMain && (
- <div className="outline-badge spacer-left">{translate('branches.main_branch')}</div>
- )}
- </div>
- <div className="big-spacer-left note">
- <BranchStatus branch={branch} concise={true} />
- </div>
- {this.props.canAdmin && (
- <div className="navbar-context-meta-branch-menu-item-actions">
- {branch.isMain ? (
- <button className="js-rename button-link" onClick={this.handleRenameBranchClick}>
- <ChangeIcon />
- </button>
- ) : (
- <button className="js-delete button-link" onClick={this.handleDeleteBranchClick}>
- <DeleteIcon />
- </button>
- )}
- </div>
- )}
- </Link>
-
- {this.state.deleteBranchModal && (
- <DeleteBranchModal
+ return (
+ <li key={branch.name} onMouseEnter={handleMouseEnter}>
+ <Link
+ className={classNames('navbar-context-meta-branch-menu-item', {
+ active: props.selected
+ })}
+ to={getProjectBranchUrl(props.component.key, branch)}>
+ <div className="navbar-context-meta-branch-menu-item-name">
+ <BranchIcon
branch={branch}
- component={this.props.component.key}
- onClose={this.handleDeleteBranchClose}
- onDelete={this.handleBranchDelete}
+ className={classNames('little-spacer-right', {
+ 'big-spacer-left': isShortLivingBranch(branch) && !branch.isOrphan
+ })}
/>
- )}
-
- {this.state.renameBranchModal && (
- <RenameBranchModal
- branch={branch}
- component={this.props.component.key}
- onClose={this.handleRenameBranchClose}
- onRename={this.handleBranchRename}
- />
- )}
- </li>
- );
- }
+ {branch.name}
+ {branch.isMain && (
+ <div className="outline-badge spacer-left">{translate('branches.main_branch')}</div>
+ )}
+ </div>
+ <div className="big-spacer-left note">
+ <BranchStatus branch={branch} concise={true} />
+ </div>
+ </Link>
+ </li>
+ );
}
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 270829acee0..9cc10de252e 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
@@ -241,6 +241,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
renderAdministrationLinks() {
return [
this.renderSettingsLink(),
+ this.renderBranchesLink(),
this.renderProfilesLink(),
this.renderQualityGateLink(),
this.renderCustomMeasuresLink(),
@@ -274,6 +275,26 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
);
}
+ renderBranchesLink() {
+ if (
+ !this.context.branchesEnabled ||
+ !this.isProject() ||
+ !this.getConfiguration().showSettings
+ ) {
+ return null;
+ }
+
+ return (
+ <li key="branches">
+ <Link
+ to={{ pathname: '/project/branches', query: { id: this.props.component.key } }}
+ activeClassName="active">
+ {translate('project_branches.page')}
+ </Link>
+ </li>
+ );
+ }
+
renderProfilesLink() {
if (!this.getConfiguration().showQualityProfiles) {
return null;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
index 6adc8751910..f63e785ca5a 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
@@ -62,15 +62,13 @@ const component = {
it('loads status', () => {
getTasksForComponent.mockClear();
- mount(
- <ComponentNav branches={[]} component={component} location={{}} onBranchesChange={jest.fn()} />
- );
+ mount(<ComponentNav branches={[]} component={component} location={{}} />);
expect(getTasksForComponent).toBeCalledWith('component');
});
it('renders', () => {
const wrapper = shallow(
- <ComponentNav branches={[]} component={component} location={{}} onBranchesChange={jest.fn()} />
+ <ComponentNav branches={[]} component={component} location={{}} />
);
wrapper.setState({
incremental: true,
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 5a2343bb4c5..8da338c23e1 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
@@ -40,7 +40,6 @@ it('renders main branch', () => {
branches={[branch, fooBranch]}
component={component}
currentBranch={branch}
- onBranchesChange={jest.fn()}
/>,
{ context: { branchesEnabled: true } }
)
@@ -62,7 +61,6 @@ it('renders short-living branch', () => {
branches={[branch, fooBranch]}
component={component}
currentBranch={branch}
- onBranchesChange={jest.fn()}
/>,
{ context: { branchesEnabled: true } }
)
@@ -77,7 +75,6 @@ it('opens menu', () => {
branches={[branch, fooBranch]}
component={component}
currentBranch={branch}
- onBranchesChange={jest.fn()}
/>,
{ context: { branchesEnabled: true } }
);
@@ -90,12 +87,7 @@ it('renders single branch popup', () => {
const branch: MainBranch = { isMain: true, name: 'master' };
const component = {} as Component;
const wrapper = shallow(
- <ComponentNavBranch
- branches={[branch]}
- component={component}
- currentBranch={branch}
- onBranchesChange={jest.fn()}
- />,
+ <ComponentNavBranch branches={[branch]} component={component} currentBranch={branch} />,
{ context: { branchesEnabled: true } }
);
expect(wrapper).toMatchSnapshot();
@@ -112,7 +104,6 @@ it('renders nothing when no branch support', () => {
branches={[branch, fooBranch]}
component={component}
currentBranch={branch}
- onBranchesChange={jest.fn()}
/>,
{ context: { branchesEnabled: false } }
);
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 13e251e059a..6232c936b4d 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,16 +29,15 @@ import {
} from '../../../../types';
import { elementKeydown } from '../../../../../helpers/testUtils';
-const project = { key: 'component' } as Component;
+const component = { key: 'component' } as Component;
it('renders list', () => {
expect(
shallow(
<ComponentNavBranchesMenu
branches={[mainBranch(), shortBranch('foo'), longBranch('bar'), shortBranch('baz', true)]}
- component={project}
+ component={component}
currentBranch={mainBranch()}
- onBranchesChange={jest.fn()}
onClose={jest.fn()}
/>
)
@@ -49,9 +48,8 @@ it('searches', () => {
const wrapper = shallow(
<ComponentNavBranchesMenu
branches={[mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')]}
- component={project}
+ component={component}
currentBranch={mainBranch()}
- onBranchesChange={jest.fn()}
onClose={jest.fn()}
/>
);
@@ -63,9 +61,8 @@ it('selects next & previous', () => {
const wrapper = shallow(
<ComponentNavBranchesMenu
branches={[mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')]}
- component={project}
+ component={component}
currentBranch={mainBranch()}
- onBranchesChange={jest.fn()}
onClose={jest.fn()}
/>
);
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 c8ff060fa56..75887b7c2f6 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
@@ -21,7 +21,6 @@ import * as React from 'react';
import { shallow } from 'enzyme';
import ComponentNavBranchesMenuItem, { Props } from '../ComponentNavBranchesMenuItem';
import { BranchType, MainBranch, ShortLivingBranch, Component } from '../../../../types';
-import { click } from '../../../../../helpers/testUtils';
const component = { key: 'component' } as Component;
@@ -47,30 +46,11 @@ it('renders short-living orhpan branch', () => {
expect(shallowRender({ branch: { ...shortBranch, isOrphan: true } })).toMatchSnapshot();
});
-it('renames main branch', () => {
- const onBranchesChange = jest.fn();
- const wrapper = shallowRender({ branch: mainBranch, canAdmin: true, onBranchesChange });
-
- click(wrapper.find('.js-rename'));
- (wrapper.find('RenameBranchModal').prop('onRename') as Function)();
- expect(onBranchesChange).toBeCalled();
-});
-
-it('deletes short-living branch', () => {
- const onBranchesChange = jest.fn();
- const wrapper = shallowRender({ canAdmin: true, onBranchesChange });
-
- click(wrapper.find('.js-delete'));
- (wrapper.find('DeleteBranchModal').prop('onDelete') as Function)();
- expect(onBranchesChange).toBeCalled();
-});
-
function shallowRender(props?: { [P in keyof Props]?: Props[P] }) {
return shallow(
<ComponentNavBranchesMenuItem
branch={shortBranch}
component={component}
- onBranchesChange={jest.fn()}
onSelect={jest.fn()}
selected={false}
{...props}
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 eda0f3f1cb5..463fb48d493 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
@@ -39,7 +39,6 @@ exports[`renders list 1`] = `
"key": "component",
}
}
- onBranchesChange={[Function]}
onSelect={[Function]}
selected={true}
/>
@@ -79,7 +78,6 @@ exports[`renders list 1`] = `
"key": "component",
}
}
- onBranchesChange={[Function]}
onSelect={[Function]}
selected={false}
/>
@@ -103,7 +101,6 @@ exports[`renders list 1`] = `
"key": "component",
}
}
- onBranchesChange={[Function]}
onSelect={[Function]}
selected={false}
/>
@@ -123,7 +120,6 @@ exports[`renders list 1`] = `
"key": "component",
}
}
- onBranchesChange={[Function]}
onSelect={[Function]}
selected={false}
/>
@@ -163,7 +159,6 @@ exports[`renders list 1`] = `
"key": "component",
}
}
- onBranchesChange={[Function]}
onSelect={[Function]}
selected={false}
/>
@@ -218,7 +213,6 @@ exports[`searches 1`] = `
"key": "component",
}
}
- onBranchesChange={[Function]}
onSelect={[Function]}
selected={true}
/>
@@ -238,7 +232,6 @@ exports[`searches 1`] = `
"key": "component",
}
}
- onBranchesChange={[Function]}
onSelect={[Function]}
selected={false}
/>
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 7b305c2b763..60d527b6a42 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
@@ -136,6 +136,23 @@ exports[`should work for all qualifiers 1`] = `
style={Object {}}
to={
Object {
+ "pathname": "/project/branches",
+ "query": Object {
+ "id": "foo",
+ },
+ }
+ }
+ >
+ project_branches.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
"pathname": "/project/deletion",
"query": Object {
"id": "foo",
@@ -1018,6 +1035,23 @@ exports[`should work with extensions 1`] = `
style={Object {}}
to={
Object {
+ "pathname": "/project/branches",
+ "query": Object {
+ "id": "foo",
+ },
+ }
+ }
+ >
+ project_branches.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
"pathname": "/project/admin/extension/foo",
"query": Object {
"id": "foo",
@@ -1223,6 +1257,23 @@ exports[`should work with multiple extensions 1`] = `
style={Object {}}
to={
Object {
+ "pathname": "/project/branches",
+ "query": Object {
+ "id": "foo",
+ },
+ }
+ }
+ >
+ project_branches.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
"pathname": "/project/admin/extension/foo",
"query": Object {
"id": "foo",
diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.js b/server/sonar-web/src/main/js/app/utils/startReactApp.js
index 1b8f7f606c3..b6577565efd 100644
--- a/server/sonar-web/src/main/js/app/utils/startReactApp.js
+++ b/server/sonar-web/src/main/js/app/utils/startReactApp.js
@@ -55,6 +55,7 @@ import permissionTemplatesRoutes from '../../apps/permission-templates/routes';
import portfolioRoutes from '../../apps/portfolio/routes';
import projectActivityRoutes from '../../apps/projectActivity/routes';
import projectAdminRoutes from '../../apps/project-admin/routes';
+import projectBranchesRoutes from '../../apps/projectBranches/routes';
import projectQualityGateRoutes from '../../apps/projectQualityGate/routes';
import projectQualityProfilesRoutes from '../../apps/projectQualityProfiles/routes';
import projectsRoutes from '../../apps/projects/routes';
@@ -206,6 +207,7 @@ const startReactApp = () => {
component={ProjectAdminPageExtension}
/>
<Route path="project/background_tasks" childRoutes={backgroundTasksRoutes} />
+ <Route path="project/branches" childRoutes={projectBranchesRoutes} />
<Route path="project/settings" childRoutes={settingsRoutes} />
<Route path="project_roles" childRoutes={projectPermissionsRoutes} />
</Route>
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx
new file mode 100644
index 00000000000..02a8e20365f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx
@@ -0,0 +1,60 @@
+/*
+ * 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 * as React from 'react';
+import BranchRow from './BranchRow';
+import { Branch } from '../../../app/types';
+import { sortBranchesAsTree } from '../../../helpers/branches';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ branches: Branch[];
+ component: { key: string };
+ onBranchesChange: () => void;
+}
+
+export default function App({ branches, component, onBranchesChange }: Props) {
+ return (
+ <div className="page page-limited">
+ <header className="page-header">
+ <h1 className="page-title">{translate('project_branches.page')}</h1>
+ </header>
+
+ <table className="data zebra zebra-hover">
+ <thead>
+ <tr>
+ <th>{translate('branch')}</th>
+ <th className="text-right">{translate('status')}</th>
+ <th className="text-right">{translate('actions')}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {sortBranchesAsTree(branches).map(branch => (
+ <BranchRow
+ branch={branch}
+ component={component.key}
+ key={branch.name}
+ onChange={onBranchesChange}
+ />
+ ))}
+ </tbody>
+ </table>
+ </div>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx
new file mode 100644
index 00000000000..e163481d759
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx
@@ -0,0 +1,139 @@
+/*
+ * 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 * as React from 'react';
+import { Branch } from '../../../app/types';
+import * as classNames from 'classnames';
+import DeleteBranchModal from './DeleteBranchModal';
+import BranchStatus from '../../../components/common/BranchStatus';
+import BranchIcon from '../../../components/icons-components/BranchIcon';
+import { isShortLivingBranch } from '../../../helpers/branches';
+import ChangeIcon from '../../../components/icons-components/ChangeIcon';
+import DeleteIcon from '../../../components/icons-components/DeleteIcon';
+import { translate } from '../../../helpers/l10n';
+import Tooltip from '../../../components/controls/Tooltip';
+import RenameBranchModal from './RenameBranchModal';
+
+interface Props {
+ branch: Branch;
+ component: string;
+ onChange: () => void;
+}
+
+interface State {
+ deleting: boolean;
+ renaming: boolean;
+}
+
+export default class BranchRow extends React.PureComponent<Props, State> {
+ mounted: boolean;
+ state: State = { deleting: false, renaming: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleDeleteClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ this.setState({ deleting: true });
+ };
+
+ handleDeletingStop = () => {
+ this.setState({ deleting: false });
+ };
+
+ handleRenameClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ this.setState({ renaming: true });
+ };
+
+ handleChange = () => {
+ if (this.mounted) {
+ this.setState({ deleting: false, renaming: false });
+ this.props.onChange();
+ }
+ };
+
+ handleRenamingStop = () => {
+ this.setState({ renaming: false });
+ };
+
+ render() {
+ const { branch, component } = this.props;
+
+ return (
+ <tr>
+ <td>
+ <BranchIcon
+ branch={branch}
+ className={classNames('little-spacer-right', {
+ 'big-spacer-left': isShortLivingBranch(branch) && !branch.isOrphan
+ })}
+ />
+ {branch.name}
+ {branch.isMain && (
+ <div className="outline-badge spacer-left">{translate('branches.main_branch')}</div>
+ )}
+ </td>
+ <td className="thin nowrap text-right">
+ <BranchStatus branch={branch} />
+ </td>
+ <td className="thin nowrap text-right">
+ {branch.isMain ? (
+ <Tooltip overlay={translate('branches.rename')}>
+ <a className="js-rename link-no-underline" href="#" onClick={this.handleRenameClick}>
+ <ChangeIcon />
+ </a>
+ </Tooltip>
+ ) : (
+ <Tooltip overlay={translate('branches.delete')}>
+ <a className="js-delete link-no-underline" href="#" onClick={this.handleDeleteClick}>
+ <DeleteIcon />
+ </a>
+ </Tooltip>
+ )}
+ </td>
+
+ {this.state.deleting && (
+ <DeleteBranchModal
+ branch={branch}
+ component={component}
+ onClose={this.handleDeletingStop}
+ onDelete={this.handleChange}
+ />
+ )}
+
+ {this.state.renaming && (
+ <RenameBranchModal
+ branch={branch}
+ component={component}
+ onClose={this.handleRenamingStop}
+ onRename={this.handleChange}
+ />
+ )}
+ </tr>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/DeleteBranchModal.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/DeleteBranchModal.tsx
index 2273692a9a8..66d14ed260f 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/DeleteBranchModal.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/DeleteBranchModal.tsx
@@ -19,9 +19,9 @@
*/
import * as React from 'react';
import Modal from 'react-modal';
-import { deleteBranch } from '../../../../api/branches';
-import { Branch } from '../../../../app/types';
-import { translate, translateWithParameters } from '../../../../helpers/l10n';
+import { deleteBranch } from '../../../api/branches';
+import { Branch } from '../../../app/types';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
interface Props {
branch: Branch;
@@ -66,7 +66,6 @@ export default class DeleteBranchModal extends React.PureComponent<Props, State>
handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
- event.stopPropagation();
this.props.onClose();
};
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/RenameBranchModal.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx
index b17401c4e08..181fee72365 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/RenameBranchModal.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx
@@ -19,9 +19,9 @@
*/
import * as React from 'react';
import Modal from 'react-modal';
-import { renameBranch } from '../../../../api/branches';
-import { Branch } from '../../../../app/types';
-import { translate } from '../../../../helpers/l10n';
+import { renameBranch } from '../../../api/branches';
+import { Branch } from '../../../app/types';
+import { translate } from '../../../helpers/l10n';
interface Props {
branch: Branch;
@@ -70,7 +70,6 @@ export default class RenameBranchModal extends React.PureComponent<Props, State>
handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
- event.stopPropagation();
this.props.onClose();
};
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx
new file mode 100644
index 00000000000..4288105f79a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx
@@ -0,0 +1,34 @@
+/*
+ * 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 * as React from 'react';
+import { shallow } from 'enzyme';
+import App from '../App';
+import { Branch, BranchType } from '../../../../app/types';
+
+it('renders sorted list of branches', () => {
+ const branches: Branch[] = [
+ { isMain: true, name: 'master' },
+ { isMain: false, name: 'branch-1.0', type: BranchType.LONG },
+ { isMain: false, name: 'branch-1.0', mergeBranch: 'master', type: BranchType.SHORT }
+ ];
+ expect(
+ shallow(<App branches={branches} component={{ key: 'foo' }} onBranchesChange={jest.fn()} />)
+ ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx
new file mode 100644
index 00000000000..4edc3ce70d6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx
@@ -0,0 +1,65 @@
+/*
+ * 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 * as React from 'react';
+import { shallow } from 'enzyme';
+import BranchRow from '../BranchRow';
+import { MainBranch, ShortLivingBranch, BranchType } from '../../../../app/types';
+import { click } from '../../../../helpers/testUtils';
+
+const mainBranch: MainBranch = { isMain: true, name: 'master' };
+
+const shortBranch: ShortLivingBranch = {
+ isMain: false,
+ name: 'feature',
+ mergeBranch: 'foo',
+ type: BranchType.SHORT
+};
+
+it('renders main branch', () => {
+ expect(shallowRender(mainBranch)).toMatchSnapshot();
+});
+
+it('renders short-living branch', () => {
+ expect(shallowRender(shortBranch)).toMatchSnapshot();
+});
+
+it('renames main branch', () => {
+ const onChange = jest.fn();
+ const wrapper = shallowRender(mainBranch, onChange);
+
+ click(wrapper.find('.js-rename'));
+ (wrapper.find('RenameBranchModal').prop('onRename') as Function)();
+ expect(onChange).toBeCalled();
+});
+
+it('deletes short-living branch', () => {
+ const onChange = jest.fn();
+ const wrapper = shallowRender(shortBranch, onChange);
+
+ click(wrapper.find('.js-delete'));
+ (wrapper.find('DeleteBranchModal').prop('onDelete') as Function)();
+ expect(onChange).toBeCalled();
+});
+
+function shallowRender(branch: MainBranch | ShortLivingBranch, onChange: () => void = jest.fn()) {
+ const wrapper = shallow(<BranchRow branch={branch} component="foo" onChange={onChange} />);
+ (wrapper.instance() as any).mounted = true;
+ return wrapper;
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/DeleteBranchModal-test.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/DeleteBranchModal-test.tsx
index 0df5a12fdbb..b2870587114 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/DeleteBranchModal-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/DeleteBranchModal-test.tsx
@@ -17,14 +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.
*/
-jest.mock('../../../../../api/branches', () => ({ deleteBranch: jest.fn() }));
+jest.mock('../../../../api/branches', () => ({ deleteBranch: jest.fn() }));
import * as React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import DeleteBranchModal from '../DeleteBranchModal';
-import { ShortLivingBranch, BranchType } from '../../../../../app/types';
-import { submit, doAsync, click } from '../../../../../helpers/testUtils';
-import { deleteBranch } from '../../../../../api/branches';
+import { ShortLivingBranch, BranchType } from '../../../../app/types';
+import { submit, doAsync, click } from '../../../../helpers/testUtils';
+import { deleteBranch } from '../../../../api/branches';
beforeEach(() => {
(deleteBranch as jest.Mock<any>).mockClear();
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/RenameBranchModal-test.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/RenameBranchModal-test.tsx
index c1a0750e5bb..3a1c962d68e 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/RenameBranchModal-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/RenameBranchModal-test.tsx
@@ -17,14 +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.
*/
-jest.mock('../../../../../api/branches', () => ({ renameBranch: jest.fn() }));
+jest.mock('../../../../api/branches', () => ({ renameBranch: jest.fn() }));
import * as React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import RenameBranchModal from '../RenameBranchModal';
-import { MainBranch } from '../../../../../app/types';
-import { submit, doAsync, click, change } from '../../../../../helpers/testUtils';
-import { renameBranch } from '../../../../../api/branches';
+import { MainBranch } from '../../../../app/types';
+import { submit, doAsync, click, change } from '../../../../helpers/testUtils';
+import { renameBranch } from '../../../../api/branches';
beforeEach(() => {
(renameBranch as jest.Mock<any>).mockClear();
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap
new file mode 100644
index 00000000000..6f983e33df8
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap
@@ -0,0 +1,73 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders sorted list of branches 1`] = `
+<div
+ className="page page-limited"
+>
+ <header
+ className="page-header"
+ >
+ <h1
+ className="page-title"
+ >
+ project_branches.page
+ </h1>
+ </header>
+ <table
+ className="data zebra zebra-hover"
+ >
+ <thead>
+ <tr>
+ <th>
+ branch
+ </th>
+ <th
+ className="text-right"
+ >
+ status
+ </th>
+ <th
+ className="text-right"
+ >
+ actions
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <BranchRow
+ branch={
+ Object {
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ component="foo"
+ onChange={[Function]}
+ />
+ <BranchRow
+ branch={
+ Object {
+ "isMain": false,
+ "mergeBranch": "master",
+ "name": "branch-1.0",
+ "type": "SHORT",
+ }
+ }
+ component="foo"
+ onChange={[Function]}
+ />
+ <BranchRow
+ branch={
+ Object {
+ "isMain": false,
+ "name": "branch-1.0",
+ "type": "LONG",
+ }
+ }
+ component="foo"
+ onChange={[Function]}
+ />
+ </tbody>
+ </table>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap
new file mode 100644
index 00000000000..ea135765ece
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap
@@ -0,0 +1,100 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders main branch 1`] = `
+<tr>
+ <td>
+ <BranchIcon
+ branch={
+ Object {
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ className="little-spacer-right"
+ />
+ master
+ <div
+ className="outline-badge spacer-left"
+ >
+ branches.main_branch
+ </div>
+ </td>
+ <td
+ className="thin nowrap text-right"
+ >
+ <BranchStatus
+ branch={
+ Object {
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ />
+ </td>
+ <td
+ className="thin nowrap text-right"
+ >
+ <Tooltip
+ overlay="branches.rename"
+ placement="bottom"
+ >
+ <a
+ className="js-rename link-no-underline"
+ href="#"
+ onClick={[Function]}
+ >
+ <ChangeIcon />
+ </a>
+ </Tooltip>
+ </td>
+</tr>
+`;
+
+exports[`renders short-living branch 1`] = `
+<tr>
+ <td>
+ <BranchIcon
+ branch={
+ Object {
+ "isMain": false,
+ "mergeBranch": "foo",
+ "name": "feature",
+ "type": "SHORT",
+ }
+ }
+ className="little-spacer-right big-spacer-left"
+ />
+ feature
+ </td>
+ <td
+ className="thin nowrap text-right"
+ >
+ <BranchStatus
+ branch={
+ Object {
+ "isMain": false,
+ "mergeBranch": "foo",
+ "name": "feature",
+ "type": "SHORT",
+ }
+ }
+ />
+ </td>
+ <td
+ className="thin nowrap text-right"
+ >
+ <Tooltip
+ overlay="branches.delete"
+ placement="bottom"
+ >
+ <a
+ className="js-delete link-no-underline"
+ href="#"
+ onClick={[Function]}
+ >
+ <DeleteIcon />
+ </a>
+ </Tooltip>
+ </td>
+</tr>
+`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/DeleteBranchModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/DeleteBranchModal-test.tsx.snap
index 934f8ed2d7d..934f8ed2d7d 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/DeleteBranchModal-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/DeleteBranchModal-test.tsx.snap
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/RenameBranchModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/RenameBranchModal-test.tsx.snap
index 7867fa4785a..7867fa4785a 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/RenameBranchModal-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/RenameBranchModal-test.tsx.snap
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/routes.ts b/server/sonar-web/src/main/js/apps/projectBranches/routes.ts
new file mode 100644
index 00000000000..520805ebac5
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectBranches/routes.ts
@@ -0,0 +1,30 @@
+/*
+ * 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 { RouterState, IndexRouteProps } from 'react-router';
+
+const routes = [
+ {
+ getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) {
+ import('./components/App').then(i => callback(null, { component: (i as any).default }));
+ }
+ }
+];
+
+export default routes;
diff --git a/server/sonar-web/src/main/js/apps/settings/components/App.js b/server/sonar-web/src/main/js/apps/settings/components/App.js
index dff1d0b74d5..866830fbb69 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/App.js
+++ b/server/sonar-web/src/main/js/apps/settings/components/App.js
@@ -98,7 +98,7 @@ export default class App extends React.PureComponent {
link: (
<Link
to={{
- pathname: '/project/settings',
+ pathname: '/project/branches',
query: { id: this.props.component && this.props.component.key }
}}>
{translate('branches.settings_hint_tab')}
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index 34add05186a..eb6a8c88bfa 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -3178,13 +3178,14 @@ branches.no_support.header.text=Analyze each branch of your project separately w
branches.delete=Delete Branch
branches.delete.are_you_sure=Are you sure you want to delete branch "{0}"?
branches.rename=Rename Branch
+branches.manage=Manage branches
branches.orphan_branch=Orphan Branch
branches.orphan_branches=Orphan Branches
branches.orphan_branches.tooltip=When a target branch of a short-living branch was deleted, this short-living branch becomes orphan.
branches.main_branch=Main Branch
branches.branch_settings=Branch Settings
-branches.settings_hint=To administrate your project, you have to go to your main branch's {link} tab.
-branches.settings_hint_tab=Administration
+branches.settings_hint=To administrate your branches, you have to go to your main branch's {link} tab.
+branches.settings_hint_tab=Administration > Branches
#------------------------------------------------------------------------------