]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9756 Build UI for branch management (#2433)
authorStas Vilchik <stas.vilchik@sonarsource.com>
Mon, 28 Aug 2017 09:43:35 +0000 (11:43 +0200)
committerJanos Gyerik <janos.gyerik@sonarsource.com>
Tue, 12 Sep 2017 09:34:54 +0000 (11:34 +0200)
45 files changed:
server/sonar-web/package.json
server/sonar-web/src/main/js/api/branches.ts
server/sonar-web/src/main/js/app/components/ProjectContainer.tsx
server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.css [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/BranchStatus-test.tsx [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/BranchStatus-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
server/sonar-web/src/main/js/app/components/search/Search.css
server/sonar-web/src/main/js/app/components/search/Search.js
server/sonar-web/src/main/js/app/utils/startReactApp.js
server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectBranches/components/DeleteBranchModal.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/DeleteBranchModal-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/RenameBranchModal-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/DeleteBranchModal-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/RenameBranchModal-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectBranches/routes.ts [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/BranchStatus.css [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/BranchStatus.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/__tests__/BranchStatus-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchStatus-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/icons-components/ChangeIcon.js [deleted file]
server/sonar-web/src/main/js/components/icons-components/ChangeIcon.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/icons-components/DeleteIcon.js [deleted file]
server/sonar-web/src/main/js/components/icons-components/DeleteIcon.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/request.ts
server/sonar-web/src/main/less/components/dropdowns.less
server/sonar-web/src/main/less/components/menu.less
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 679b0c8538aea721e96f5684190006ad7da2d6c2..26755ecd3c8ead250d7532e179bb98ef54cdc266 100644 (file)
@@ -26,6 +26,7 @@
     "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",
@@ -96,7 +97,6 @@
     "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",
index 6435c575aaea4cc2ae4e65c2180af9abacc894ec..ec3e79e093238e7a6014c7b4c48cd01e23c9bdca 100644 (file)
  * 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);
+}
index f7ae6453557a16212958ff6c4643449552f8621f..804c420ee88f3d65dba77b69b591240920d6f04a 100644 (file)
@@ -83,9 +83,7 @@ export default class ProjectContainer extends React.PureComponent<Props, State>
 
     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 });
         }
@@ -93,12 +91,30 @@ export default class ProjectContainer extends React.PureComponent<Props, State>
     }, 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;
@@ -128,7 +144,9 @@ export default class ProjectContainer extends React.PureComponent<Props, State>
           />}
         {React.cloneElement(this.props.children, {
           branch,
+          branches,
           component: component,
+          onBranchesChange: this.handleBranchesChange,
           onComponentChange: this.handleProjectChange
         })}
       </div>
index beb6580feb8f5a8a330b56c499cbdf372ca657b4..ee31dea0b0c418f4c5b05f230127595a99d1fe14 100644 (file)
@@ -100,3 +100,21 @@ it("doesn't load branches portfolio", () => {
     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');
+});
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.css b/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.css
deleted file mode 100644 (file)
index 74278d6..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-.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;
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.tsx b/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.tsx
deleted file mode 100644 (file)
index 9a7937d..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * 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} />;
-  }
-}
index 189ace21d11f64ee828b26189eceed160a79435e..d1b95c1a06f351787d9106ebb441e6ec44abf2c3 100644 (file)
@@ -102,6 +102,8 @@ export default class ComponentNav extends React.PureComponent<Props, State> {
         <ComponentNavBranch
           branches={this.props.branches}
           currentBranch={this.props.currentBranch}
+          // to close dropdown on any location change
+          location={this.props.location}
           project={this.props.component}
         />
 
@@ -116,6 +118,8 @@ export default class ComponentNav extends React.PureComponent<Props, State> {
           branch={this.props.currentBranch}
           component={this.props.component}
           conf={this.props.conf}
+          // to re-render selected menu item
+          location={this.props.location}
         />
       </ContextNavBar>
     );
index 1a99483710fa86b4eda580d4325fc1193d9c5206..cf293b64b4a567b10d3b78b85f63caabac632719 100644 (file)
@@ -33,6 +33,7 @@ import BubblePopupHelper from '../../../../components/common/BubblePopupHelper';
 interface Props {
   branches: Branch[];
   currentBranch: Branch;
+  location?: any;
   project: Component;
 }
 
@@ -61,7 +62,8 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State
   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 });
     }
index 261abe6dd2b821263e4c96aaf578e28d5d87f59d..e58e796e9a0e8e11700f99e0bbd5ef341f691240 100644 (file)
@@ -28,6 +28,7 @@ import {
 } from '../../../../helpers/branches';
 import { translate } from '../../../../helpers/l10n';
 import { getProjectBranchUrl } from '../../../../helpers/urls';
