"keymaster": "1.6.2",
"lodash": "4.17.4",
"numeral": "1.5.3",
+ "prop-types": "15.5.10",
"rc-tooltip": "3.4.7",
"react": "15.6.1",
"react-dom": "15.6.1",
"less-loader": "4.0.4",
"postcss-loader": "2.0.6",
"prettier": "1.5.2",
- "prop-types": "15.5.10",
"react-dev-utils": "3.0.0",
"react-error-overlay": "1.0.7",
"react-test-renderer": "15.6.1",
"jest": {
"coverageDirectory": "<rootDir>/target/coverage",
"coveragePathIgnorePatterns": ["<rootDir>/node_modules", "<rootDir>/tests"],
+ "mapCoverage": true,
"moduleFileExtensions": ["ts", "tsx", "js", "json"],
"moduleNameMapper": {
"^.+\\.(hbs|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/config/jest/FileStub.js",
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { getJSON } from '../helpers/request';
+import { getJSON, post } from '../helpers/request';
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 deleteBranch(project: string, branch: string): Promise<void | Response> {
+ return post('/api/project_branches/delete', { project, branch }).catch(throwGlobalError);
+}
+
+export function renameBranch(project: string, branch: string): Promise<void | Response> {
+ return post('/api/project_branches/rename', { project, branch }).catch(throwGlobalError);
+}
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 => {
+ this.fetchBranches(component).then(branches => {
if (this.mounted) {
this.setState({ loading: false, branches, component });
}
}, onError);
}
+ fetchBranches = (component: Component) => {
+ const project = component.breadcrumbs.find((c: Component) => c.qualifier === 'TRK');
+ return project ? getBranches(project.key) : Promise.resolve([]);
+ };
+
handleProjectChange = (changes: {}) => {
if (this.mounted) {
this.setState(state => ({ component: { ...state.component, ...changes } }));
}
};
+ handleBranchesChange = () => {
+ if (this.mounted && this.state.component) {
+ this.fetchBranches(this.state.component).then(
+ branches => {
+ if (this.mounted) {
+ this.setState({ branches });
+ }
+ },
+ () => {}
+ );
+ }
+ };
+
render() {
const { query } = this.props.location;
const { branches, component, loading } = this.state;
/>}
{React.cloneElement(this.props.children, {
branch,
+ branches,
component: component,
+ onBranchesChange: this.handleBranchesChange,
onComponentChange: this.handleProjectChange
})}
</div>
expect(getComponentNavigation).toBeCalledWith('portfolioKey');
});
});
+
+it('updates branches on change', () => {
+ (getBranches as jest.Mock<any>).mockImplementation(() => Promise.resolve([]));
+ const Inner = () => <div />;
+ const wrapper = shallow(
+ <ProjectContainer location={{ query: { id: 'portfolioKey' } }}>
+ <Inner />
+ </ProjectContainer>
+ );
+ (wrapper.instance() as ProjectContainer).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');
+});
+++ /dev/null
-.branch-status {
- min-width: 64px;
- text-align: right;
-}
-
-.branch-status-indicator {
- display: block;
- width: 8px;
- height: 8px;
- border-radius: 8px;
- margin: 4px 0;
-}
-
-.branch-status-indicator.is-failed {
- background-color: #d4333f;
-}
-
-.branch-status-indicator.is-passed {
- background-color: #00aa00;
-}
+++ /dev/null
-/*
- * 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 * 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';
-import { isShortLivingBranch } from '../../../../helpers/branches';
-import './BranchStatus.css';
-
-interface Props {
- branch: Branch;
- concise?: boolean;
-}
-
-export default function BranchStatus({ branch, concise = false }: Props) {
- if (isShortLivingBranch(branch)) {
- if (!branch.status) {
- return null;
- }
-
- 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 &&
- <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} />;
- }
-}
<ComponentNavBranch
branches={this.props.branches}
currentBranch={this.props.currentBranch}
+ // to close dropdown on any location change
+ location={this.props.location}
project={this.props.component}
/>
branch={this.props.currentBranch}
component={this.props.component}
conf={this.props.conf}
+ // to re-render selected menu item
+ location={this.props.location}
/>
</ContextNavBar>
);
interface Props {
branches: Branch[];
currentBranch: Branch;
+ location?: any;
project: Component;
}
componentWillReceiveProps(nextProps: Props) {
if (
nextProps.project !== this.props.project ||
- nextProps.currentBranch !== this.props.currentBranch
+ nextProps.currentBranch !== this.props.currentBranch ||
+ nextProps.location !== this.props.location
) {
this.setState({ dropdownOpen: false, singleBranchPopupOpen: false });
}
} from '../../../../helpers/branches';
import { translate } from '../../../../helpers/l10n';
import { getProjectBranchUrl } from '../../../../helpers/urls';
+import { Link } from 'react-router';
interface Props {
branches: Branch[];
});
return (
- <ul className="menu">
+ <ul className="menu menu-vertically-limited">
{menu}
</ul>
);
};
render() {
+ const { project } = this.props;
+ const showManageLink =
+ project.qualifier === 'TRK' && project.configuration && project.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: project.key } }}>
+ {translate('branches.manage')}
+ </Link>
+ </div>}
</div>
);
}
import * as React from 'react';
import { Link } from 'react-router';
import * as classNames from 'classnames';
-import BranchStatus from './BranchStatus';
+import BranchStatus from '../../../../components/common/BranchStatus';
import { Branch, Component } from '../../../types';
import BranchIcon from '../../../../components/icons-components/BranchIcon';
import { isShortLivingBranch } from '../../../../helpers/branches';
import * as React from 'react';
import { Link } from 'react-router';
import * as classNames from 'classnames';
+import * as PropTypes from 'prop-types';
import { Branch, Component, ComponentExtension, ComponentConfiguration } from '../../../types';
import NavBarTabs from '../../../../components/nav/NavBarTabs';
import { isShortLivingBranch, getBranchName } from '../../../../helpers/branches';
const SETTINGS_URLS = [
'/project/admin',
+ '/project/branches',
'/project/settings',
'/project/quality_profiles',
'/project/quality_gate',
branch: Branch;
component: Component;
conf: ComponentConfiguration;
+ location?: any;
}
export default class ComponentNavMenu extends React.PureComponent<Props> {
+ static contextTypes = {
+ branchesEnabled: PropTypes.bool.isRequired
+ };
+
isProject() {
return this.props.component.qualifier === 'TRK';
}
renderAdministrationLinks() {
return [
this.renderSettingsLink(),
+ this.renderBranchesLink(),
this.renderProfilesLink(),
this.renderQualityGateLink(),
this.renderCustomMeasuresLink(),
);
}
+ renderBranchesLink() {
+ if (!this.context.branchesEnabled || !this.isProject() || !this.props.conf.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.props.conf.showQualityProfiles) {
return null;
*/
import * as React from 'react';
import IncrementalBadge from './IncrementalBadge';
-import BranchStatus from './BranchStatus';
+import BranchStatus from '../../../../components/common/BranchStatus';
import { Branch, Component, ComponentConfiguration } from '../../../types';
import Tooltip from '../../../../components/controls/Tooltip';
import PendingIcon from '../../../../components/icons-components/PendingIcon';
+++ /dev/null
-/*
- * 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 BranchStatus from '../BranchStatus';
-import { BranchType, LongLivingBranch } from '../../../../types';
-
-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();
- }
-});
-
-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();
- }
-});
extensions: [{ key: 'foo', name: 'Foo' }]
};
expect(
- shallow(<ComponentNavMenu branch={mainBranch} component={component as Component} conf={conf} />)
+ shallow(
+ <ComponentNavMenu branch={mainBranch} component={component as Component} conf={conf} />,
+ { context: { branchesEnabled: true } }
+ )
).toMatchSnapshot();
});
extensions: [{ key: 'foo', name: 'Foo' }, { key: 'bar', name: 'Bar' }]
};
expect(
- shallow(<ComponentNavMenu branch={mainBranch} component={component as Component} conf={conf} />)
+ shallow(
+ <ComponentNavMenu branch={mainBranch} component={component as Component} conf={conf} />,
+ { context: { branchesEnabled: true } }
+ )
).toMatchSnapshot();
});
const component = { key: 'foo', qualifier: 'TRK' } as Component;
const conf = { showSettings: true };
expect(
- shallow(<ComponentNavMenu branch={branch} component={component} conf={conf} />)
+ shallow(<ComponentNavMenu branch={branch} component={component} conf={conf} />, {
+ context: { branchesEnabled: true }
+ })
).toMatchSnapshot();
});
const component = { key: 'foo', qualifier: 'TRK' } as Component;
const conf = { showSettings: true };
expect(
- shallow(<ComponentNavMenu branch={branch} component={component} conf={conf} />)
+ shallow(<ComponentNavMenu branch={branch} component={component} conf={conf} />, {
+ context: { branchesEnabled: true }
+ })
).toMatchSnapshot();
});
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-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"
->
- <li>
- <i
- className="branch-status-indicator is-passed"
- />
- </li>
- <li>
- 0
- <BugIcon
- className="little-spacer-left"
- />
- </li>
- <li>
- 0
- <VulnerabilityIcon
- className="little-spacer-left"
- />
- </li>
- <li>
- 0
- <CodeSmellIcon
- className="little-spacer-left"
- />
- </li>
-</ul>
-`;
-
-exports[`renders status of short-living branches 2`] = `
-<ul
- className="list-inline branch-status"
->
- <li>
- <i
- className="branch-status-indicator is-failed"
- />
- </li>
- <li>
- 0
- <BugIcon
- className="little-spacer-left"
- />
- </li>
- <li>
- 0
- <VulnerabilityIcon
- className="little-spacer-left"
- />
- </li>
- <li>
- 1
- <CodeSmellIcon
- className="little-spacer-left"
- />
- </li>
-</ul>
-`;
-
-exports[`renders status of short-living branches 3`] = `
-<ul
- className="list-inline branch-status"
->
- <li>
- <i
- className="branch-status-indicator is-failed"
- />
- </li>
- <li>
- 7
- <BugIcon
- className="little-spacer-left"
- />
- </li>
- <li>
- 6
- <VulnerabilityIcon
- className="little-spacer-left"
- />
- </li>
- <li>
- 3
- <CodeSmellIcon
- className="little-spacer-left"
- />
- </li>
-</ul>
-`;
/>
</div>
<ul
- className="menu"
+ className="menu menu-vertically-limited"
>
<ComponentNavBranchesMenuItem
branch={
/>
</div>
<ul
- className="menu"
+ className="menu menu-vertically-limited"
>
<ComponentNavBranchesMenuItem
branch={
project_settings.page
</Link>
</li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/branches",
+ "query": Object {
+ "id": "foo",
+ },
+ }
+ }
+ >
+ project_branches.page
+ </Link>
+ </li>
<li>
<Link
activeClassName="active"
project_settings.page
</Link>
</li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/branches",
+ "query": Object {
+ "id": "foo",
+ },
+ }
+ }
+ >
+ project_branches.page
+ </Link>
+ </li>
<li>
<Link
activeClassName="active"
left: -5px;
}
-.navbar-search-shortcut-hint {
- line-height: 16px;
- margin-top: 5px;
- padding: 5px 10px;
- border-top: 1px solid #e6e6e6;
- background-color: #f3f3f3;
- color: #777;
- font-size: 11px;
-}
-
.navbar-search-no-results {
margin-top: 4px;
padding: 5px 10px;
overflow-y: auto;
overflow-x: hidden;
}
+
+.global-navbar-search-dropdown .dropdown-bottom-hint {
+ margin-bottom: 0;
+}
results={this.state.results}
selected={this.state.selected}
/>
- <div className="navbar-search-shortcut-hint">
+ <div className="dropdown-bottom-hint">
<div className="pull-right">
<ClockIcon className="little-spacer-right" size={12} />
{translate('recently_browsed')}
import permissionTemplatesRoutes from '../../apps/permission-templates/routes';
import projectActivityRoutes from '../../apps/projectActivity/routes';
import projectAdminRoutes from '../../apps/project-admin/routes';
+import projectBranchesRoutes from '../../apps/projectBranches/routes';
import projectsRoutes from '../../apps/projects/routes';
import projectsManagementRoutes from '../../apps/projectsManagement/routes';
import qualityGatesRoutes from '../../apps/quality-gates/routes';
component={ProjectPageExtension}
/>
<Route path="background_tasks" childRoutes={backgroundTasksRoutes} />
+ <Route path="branches" childRoutes={projectBranchesRoutes} />
<Route path="issues" childRoutes={issuesRoutes} />
<Route path="settings" childRoutes={settingsRoutes} />
{projectAdminRoutes}
--- /dev/null
+/*
+ * 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>
+ );
+}
--- /dev/null
+/*
+ * 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}
+ </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>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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 Modal from 'react-modal';
+import { deleteBranch } from '../../../api/branches';
+import { Branch } from '../../../app/types';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+interface Props {
+ branch: Branch;
+ component: string;
+ onClose: () => void;
+ onDelete: () => void;
+}
+
+interface State {
+ loading: boolean;
+}
+
+export default class DeleteBranchModal extends React.PureComponent<Props, State> {
+ mounted: boolean;
+ state: State = { loading: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
+ event.preventDefault();
+ this.setState({ loading: true });
+ deleteBranch(this.props.component, this.props.branch.name).then(
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ this.props.onDelete();
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ }
+ );
+ };
+
+ handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ this.props.onClose();
+ };
+
+ render() {
+ const { branch } = this.props;
+ const header = translate('branches.delete');
+
+ return (
+ <Modal
+ isOpen={true}
+ contentLabel={header}
+ className="modal"
+ overlayClassName="modal-overlay"
+ onRequestClose={this.props.onClose}>
+ <header className="modal-head">
+ <h2>
+ {header}
+ </h2>
+ </header>
+ <form onSubmit={this.handleSubmit}>
+ <div className="modal-body">
+ {translateWithParameters('branches.delete.are_you_sure', branch.name)}
+ </div>
+ <footer className="modal-foot">
+ {this.state.loading && <i className="spinner spacer-right" />}
+ <button className="button-red" disabled={this.state.loading} type="submit">
+ {translate('delete')}
+ </button>
+ <a href="#" onClick={this.handleCancelClick}>
+ {translate('cancel')}
+ </a>
+ </footer>
+ </form>
+ </Modal>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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 Modal from 'react-modal';
+import { renameBranch } from '../../../api/branches';
+import { Branch } from '../../../app/types';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ branch: Branch;
+ component: string;
+ onClose: () => void;
+ onRename: () => void;
+}
+
+interface State {
+ loading: boolean;
+ name?: string;
+}
+
+export default class RenameBranchModal extends React.PureComponent<Props, State> {
+ mounted: boolean;
+ state: State = { loading: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
+ event.preventDefault();
+ if (!this.state.name) {
+ return;
+ }
+ this.setState({ loading: true });
+ renameBranch(this.props.component, this.state.name).then(
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ this.props.onRename();
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ }
+ );
+ };
+
+ handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ this.props.onClose();
+ };
+
+ handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
+ this.setState({ name: event.currentTarget.value });
+ };
+
+ render() {
+ const { branch } = this.props;
+ const header = translate('branches.rename');
+ const submitDisabled =
+ this.state.loading || !this.state.name || this.state.name === branch.name;
+
+ return (
+ <Modal
+ isOpen={true}
+ contentLabel={header}
+ className="modal"
+ overlayClassName="modal-overlay"
+ onRequestClose={this.props.onClose}>
+ <header className="modal-head">
+ <h2>
+ {header}
+ </h2>
+ </header>
+ <form onSubmit={this.handleSubmit}>
+ <div className="modal-body">
+ <div className="modal-field">
+ <label htmlFor="rename-branch-name">
+ {translate('new_name')}
+ <em className="mandatory">*</em>
+ </label>
+ <input
+ autoFocus={true}
+ id="rename-branch-name"
+ maxLength={100}
+ name="name"
+ onChange={this.handleNameChange}
+ required={true}
+ size={50}
+ type="text"
+ value={this.state.name != undefined ? this.state.name : branch.name}
+ />
+ </div>
+ </div>
+ <footer className="modal-foot">
+ {this.state.loading && <i className="spinner spacer-right" />}
+ <button disabled={submitDisabled} type="submit">
+ {translate('rename')}
+ </button>
+ <a href="#" onClick={this.handleCancelClick}>
+ {translate('cancel')}
+ </a>
+ </footer>
+ </form>
+ </Modal>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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();
+});
--- /dev/null
+/*
+ * 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;
+}
--- /dev/null
+/*
+ * 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.
+ */
+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';
+
+beforeEach(() => {
+ (deleteBranch as jest.Mock<any>).mockClear();
+});
+
+it('renders', () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setState({ loading: true });
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('deletes branch', () => {
+ (deleteBranch as jest.Mock<any>).mockImplementation(() => Promise.resolve());
+ const onDelete = jest.fn();
+ const wrapper = shallowRender(onDelete);
+
+ submitForm(wrapper);
+
+ return doAsync().then(() => {
+ wrapper.update();
+ expect(wrapper.state().loading).toBe(false);
+ expect(onDelete).toBeCalled();
+ expect(deleteBranch).toBeCalledWith('foo', 'feature');
+ });
+});
+
+it('cancels', () => {
+ const onClose = jest.fn();
+ const wrapper = shallowRender(jest.fn(), onClose);
+
+ click(wrapper.find('a'));
+
+ return doAsync().then(() => {
+ expect(onClose).toBeCalled();
+ });
+});
+
+it('stops loading on WS error', () => {
+ (deleteBranch as jest.Mock<any>).mockImplementation(() => Promise.reject(null));
+ const onDelete = jest.fn();
+ const wrapper = shallowRender(onDelete);
+
+ submitForm(wrapper);
+
+ return doAsync().then(() => {
+ wrapper.update();
+ expect(wrapper.state().loading).toBe(false);
+ expect(onDelete).not.toBeCalled();
+ expect(deleteBranch).toBeCalledWith('foo', 'feature');
+ });
+});
+
+function shallowRender(onDelete: () => void = jest.fn(), onClose: () => void = jest.fn()) {
+ const branch: ShortLivingBranch = {
+ isMain: false,
+ name: 'feature',
+ mergeBranch: 'master',
+ type: BranchType.SHORT
+ };
+ const wrapper = shallow(
+ <DeleteBranchModal branch={branch} component="foo" onClose={onClose} onDelete={onDelete} />
+ );
+ (wrapper.instance() as any).mounted = true;
+ return wrapper;
+}
+
+function submitForm(wrapper: ShallowWrapper<any, any>) {
+ submit(wrapper.find('form'));
+ expect(wrapper.state().loading).toBe(true);
+}
--- /dev/null
+/*
+ * 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.
+ */
+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';
+
+beforeEach(() => {
+ (renameBranch as jest.Mock<any>).mockClear();
+});
+
+it('renders', () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setState({ name: 'dev' });
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setState({ loading: true });
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('renames branch', () => {
+ (renameBranch as jest.Mock<any>).mockImplementation(() => Promise.resolve());
+ const onRename = jest.fn();
+ const wrapper = shallowRender(onRename);
+
+ fillAndSubmit(wrapper);
+
+ return doAsync().then(() => {
+ wrapper.update();
+ expect(wrapper.state().loading).toBe(false);
+ expect(onRename).toBeCalled();
+ expect(renameBranch).toBeCalledWith('foo', 'dev');
+ });
+});
+
+it('cancels', () => {
+ const onClose = jest.fn();
+ const wrapper = shallowRender(jest.fn(), onClose);
+
+ click(wrapper.find('a'));
+
+ return doAsync().then(() => {
+ expect(onClose).toBeCalled();
+ });
+});
+
+it('stops loading on WS error', () => {
+ (renameBranch as jest.Mock<any>).mockImplementation(() => Promise.reject(null));
+ const onRename = jest.fn();
+ const wrapper = shallowRender(onRename);
+
+ fillAndSubmit(wrapper);
+
+ return doAsync().then(() => {
+ wrapper.update();
+ expect(wrapper.state().loading).toBe(false);
+ expect(onRename).not.toBeCalled();
+ });
+});
+
+function shallowRender(onRename: () => void = jest.fn(), onClose: () => void = jest.fn()) {
+ const branch: MainBranch = { isMain: true, name: 'master' };
+ const wrapper = shallow(
+ <RenameBranchModal branch={branch} component="foo" onClose={onClose} onRename={onRename} />
+ );
+ (wrapper.instance() as any).mounted = true;
+ return wrapper;
+}
+
+function fillAndSubmit(wrapper: ShallowWrapper<any, any>) {
+ change(wrapper.find('input'), 'dev');
+ submit(wrapper.find('form'));
+ expect(wrapper.state().loading).toBe(true);
+}
--- /dev/null
+// 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>
+`;
--- /dev/null
+// 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
+ </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>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<Modal
+ ariaHideApp={true}
+ bodyOpenClassName="ReactModal__Body--open"
+ className="modal"
+ closeTimeoutMS={0}
+ contentLabel="branches.delete"
+ isOpen={true}
+ onRequestClose={[Function]}
+ overlayClassName="modal-overlay"
+ parentSelector={[Function]}
+ portalClassName="ReactModalPortal"
+ shouldCloseOnOverlayClick={true}
+>
+ <header
+ className="modal-head"
+ >
+ <h2>
+ branches.delete
+ </h2>
+ </header>
+ <form
+ onSubmit={[Function]}
+ >
+ <div
+ className="modal-body"
+ >
+ branches.delete.are_you_sure.feature
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <button
+ className="button-red"
+ disabled={false}
+ type="submit"
+ >
+ delete
+ </button>
+ <a
+ href="#"
+ onClick={[Function]}
+ >
+ cancel
+ </a>
+ </footer>
+ </form>
+</Modal>
+`;
+
+exports[`renders 2`] = `
+<Modal
+ ariaHideApp={true}
+ bodyOpenClassName="ReactModal__Body--open"
+ className="modal"
+ closeTimeoutMS={0}
+ contentLabel="branches.delete"
+ isOpen={true}
+ onRequestClose={[Function]}
+ overlayClassName="modal-overlay"
+ parentSelector={[Function]}
+ portalClassName="ReactModalPortal"
+ shouldCloseOnOverlayClick={true}
+>
+ <header
+ className="modal-head"
+ >
+ <h2>
+ branches.delete
+ </h2>
+ </header>
+ <form
+ onSubmit={[Function]}
+ >
+ <div
+ className="modal-body"
+ >
+ branches.delete.are_you_sure.feature
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <i
+ className="spinner spacer-right"
+ />
+ <button
+ className="button-red"
+ disabled={true}
+ type="submit"
+ >
+ delete
+ </button>
+ <a
+ href="#"
+ onClick={[Function]}
+ >
+ cancel
+ </a>
+ </footer>
+ </form>
+</Modal>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<Modal
+ ariaHideApp={true}
+ bodyOpenClassName="ReactModal__Body--open"
+ className="modal"
+ closeTimeoutMS={0}
+ contentLabel="branches.rename"
+ isOpen={true}
+ onRequestClose={[Function]}
+ overlayClassName="modal-overlay"
+ parentSelector={[Function]}
+ portalClassName="ReactModalPortal"
+ shouldCloseOnOverlayClick={true}
+>
+ <header
+ className="modal-head"
+ >
+ <h2>
+ branches.rename
+ </h2>
+ </header>
+ <form
+ onSubmit={[Function]}
+ >
+ <div
+ className="modal-body"
+ >
+ <div
+ className="modal-field"
+ >
+ <label
+ htmlFor="rename-branch-name"
+ >
+ new_name
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </label>
+ <input
+ autoFocus={true}
+ id="rename-branch-name"
+ maxLength={100}
+ name="name"
+ onChange={[Function]}
+ required={true}
+ size={50}
+ type="text"
+ value="master"
+ />
+ </div>
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <button
+ disabled={true}
+ type="submit"
+ >
+ rename
+ </button>
+ <a
+ href="#"
+ onClick={[Function]}
+ >
+ cancel
+ </a>
+ </footer>
+ </form>
+</Modal>
+`;
+
+exports[`renders 2`] = `
+<Modal
+ ariaHideApp={true}
+ bodyOpenClassName="ReactModal__Body--open"
+ className="modal"
+ closeTimeoutMS={0}
+ contentLabel="branches.rename"
+ isOpen={true}
+ onRequestClose={[Function]}
+ overlayClassName="modal-overlay"
+ parentSelector={[Function]}
+ portalClassName="ReactModalPortal"
+ shouldCloseOnOverlayClick={true}
+>
+ <header
+ className="modal-head"
+ >
+ <h2>
+ branches.rename
+ </h2>
+ </header>
+ <form
+ onSubmit={[Function]}
+ >
+ <div
+ className="modal-body"
+ >
+ <div
+ className="modal-field"
+ >
+ <label
+ htmlFor="rename-branch-name"
+ >
+ new_name
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </label>
+ <input
+ autoFocus={true}
+ id="rename-branch-name"
+ maxLength={100}
+ name="name"
+ onChange={[Function]}
+ required={true}
+ size={50}
+ type="text"
+ value="dev"
+ />
+ </div>
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <button
+ disabled={false}
+ type="submit"
+ >
+ rename
+ </button>
+ <a
+ href="#"
+ onClick={[Function]}
+ >
+ cancel
+ </a>
+ </footer>
+ </form>
+</Modal>
+`;
+
+exports[`renders 3`] = `
+<Modal
+ ariaHideApp={true}
+ bodyOpenClassName="ReactModal__Body--open"
+ className="modal"
+ closeTimeoutMS={0}
+ contentLabel="branches.rename"
+ isOpen={true}
+ onRequestClose={[Function]}
+ overlayClassName="modal-overlay"
+ parentSelector={[Function]}
+ portalClassName="ReactModalPortal"
+ shouldCloseOnOverlayClick={true}
+>
+ <header
+ className="modal-head"
+ >
+ <h2>
+ branches.rename
+ </h2>
+ </header>
+ <form
+ onSubmit={[Function]}
+ >
+ <div
+ className="modal-body"
+ >
+ <div
+ className="modal-field"
+ >
+ <label
+ htmlFor="rename-branch-name"
+ >
+ new_name
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </label>
+ <input
+ autoFocus={true}
+ id="rename-branch-name"
+ maxLength={100}
+ name="name"
+ onChange={[Function]}
+ required={true}
+ size={50}
+ type="text"
+ value="dev"
+ />
+ </div>
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <i
+ className="spinner spacer-right"
+ />
+ <button
+ disabled={true}
+ type="submit"
+ >
+ rename
+ </button>
+ <a
+ href="#"
+ onClick={[Function]}
+ >
+ cancel
+ </a>
+ </footer>
+ </form>
+</Modal>
+`;
--- /dev/null
+/*
+ * 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;
--- /dev/null
+.branch-status {
+ min-width: 64px;
+ text-align: right;
+}
+
+.branch-status-indicator {
+ display: block;
+ width: 8px;
+ height: 8px;
+ border-radius: 8px;
+ margin: 4px 0;
+}
+
+.branch-status-indicator.is-failed {
+ background-color: #d4333f;
+}
+
+.branch-status-indicator.is-passed {
+ background-color: #00aa00;
+}
--- /dev/null
+/*
+ * 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 * as classNames from 'classnames';
+import { Branch } from '../../app/types';
+import Level from '../ui/Level';
+import BugIcon from '../icons-components/BugIcon';
+import CodeSmellIcon from '../icons-components/CodeSmellIcon';
+import VulnerabilityIcon from '../icons-components/VulnerabilityIcon';
+import { isShortLivingBranch } from '../../helpers/branches';
+import './BranchStatus.css';
+
+interface Props {
+ branch: Branch;
+ concise?: boolean;
+}
+
+export default function BranchStatus({ branch, concise = false }: Props) {
+ if (isShortLivingBranch(branch)) {
+ if (!branch.status) {
+ return null;
+ }
+
+ 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 &&
+ <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} />;
+ }
+}
--- /dev/null
+/*
+ * 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 BranchStatus from '../BranchStatus';
+import { BranchType, LongLivingBranch } from '../../../app/types';
+
+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();
+ }
+});
+
+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();
+ }
+});
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+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"
+>
+ <li>
+ <i
+ className="branch-status-indicator is-passed"
+ />
+ </li>
+ <li>
+ 0
+ <BugIcon
+ className="little-spacer-left"
+ />
+ </li>
+ <li>
+ 0
+ <VulnerabilityIcon
+ className="little-spacer-left"
+ />
+ </li>
+ <li>
+ 0
+ <CodeSmellIcon
+ className="little-spacer-left"
+ />
+ </li>
+</ul>
+`;
+
+exports[`renders status of short-living branches 2`] = `
+<ul
+ className="list-inline branch-status"
+>
+ <li>
+ <i
+ className="branch-status-indicator is-failed"
+ />
+ </li>
+ <li>
+ 0
+ <BugIcon
+ className="little-spacer-left"
+ />
+ </li>
+ <li>
+ 0
+ <VulnerabilityIcon
+ className="little-spacer-left"
+ />
+ </li>
+ <li>
+ 1
+ <CodeSmellIcon
+ className="little-spacer-left"
+ />
+ </li>
+</ul>
+`;
+
+exports[`renders status of short-living branches 3`] = `
+<ul
+ className="list-inline branch-status"
+>
+ <li>
+ <i
+ className="branch-status-indicator is-failed"
+ />
+ </li>
+ <li>
+ 7
+ <BugIcon
+ className="little-spacer-left"
+ />
+ </li>
+ <li>
+ 6
+ <VulnerabilityIcon
+ className="little-spacer-left"
+ />
+ </li>
+ <li>
+ 3
+ <CodeSmellIcon
+ className="little-spacer-left"
+ />
+ </li>
+</ul>
+`;
+++ /dev/null
-/*
- * 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.
- */
-// @flow
-import React from 'react';
-
-/*::
-type Props = { className?: string, size?: number };
-*/
-
-export default function ChangeIcon({ className, size = 12 } /*: Props */) {
- /* eslint-disable max-len */
- return (
- <svg
- className={className}
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 14 14"
- width={size}
- height={size}>
- <path
- fill="#236a97"
- d="M3.35 12.82l.85-.84L2.02 9.8l-.84.85v.98h1.2v1.2h.97zM8.2 4.24c0-.13-.08-.2-.22-.2-.06 0-.1.02-.15.06l-5 5c-.05.05-.08.1-.08.17 0 .13.07.2.2.2.07 0 .12-.02.16-.06l5.02-5c.05-.04.07-.1.07-.16zm-.5-1.77l3.83 3.84-7.7 7.7H0v-3.84l7.7-7.7zm6.3.88c0 .33-.1.6-.34.84L12.12 5.7 8.28 1.88 9.8.35c.24-.23.5-.35.85-.35.32 0 .6.12.84.35l2.16 2.16c.23.25.34.53.34.85z"
- />
- </svg>
- );
-}
--- /dev/null
+/*
+ * 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;
+ size?: number;
+}
+
+export default function ChangeIcon({ className, size = 12 }: Props) {
+ return (
+ <svg
+ className={className}
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 14 14"
+ width={size}
+ height={size}>
+ <path
+ fill="#236a97"
+ d="M3.35 12.82l.85-.84L2.02 9.8l-.84.85v.98h1.2v1.2h.97zM8.2 4.24c0-.13-.08-.2-.22-.2-.06 0-.1.02-.15.06l-5 5c-.05.05-.08.1-.08.17 0 .13.07.2.2.2.07 0 .12-.02.16-.06l5.02-5c.05-.04.07-.1.07-.16zm-.5-1.77l3.83 3.84-7.7 7.7H0v-3.84l7.7-7.7zm6.3.88c0 .33-.1.6-.34.84L12.12 5.7 8.28 1.88 9.8.35c.24-.23.5-.35.85-.35.32 0 .6.12.84.35l2.16 2.16c.23.25.34.53.34.85z"
+ />
+ </svg>
+ );
+}
+++ /dev/null
-/*
- * 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.
- */
-// @flow
-import React from 'react';
-
-/*::
-type Props = { className?: string, size?: number };
-*/
-
-export default function DeleteIcon({ className, size = 12 } /*: Props */) {
- /* eslint-disable max-len */
- return (
- <svg
- className={className}
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 14 14"
- width={size}
- height={size}>
- <path
- fill="#d4333f"
- d="M14 11.27c0 .3-.1.58-.33.8l-1.6 1.6c-.22.22-.5.33-.8.33-.32 0-.6-.1-.8-.33L7 10.2l-3.46 3.47c-.22.22-.5.33-.8.33-.32 0-.6-.1-.8-.33l-1.6-1.6c-.23-.22-.34-.5-.34-.8 0-.32.1-.6.33-.8L3.8 7 .32 3.54C.1 3.32 0 3.04 0 2.74c0-.32.1-.6.33-.8l1.6-1.6c.22-.23.5-.34.8-.34.32 0 .6.1.8.33L7 3.8 10.46.32c.22-.22.5-.33.8-.33.32 0 .6.1.8.33l1.6 1.6c.23.22.34.5.34.8 0 .32-.1.6-.33.8L10.2 7l3.47 3.46c.22.22.33.5.33.8z"
- />
- </svg>
- );
-}
--- /dev/null
+/*
+ * 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;
+ size?: number;
+}
+
+export default function DeleteIcon({ className, size = 12 }: Props) {
+ return (
+ <svg
+ className={className}
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 14 14"
+ width={size}
+ height={size}>
+ <path
+ fill="#d4333f"
+ d="M14 11.27c0 .3-.1.58-.33.8l-1.6 1.6c-.22.22-.5.33-.8.33-.32 0-.6-.1-.8-.33L7 10.2l-3.46 3.47c-.22.22-.5.33-.8.33-.32 0-.6-.1-.8-.33l-1.6-1.6c-.23-.22-.34-.5-.34-.8 0-.32.1-.6.33-.8L3.8 7 .32 3.54C.1 3.32 0 3.04 0 2.74c0-.32.1-.6.33-.8l1.6-1.6c.22-.23.5-.34.8-.34.32 0 .6.1.8.33L7 3.8 10.46.32c.22-.22.5-.33.8-.33.32 0 .6.1.8.33l1.6 1.6c.23.22.34.5.34.8 0 .32-.1.6-.33.8L10.2 7l3.47 3.46c.22.22.33.5.33.8z"
+ />
+ </svg>
+ );
+}
* Shortcut to do a POST request
*/
export function post(url: string, data?: RequestData): Promise<void> {
- return new Promise(resolve => {
+ return new Promise((resolve, reject) => {
request(url).setMethod('POST').setData(data).submit().then(checkStatus).then(() => {
resolve();
- });
+ }, reject);
});
}
top: 0;
z-index: (1000 - 10);
}
+
+.dropdown-bottom-hint {
+ line-height: 16px;
+ margin-top: 5px;
+ margin-bottom: -5px;
+ padding: 5px 10px;
+ border-top: 1px solid #e6e6e6;
+ background-color: #f3f3f3;
+ color: #777;
+ font-size: 11px;
+}
}
}
+ .menu-vertically-limited {
+ max-height: 300px;
+ overflow-y: auto;
+ }
+
.menu-footer > a > span {
border-bottom: 1px solid @darkGrey;
color: @secondFontColor;
name_too_long_x=Name is too long (maximum is {0} characters)
navigation=Navigation
never=Never
+new_name=New name
none=None
no_tags=No tags
off=Off
application_deletion.page.description=Delete this application from SonarQube. Application projects will not be deleted. This operation cannot be undone.
provisioning.page=Provisioning
provisioning.page.description=Use this page to initialize projects if you would like to configure them before the first analysis. Once a project is provisioned, you have access to perform all project configurations on it.
+project_branches.page=Branches
#------------------------------------------------------------------------------
#
branches.learn_how_to_analyze.text=Quickly setup branch analysis and get separate insights for each of your branches and pull requests.
branches.no_support.header=Get the most out of SonarQube with branches analysis
branches.no_support.header.text=Analyze each branch of your project separately with our Developer Pack.
+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