From: Stas Vilchik Date: Mon, 28 Aug 2017 09:43:35 +0000 (+0200) Subject: SONAR-9756 Build UI for branch management (#2433) X-Git-Tag: 6.6-RC1~380^2~40 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=25140ec8ed74cc5ea5f50f05325f0d4a7f8753fd;p=sonarqube.git SONAR-9756 Build UI for branch management (#2433) --- diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index 679b0c8538a..26755ecd3c8 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -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", @@ -132,6 +132,7 @@ "jest": { "coverageDirectory": "/target/coverage", "coveragePathIgnorePatterns": ["/node_modules", "/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)$": "/config/jest/FileStub.js", diff --git a/server/sonar-web/src/main/js/api/branches.ts b/server/sonar-web/src/main/js/api/branches.ts index 6435c575aae..ec3e79e0932 100644 --- a/server/sonar-web/src/main/js/api/branches.ts +++ b/server/sonar-web/src/main/js/api/branches.ts @@ -17,9 +17,17 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { getJSON } from '../helpers/request'; +import { getJSON, post } from '../helpers/request'; import throwGlobalError from '../app/utils/throwGlobalError'; export function getBranches(project: string): Promise { return getJSON('/api/project_branches/list', { project }).then(r => r.branches, throwGlobalError); } + +export function deleteBranch(project: string, branch: string): Promise { + return post('/api/project_branches/delete', { project, branch }).catch(throwGlobalError); +} + +export function renameBranch(project: string, branch: string): Promise { + return post('/api/project_branches/rename', { project, branch }).catch(throwGlobalError); +} diff --git a/server/sonar-web/src/main/js/app/components/ProjectContainer.tsx b/server/sonar-web/src/main/js/app/components/ProjectContainer.tsx index f7ae6453557..804c420ee88 100644 --- a/server/sonar-web/src/main/js/app/components/ProjectContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/ProjectContainer.tsx @@ -83,9 +83,7 @@ export default class ProjectContainer extends React.PureComponent 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 }, 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 />} {React.cloneElement(this.props.children, { branch, + branches, component: component, + onBranchesChange: this.handleBranchesChange, onComponentChange: this.handleProjectChange })} diff --git a/server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx index beb6580feb8..ee31dea0b0c 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx @@ -100,3 +100,21 @@ it("doesn't load branches portfolio", () => { expect(getComponentNavigation).toBeCalledWith('portfolioKey'); }); }); + +it('updates branches on change', () => { + (getBranches as jest.Mock).mockImplementation(() => Promise.resolve([])); + const Inner = () =>
; + const wrapper = shallow( + + + + ); + (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 index 74278d67573..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.css +++ /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 index 9a7937deaba..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.tsx +++ /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 ( -
    -
  • - 0, - 'is-passed': totalIssues === 0 - })} - /> -
  • - {concise && -
  • - {totalIssues} -
  • } - {!concise && -
  • - {branch.status.bugs} - -
  • } - {!concise && -
  • - {branch.status.vulnerabilities} - -
  • } - {!concise && -
  • - {branch.status.codeSmells} - -
  • } -