+import { Link } from 'react-router';
 
 interface Props {
   branches: Branch[];
@@ -179,17 +180,29 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
     });
 
     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>
     );
   }
index 9d218df274f776f6ed578c35d32308a8d299b397..ece4360a4dc266d2b043f6eb870a0a4f300de3e0 100644 (file)
@@ -20,7 +20,7 @@
 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';
index f0edfde9eab3dce33cdd846c234661c177c76c98..3c1a4f512b790e9016f5954c368ab7a122fba44a 100644 (file)
@@ -20,6 +20,7 @@
 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';
@@ -27,6 +28,7 @@ import { translate } from '../../../../helpers/l10n';
 
 const SETTINGS_URLS = [
   '/project/admin',
+  '/project/branches',
   '/project/settings',
   '/project/quality_profiles',
   '/project/quality_gate',
@@ -43,9 +45,14 @@ interface Props {
   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';
   }
@@ -196,6 +203,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
   renderAdministrationLinks() {
     return [
       this.renderSettingsLink(),
+      this.renderBranchesLink(),
       this.renderProfilesLink(),
       this.renderQualityGateLink(),
       this.renderCustomMeasuresLink(),
@@ -223,6 +231,21 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
     );
   }
 
+  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;
index 00c4db25561b137664727ea8e11511bce45310a2..07d5cbc5bbf234c9b158580a1be60cb197723376 100644 (file)
@@ -19,7 +19,7 @@
  */
 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';
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/BranchStatus-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/BranchStatus-test.tsx
deleted file mode 100644 (file)
index be77985..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * 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();
-  }
-});
index 2b6574a85edf6b2198710f213be3b596a77821fc..c69eda2ef7a460a0934e1e6cc284d073d86f3db7 100644 (file)
@@ -44,7 +44,10 @@ it('should work with extensions', () => {
     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();
 });
 
@@ -62,7 +65,10 @@ it('should work with multiple extensions', () => {
     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();
 });
 
@@ -77,7 +83,9 @@ it('should work for short-living branches', () => {
   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();
 });
 
@@ -86,6 +94,8 @@ it('should work for long-living branches', () => {
   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();
 });
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/BranchStatus-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/BranchStatus-test.tsx.snap
deleted file mode 100644 (file)
index 1f4ccfc..0000000
+++ /dev/null
@@ -1,107 +0,0 @@
-// 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>
-`;
index d80344beadd8935bcb2f4de1b77916b2ced2aec3..2974d68eb6103d5a889c9fdf8cdca28fe1f24e5a 100644 (file)
@@ -25,7 +25,7 @@ exports[`renders list 1`] = `
     />
   </div>
   <ul