- ); - } else { - if (!branch.status) { - return null; - } - - return ; - } -} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx index 189ace21d11..d1b95c1a06f 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx @@ -102,6 +102,8 @@ export default class ComponentNav extends React.PureComponent { @@ -116,6 +118,8 @@ export default class ComponentNav extends React.PureComponent { branch={this.props.currentBranch} component={this.props.component} conf={this.props.conf} + // to re-render selected menu item + location={this.props.location} /> ); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx index 1a99483710f..cf293b64b4a 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx @@ -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 +
    {menu}
); }; render() { + const { project } = this.props; + const showManageLink = + project.qualifier === 'TRK' && project.configuration && project.configuration.showSettings; + return (
(this.node = node)}> {this.renderSearch()} {this.renderBranchesList()} + {showManageLink && +
+ + {translate('branches.manage')} + +
}
); } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx index 9d218df274f..ece4360a4dc 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx @@ -20,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'; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx index f0edfde9eab..3c1a4f512b7 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx @@ -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 { + static contextTypes = { + branchesEnabled: PropTypes.bool.isRequired + }; + isProject() { return this.props.component.qualifier === 'TRK'; } @@ -196,6 +203,7 @@ export default class ComponentNavMenu extends React.PureComponent { renderAdministrationLinks() { return [ this.renderSettingsLink(), + this.renderBranchesLink(), this.renderProfilesLink(), this.renderQualityGateLink(), this.renderCustomMeasuresLink(), @@ -223,6 +231,21 @@ export default class ComponentNavMenu extends React.PureComponent { ); } + renderBranchesLink() { + if (!this.context.branchesEnabled || !this.isProject() || !this.props.conf.showSettings) { + return null; + } + return ( +
  • + + {translate('project_branches.page')} + +
  • + ); + } + renderProfilesLink() { if (!this.props.conf.showQualityProfiles) { return null; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx index 00c4db25561..07d5cbc5bbf 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx @@ -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 index be779854e94..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/BranchStatus-test.tsx +++ /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( - - ) - ).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()).toMatchSnapshot(); - } -}); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx index 2b6574a85ed..c69eda2ef7a 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx @@ -44,7 +44,10 @@ it('should work with extensions', () => { extensions: [{ key: 'foo', name: 'Foo' }] }; expect( - shallow() + shallow( + , + { 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() + shallow( + , + { 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() + shallow(, { + 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() + shallow(, { + 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 index 1f4ccfc4484..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/BranchStatus-test.tsx.snap +++ /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`] = ` - -`; - -exports[`renders status of long-living branches 3`] = ` - -`; - -exports[`renders status of short-living branches 1`] = ` -
      -
    • - -
    • -
    • - 0 - -
    • -
    • - 0 - -
    • -
    • - 0 - -
    • -
    -`; - -exports[`renders status of short-living branches 2`] = ` -
      -
    • - -
    • -
    • - 0 - -
    • -
    • - 0 - -
    • -
    • - 1 - -
    • -
    -`; - -exports[`renders status of short-living branches 3`] = ` -
      -
    • - -
    • -
    • - 7 - -
    • -
    • - 6 - -
    • -
    • - 3 - -
    • -
    -`; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap index d80344beadd..2974d68eb61 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap @@ -25,7 +25,7 @@ exports[`renders list 1`] = ` />
        +
      • + + project_branches.page + +
      • +
      • + + project_branches.page + +
      • -
        +
        {translate('recently_browsed')} diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.js b/server/sonar-web/src/main/js/app/utils/startReactApp.js index 910761f150c..af23e36912b 100644 --- a/server/sonar-web/src/main/js/app/utils/startReactApp.js +++ b/server/sonar-web/src/main/js/app/utils/startReactApp.js @@ -55,6 +55,7 @@ import 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} /> + {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 index 00000000000..442f2f2bc97 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx @@ -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 ( +
        +
        +

        + {translate('project_branches.page')} +

        +
        + + + + + + + + + + + {sortBranchesAsTree(branches).map(branch => + + )} + +
        + {translate('branch')} + + {translate('status')} + + {translate('actions')} +
        +
        + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx new file mode 100644 index 00000000000..b8bba666f10 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx @@ -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 { + mounted: boolean; + state: State = { deleting: false, renaming: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleDeleteClick = (event: React.SyntheticEvent) => { + event.preventDefault(); + event.currentTarget.blur(); + this.setState({ deleting: true }); + }; + + handleDeletingStop = () => { + this.setState({ deleting: false }); + }; + + handleRenameClick = (event: React.SyntheticEvent) => { + 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 ( + + + + {branch.name} + + + + + + {branch.isMain + ? + + + + + : + + + + } + + + {this.state.deleting && + } + + {this.state.renaming && + } + + ); + } +} 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 index 00000000000..2e51b553c9a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/DeleteBranchModal.tsx @@ -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 { + mounted: boolean; + state: State = { loading: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleSubmit = (event: React.SyntheticEvent) => { + 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) => { + event.preventDefault(); + this.props.onClose(); + }; + + render() { + const { branch } = this.props; + const header = translate('branches.delete'); + + return ( + +
        +

        + {header} +

        +
        +
        +
        + {translateWithParameters('branches.delete.are_you_sure', branch.name)} +
        + + +
        + ); + } +} 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 index 00000000000..bcc8eedceeb --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx @@ -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 { + mounted: boolean; + state: State = { loading: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleSubmit = (event: React.SyntheticEvent) => { + 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) => { + event.preventDefault(); + this.props.onClose(); + }; + + handleNameChange = (event: React.SyntheticEvent) => { + 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 ( + +
        +

        + {header} +

        +
        +
        +
        +
        + + +
        +
        + + +
        + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx new file mode 100644 index 00000000000..4288105f79a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx @@ -0,0 +1,34 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import App from '../App'; +import { Branch, BranchType } from '../../../../app/types'; + +it('renders sorted list of branches', () => { + const branches: Branch[] = [ + { isMain: true, name: 'master' }, + { isMain: false, name: 'branch-1.0', type: BranchType.LONG }, + { isMain: false, name: 'branch-1.0', mergeBranch: 'master', type: BranchType.SHORT } + ]; + expect( + shallow() + ).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx new file mode 100644 index 00000000000..4edc3ce70d6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx @@ -0,0 +1,65 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import BranchRow from '../BranchRow'; +import { MainBranch, ShortLivingBranch, BranchType } from '../../../../app/types'; +import { click } from '../../../../helpers/testUtils'; + +const mainBranch: MainBranch = { isMain: true, name: 'master' }; + +const shortBranch: ShortLivingBranch = { + isMain: false, + name: 'feature', + mergeBranch: 'foo', + type: BranchType.SHORT +}; + +it('renders main branch', () => { + expect(shallowRender(mainBranch)).toMatchSnapshot(); +}); + +it('renders short-living branch', () => { + expect(shallowRender(shortBranch)).toMatchSnapshot(); +}); + +it('renames main branch', () => { + const onChange = jest.fn(); + const wrapper = shallowRender(mainBranch, onChange); + + click(wrapper.find('.js-rename')); + (wrapper.find('RenameBranchModal').prop('onRename') as Function)(); + expect(onChange).toBeCalled(); +}); + +it('deletes short-living branch', () => { + const onChange = jest.fn(); + const wrapper = shallowRender(shortBranch, onChange); + + click(wrapper.find('.js-delete')); + (wrapper.find('DeleteBranchModal').prop('onDelete') as Function)(); + expect(onChange).toBeCalled(); +}); + +function shallowRender(branch: MainBranch | ShortLivingBranch, onChange: () => void = jest.fn()) { + const wrapper = shallow(); + (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 index 00000000000..b2870587114 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/DeleteBranchModal-test.tsx @@ -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).mockClear(); +}); + +it('renders', () => { + const wrapper = shallowRender(); + expect(wrapper).toMatchSnapshot(); + wrapper.setState({ loading: true }); + expect(wrapper).toMatchSnapshot(); +}); + +it('deletes branch', () => { + (deleteBranch as jest.Mock).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).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( + + ); + (wrapper.instance() as any).mounted = true; + return wrapper; +} + +function submitForm(wrapper: ShallowWrapper) { + 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 index 00000000000..3a1c962d68e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/RenameBranchModal-test.tsx @@ -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).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).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).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( + + ); + (wrapper.instance() as any).mounted = true; + return wrapper; +} + +function fillAndSubmit(wrapper: ShallowWrapper) { + 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 index 00000000000..6f983e33df8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap @@ -0,0 +1,73 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders sorted list of branches 1`] = ` +
        +
        +

        + project_branches.page +

        +
        + + + + + + + + + + + + + +
        + branch + + status + + actions +
        +
        +`; diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap new file mode 100644 index 00000000000..f31097d6911 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap @@ -0,0 +1,95 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders main branch 1`] = ` + + + + master + + + + + + + + + + + + +`; + +exports[`renders short-living branch 1`] = ` + + + + feature + + + + + + + + + + + + +`; 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 index 00000000000..934f8ed2d7d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/DeleteBranchModal-test.tsx.snap @@ -0,0 +1,104 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders 1`] = ` + +
        +

        + branches.delete +

        +
        +
        +
        + branches.delete.are_you_sure.feature +
        + +
        +
        +`; + +exports[`renders 2`] = ` + +
        +

        + branches.delete +

        +
        +
        +
        + branches.delete.are_you_sure.feature +
        + + +
        +`; 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 index 00000000000..7867fa4785a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/RenameBranchModal-test.tsx.snap @@ -0,0 +1,223 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders 1`] = ` + +
        +

        + branches.rename +

        +
        +
        +
        +
        + + +
        +
        + +
        +
        +`; + +exports[`renders 2`] = ` + +
        +

        + branches.rename +

        +
        +
        +
        +
        + + +
        +
        + +
        +
        +`; + +exports[`renders 3`] = ` + +
        +

        + branches.rename +

        +
        +
        +
        +
        + + +
        +
        + + +
        +`; diff --git a/server/sonar-web/src/main/js/apps/projectBranches/routes.ts b/server/sonar-web/src/main/js/apps/projectBranches/routes.ts new file mode 100644 index 00000000000..520805ebac5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBranches/routes.ts @@ -0,0 +1,30 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { RouterState, IndexRouteProps } from 'react-router'; + +const routes = [ + { + getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) { + import('./components/App').then(i => callback(null, { component: (i as any).default })); + } + } +]; + +export default routes; diff --git a/server/sonar-web/src/main/js/components/common/BranchStatus.css b/server/sonar-web/src/main/js/components/common/BranchStatus.css new file mode 100644 index 00000000000..74278d67573 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/BranchStatus.css @@ -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 index 00000000000..ae36462f6f5 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/BranchStatus.tsx @@ -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 ( +
          +
        • + 0, + 'is-passed': totalIssues === 0 + })} + /> +
        • + {concise && +
        • + {totalIssues} +
        • } + {!concise && +
        • + {branch.status.bugs} + +
        • } + {!concise && +
        • + {branch.status.vulnerabilities} + +
        • } + {!concise && +
        • + {branch.status.codeSmells} + +
        • } +
        + ); + } else { + if (!branch.status) { + return null; + } + + return ; + } +} 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 index 00000000000..7b2de5d01d7 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/__tests__/BranchStatus-test.tsx @@ -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( + + ) + ).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()).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 index 00000000000..1f4ccfc4484 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchStatus-test.tsx.snap @@ -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`] = ` + +`; + +exports[`renders status of long-living branches 3`] = ` + +`; + +exports[`renders status of short-living branches 1`] = ` +
          +
        • + +
        • +
        • + 0 + +
        • +
        • + 0 + +
        • +
        • + 0 + +
        • +
        +`; + +exports[`renders status of short-living branches 2`] = ` +
          +
        • + +
        • +
        • + 0 + +
        • +
        • + 0 + +
        • +
        • + 1 + +
        • +
        +`; + +exports[`renders status of short-living branches 3`] = ` +
          +
        • + +
        • +
        • + 7 + +
        • +
        • + 6 + +
        • +
        • + 3 + +
        • +
        +`; 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 index e6b7498ab33..00000000000 --- a/server/sonar-web/src/main/js/components/icons-components/ChangeIcon.js +++ /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 ( - - - - ); -} 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 index 00000000000..59918140bc6 --- /dev/null +++ b/server/sonar-web/src/main/js/components/icons-components/ChangeIcon.tsx @@ -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 ( + + + + ); +} 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 index 9c90a1a9511..00000000000 --- a/server/sonar-web/src/main/js/components/icons-components/DeleteIcon.js +++ /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 ( - - - - ); -} 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 index 00000000000..08a43811da1 --- /dev/null +++ b/server/sonar-web/src/main/js/components/icons-components/DeleteIcon.tsx @@ -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 ( + + + + ); +} diff --git a/server/sonar-web/src/main/js/helpers/request.ts b/server/sonar-web/src/main/js/helpers/request.ts index 4bac03dadd6..1acb519e000 100644 --- a/server/sonar-web/src/main/js/helpers/request.ts +++ b/server/sonar-web/src/main/js/helpers/request.ts @@ -170,10 +170,10 @@ export function postJSON(url: string, data?: RequestData): Promise { * Shortcut to do a POST request */ export function post(url: string, data?: RequestData): Promise { - return new Promise(resolve => { + return new Promise((resolve, reject) => { request(url).setMethod('POST').setData(data).submit().then(checkStatus).then(() => { resolve(); - }); + }, reject); }); } diff --git a/server/sonar-web/src/main/less/components/dropdowns.less b/server/sonar-web/src/main/less/components/dropdowns.less index 6213f652aa4..53371a95b8f 100644 --- a/server/sonar-web/src/main/less/components/dropdowns.less +++ b/server/sonar-web/src/main/less/components/dropdowns.less @@ -99,3 +99,14 @@ 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; +} diff --git a/server/sonar-web/src/main/less/components/menu.less b/server/sonar-web/src/main/less/components/menu.less index bf751939d5d..ba566680624 100644 --- a/server/sonar-web/src/main/less/components/menu.less +++ b/server/sonar-web/src/main/less/components/menu.less @@ -80,6 +80,11 @@ } } + .menu-vertically-limited { + max-height: 300px; + overflow-y: auto; + } + .menu-footer > a > span { border-bottom: 1px solid @darkGrey; color: @secondFontColor; diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index bd67305da26..87353b720dd 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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