-    className="menu"
+    className="menu menu-vertically-limited"
   >
     <ComponentNavBranchesMenuItem
       branch={
@@ -165,7 +165,7 @@ exports[`searches 1`] = `
     />
   </div>
   <ul
-    className="menu"
+    className="menu menu-vertically-limited"
   >
     <ComponentNavBranchesMenuItem
       branch={
index ab9cc70c6b1b70698004669209269be605087442..f406307cd81c681c49236a60a27f66f2fd841b48 100644 (file)
@@ -266,6 +266,23 @@ exports[`should work with extensions 1`] = `
           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"
@@ -470,6 +487,23 @@ exports[`should work with multiple extensions 1`] = `
           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"
index 7cdd0a20035c900bd1af99e4115d4554570e3d78..3799637ce66f9c7a4c1ec2930d0cf742fa5d7053 100644 (file)
   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;
@@ -94,3 +84,7 @@
   overflow-y: auto;
   overflow-x: hidden;
 }
+
+.global-navbar-search-dropdown .dropdown-bottom-hint {
+  margin-bottom: 0;
+}
index ea4592d3992813572fdcea3bf3f2d6481ffc1b6e..e28606b0d7dcc2193e045bf0ff6f5479e20e32d9 100644 (file)
@@ -367,7 +367,7 @@ export default class Search extends React.PureComponent {
               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')}
index 910761f150c07af808e5b61e260d3031cfca22b9..af23e36912b66009dc75869a0b15e70132f88821 100644 (file)
@@ -55,6 +55,7 @@ import organizationsRoutes from '../../apps/organizations/routes';
 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';
@@ -187,6 +188,7 @@ const startReactApp = () => {
                         component={ProjectPageExtension}
                       />
                       <Route path="background_tasks" childRoutes={backgroundTasksRoutes} />
+                      <Route path="branches" childRoutes={projectBranchesRoutes} />
                       <Route path="issues" childRoutes={issuesRoutes} />
                       <Route path="settings" childRoutes={settingsRoutes} />
                       {projectAdminRoutes}
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 (file)
index 0000000..442f2f2
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * 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 (file)
index 0000000..b8bba66
--- /dev/null
@@ -0,0 +1,138 @@
+/*
+ * 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>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/DeleteBranchModal.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/DeleteBranchModal.tsx
new file mode 100644 (file)
index 0000000..2e51b55
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+ * 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>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx
new file mode 100644 (file)
index 0000000..bcc8eed
--- /dev/null
@@ -0,0 +1,131 @@
+/*
+ * 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>
+    );
+  }
+}
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 (file)
index 0000000..4288105
--- /dev/null
@@ -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 (file)
index 0000000..4edc3ce
--- /dev/null
@@ -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/apps/projectBranches/components/__tests__/DeleteBranchModal-test.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/DeleteBranchModal-test.tsx
new file mode 100644 (file)
index 0000000..b287058
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * 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);
+}
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/RenameBranchModal-test.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/RenameBranchModal-test.tsx
new file mode 100644 (file)
index 0000000..3a1c962
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * 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);
+}
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 (file)
index 0000000..6f983e3
--- /dev/null
@@ -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 (file)
index 0000000..f31097d
--- /dev/null
@@ -0,0 +1,95 @@
+// 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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/DeleteBranchModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/DeleteBranchModal-test.tsx.snap
new file mode 100644 (file)
index 0000000..934f8ed
--- /dev/null
@@ -0,0 +1,104 @@
+// 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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/RenameBranchModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/RenameBranchModal-test.tsx.snap
new file mode 100644 (file)
index 0000000..7867fa4
--- /dev/null
@@ -0,0 +1,223 @@
+// 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>
+`;
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 (file)
index 0000000..520805e
--- /dev/null
@@ -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/components/common/BranchStatus.css b/server/sonar-web/src/main/js/components/common/BranchStatus.css
new file mode 100644 (file)
index 0000000..74278d6
--- /dev/null
@@ -0,0 +1,20 @@
+.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;
+}
diff --git a/server/sonar-web/src/main/js/components/common/BranchStatus.tsx b/server/sonar-web/src/main/js/components/common/BranchStatus.tsx
new file mode 100644 (file)
index 0000000..ae36462
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * 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} />;
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/BranchStatus-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/BranchStatus-test.tsx
new file mode 100644 (file)
index 0000000..7b2de5d
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * 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();
+  }
+});
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchStatus-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchStatus-test.tsx.snap
new file mode 100644 (file)
index 0000000..1f4ccfc
--- /dev/null
@@ -0,0 +1,107 @@
+// 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>
+`;
diff --git a/server/sonar-web/src/main/js/components/icons-components/ChangeIcon.js b/server/sonar-web/src/main/js/components/icons-components/ChangeIcon.js
deleted file mode 100644 (file)
index e6b7498..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * 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>
-  );
-}
diff --git a/server/sonar-web/src/main/js/components/icons-components/ChangeIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/ChangeIcon.tsx
new file mode 100644 (file)
index 0000000..5991814
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * 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>
+  );
+}
diff --git a/server/sonar-web/src/main/js/components/icons-components/DeleteIcon.js b/server/sonar-web/src/main/js/components/icons-components/DeleteIcon.js
deleted file mode 100644 (file)
index 9c90a1a..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * 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>
-  );
-}
diff --git a/server/sonar-web/src/main/js/components/icons-components/DeleteIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/DeleteIcon.tsx
new file mode 100644 (file)
index 0000000..08a4381
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * 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>
+  );
+}
index 4bac03dadd68a31f9afa5180bd76a70a1491d39c..1acb519e00013806e48ad667f4037f9fd8be5825 100644 (file)
@@ -170,10 +170,10 @@ export function postJSON(url: string, data?: RequestData): Promise<any> {
  * 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);
   });
 }
 
index 6213f652aa469d004249f033757482aeb50202c2..53371a95b8f0c4d1814455fd69946b5df9496913 100644 (file)
   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;
+}
index bf751939d5d783f09faad8370f997477287306cf..ba566680624cc21a650537a5a3645739c472bec4 100644 (file)
     }
   }
 
+  .menu-vertically-limited {
+    max-height: 300px;
+    overflow-y: auto;
+  }
+
   .menu-footer > a > span {
     border-bottom: 1px solid @darkGrey;
     color: @secondFontColor;
index bd67305da2648d5c839df925bc18cd62764545a3..87353b720dd3de4d74881d38581f16d5d4609aa5 100644 (file)
@@ -112,6 +112,7 @@ name=Name
 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
@@ -587,6 +588,7 @@ portfolio_deletion.page.description=Delete this portfolio from SonarQube. Compon
 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
 
 #------------------------------------------------------------------------------
 #
@@ -3164,3 +3166,7 @@ branches.learn_how_to_analyze=Learn how to analyze branches in SonarQube
 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