From 404d315b077c84da3c31b0c0f4dde852d918c8d1 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Thu, 24 Aug 2017 11:53:47 +0200 Subject: SONAR-9736 Build UI for long-living branches (#2390) --- server/sonar-web/src/main/js/api/branches.ts | 7 - server/sonar-web/src/main/js/api/components.ts | 8 +- server/sonar-web/src/main/js/api/measures.ts | 8 +- .../sonar-web/src/main/js/api/projectActivity.ts | 1 + .../main/js/app/components/ProjectContainer.tsx | 74 +++-- .../js/app/components/ProjectContainerNotFound.tsx | 56 ++++ .../components/__tests__/ProjectContainer-test.tsx | 65 ++++- .../app/components/nav/component/BranchStatus.css | 2 + .../app/components/nav/component/BranchStatus.tsx | 79 +++--- .../app/components/nav/component/ComponentNav.tsx | 13 +- .../nav/component/ComponentNavBranch.tsx | 25 +- .../nav/component/ComponentNavBranchesMenu.tsx | 128 ++++----- .../nav/component/ComponentNavBranchesMenuItem.tsx | 11 +- .../components/nav/component/ComponentNavMenu.tsx | 25 +- .../components/nav/component/ComponentNavMeta.tsx | 3 +- .../nav/component/__tests__/BranchStatus-test.tsx | 57 ++-- .../__tests__/ComponentNavBranch-test.tsx | 17 +- .../__tests__/ComponentNavBranchesMenu-test.tsx | 53 ++-- .../ComponentNavBranchesMenuItem-test.tsx | 25 +- .../component/__tests__/ComponentNavMenu-test.tsx | 45 ++- .../component/__tests__/ComponentNavMeta-test.tsx | 33 ++- .../__snapshots__/BranchStatus-test.tsx.snap | 22 +- .../__snapshots__/ComponentNavBranch-test.tsx.snap | 28 ++ .../ComponentNavBranchesMenu-test.tsx.snap | 313 ++++++++++++--------- .../ComponentNavBranchesMenuItem-test.tsx.snap | 87 +++++- .../__snapshots__/ComponentNavMenu-test.tsx.snap | 144 ++++++++++ .../__snapshots__/ComponentNavMeta-test.tsx.snap | 41 +++ .../__snapshots__/SearchResult-test.js.snap | 8 + .../ProjectNotifications-test.js.snap | 1 + server/sonar-web/src/main/js/apps/code/bucket.js | 52 ---- server/sonar-web/src/main/js/apps/code/bucket.ts | 71 +++++ .../src/main/js/apps/code/components/App.js | 213 -------------- .../src/main/js/apps/code/components/App.tsx | 241 ++++++++++++++++ .../src/main/js/apps/code/components/Breadcrumb.js | 27 -- .../main/js/apps/code/components/Breadcrumbs.js | 37 --- .../main/js/apps/code/components/Breadcrumbs.tsx | 45 +++ .../src/main/js/apps/code/components/Component.js | 128 --------- .../src/main/js/apps/code/components/Component.tsx | 146 ++++++++++ .../js/apps/code/components/ComponentDetach.js | 32 --- .../js/apps/code/components/ComponentDetach.tsx | 38 +++ .../js/apps/code/components/ComponentMeasure.js | 43 --- .../js/apps/code/components/ComponentMeasure.tsx | 51 ++++ .../main/js/apps/code/components/ComponentName.js | 98 ------- .../main/js/apps/code/components/ComponentName.tsx | 109 +++++++ .../main/js/apps/code/components/ComponentPin.js | 42 --- .../main/js/apps/code/components/ComponentPin.tsx | 46 +++ .../src/main/js/apps/code/components/Components.js | 56 ---- .../main/js/apps/code/components/Components.tsx | 68 +++++ .../js/apps/code/components/ComponentsEmpty.js | 32 --- .../js/apps/code/components/ComponentsEmpty.tsx | 32 +++ .../js/apps/code/components/ComponentsHeader.js | 60 ---- .../js/apps/code/components/ComponentsHeader.tsx | 64 +++++ .../src/main/js/apps/code/components/Search.js | 228 --------------- .../src/main/js/apps/code/components/Search.tsx | 234 +++++++++++++++ .../src/main/js/apps/code/components/Truncated.js | 28 -- .../src/main/js/apps/code/components/Truncated.tsx | 33 +++ server/sonar-web/src/main/js/apps/code/types.ts | 41 +++ server/sonar-web/src/main/js/apps/code/utils.js | 228 --------------- server/sonar-web/src/main/js/apps/code/utils.ts | 245 ++++++++++++++++ .../js/apps/component-measures/components/App.js | 15 +- .../component-measures/components/AppContainer.js | 14 +- .../component-measures/components/Breadcrumbs.js | 6 +- .../components/MeasureContent.js | 26 +- .../components/MeasureContentContainer.js | 10 +- .../components/MeasureOverview.js | 12 +- .../components/MeasureOverviewContainer.js | 7 +- .../components/__tests__/Breadcrumbs-test.js | 3 + .../__snapshots__/MeasureHeader-test.js.snap | 1 + .../component-measures/drilldown/ComponentCell.js | 10 +- .../component-measures/drilldown/ComponentsList.js | 4 +- .../drilldown/ComponentsListRow.js | 5 +- .../apps/component-measures/drilldown/FilesView.js | 2 + .../component-measures/drilldown/TreeMapView.js | 6 +- .../src/main/js/apps/issues/components/App.js | 16 +- .../apps/issues/components/IssuesSourceViewer.js | 4 +- .../src/main/js/apps/overview/components/App.js | 10 +- .../js/apps/overview/components/OverviewApp.js | 35 ++- .../main/js/apps/overview/events/AnalysesList.js | 15 +- .../main/js/apps/overview/events/PreviewGraph.js | 7 +- .../apps/overview/main/BugsAndVulnerabilities.js | 8 +- .../src/main/js/apps/overview/main/CodeSmells.js | 10 +- .../src/main/js/apps/overview/main/Coverage.js | 14 +- .../src/main/js/apps/overview/main/Duplications.js | 17 +- .../src/main/js/apps/overview/main/enhance.js | 24 +- .../src/main/js/apps/overview/meta/Meta.js | 4 +- .../src/main/js/apps/overview/meta/MetaLinks.js | 2 +- .../src/main/js/apps/overview/meta/MetaSize.js | 12 +- .../js/apps/overview/qualityGate/QualityGate.js | 5 +- .../overview/qualityGate/QualityGateCondition.js | 10 +- .../overview/qualityGate/QualityGateConditions.js | 13 +- .../__tests__/QualityGateCondition-test.js | 83 +++++- .../ApplicationQualityGateProject-test.js.snap | 1 + .../QualityGateCondition-test.js.snap | 67 +++++ .../components/ProjectActivityAppContainer.js | 11 +- .../__tests__/ProjectActivityApp-test.js | 1 + .../js/components/SourceViewer/SourceViewerBase.js | 29 +- .../components/SourceViewer/SourceViewerHeader.js | 23 +- .../SourceViewer/popups/coverage-popup.js | 2 +- .../SourceViewer/popups/duplication-popup.js | 2 +- .../SourceViewer/popups/line-actions-popup.js | 11 +- .../SourceViewer/views/measures-overlay.js | 9 +- .../js/components/icons-components/BranchIcon.tsx | 27 +- .../icons-components/LongLivingBranchIcon.tsx | 45 +++ .../icons-components/PullRequestIcon.tsx | 43 +++ .../icons-components/ShortLivingBranchIcon.tsx | 45 +++ .../main/js/components/shared/drilldown-link.js | 12 +- .../src/main/js/helpers/__tests__/branches-test.ts | 71 +++++ server/sonar-web/src/main/js/helpers/branches.ts | 57 +++- server/sonar-web/src/main/js/helpers/testUtils.ts | 10 +- server/sonar-web/src/main/js/helpers/urls.ts | 27 +- 110 files changed, 3195 insertions(+), 1855 deletions(-) create mode 100644 server/sonar-web/src/main/js/app/components/ProjectContainerNotFound.tsx create mode 100644 server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/code/bucket.js create mode 100644 server/sonar-web/src/main/js/apps/code/bucket.ts delete mode 100644 server/sonar-web/src/main/js/apps/code/components/App.js create mode 100644 server/sonar-web/src/main/js/apps/code/components/App.tsx delete mode 100644 server/sonar-web/src/main/js/apps/code/components/Breadcrumb.js delete mode 100644 server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.js create mode 100644 server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.tsx delete mode 100644 server/sonar-web/src/main/js/apps/code/components/Component.js create mode 100644 server/sonar-web/src/main/js/apps/code/components/Component.tsx delete mode 100644 server/sonar-web/src/main/js/apps/code/components/ComponentDetach.js create mode 100644 server/sonar-web/src/main/js/apps/code/components/ComponentDetach.tsx delete mode 100644 server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.js create mode 100644 server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx delete mode 100644 server/sonar-web/src/main/js/apps/code/components/ComponentName.js create mode 100644 server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx delete mode 100644 server/sonar-web/src/main/js/apps/code/components/ComponentPin.js create mode 100644 server/sonar-web/src/main/js/apps/code/components/ComponentPin.tsx delete mode 100644 server/sonar-web/src/main/js/apps/code/components/Components.js create mode 100644 server/sonar-web/src/main/js/apps/code/components/Components.tsx delete mode 100644 server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.js create mode 100644 server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.tsx delete mode 100644 server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.js create mode 100644 server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.tsx delete mode 100644 server/sonar-web/src/main/js/apps/code/components/Search.js create mode 100644 server/sonar-web/src/main/js/apps/code/components/Search.tsx delete mode 100644 server/sonar-web/src/main/js/apps/code/components/Truncated.js create mode 100644 server/sonar-web/src/main/js/apps/code/components/Truncated.tsx create mode 100644 server/sonar-web/src/main/js/apps/code/types.ts delete mode 100644 server/sonar-web/src/main/js/apps/code/utils.js create mode 100644 server/sonar-web/src/main/js/apps/code/utils.ts create mode 100644 server/sonar-web/src/main/js/components/icons-components/LongLivingBranchIcon.tsx create mode 100644 server/sonar-web/src/main/js/components/icons-components/PullRequestIcon.tsx create mode 100644 server/sonar-web/src/main/js/components/icons-components/ShortLivingBranchIcon.tsx create mode 100644 server/sonar-web/src/main/js/helpers/__tests__/branches-test.ts (limited to 'server/sonar-web') diff --git a/server/sonar-web/src/main/js/api/branches.ts b/server/sonar-web/src/main/js/api/branches.ts index 5c597385c85..6435c575aae 100644 --- a/server/sonar-web/src/main/js/api/branches.ts +++ b/server/sonar-web/src/main/js/api/branches.ts @@ -23,10 +23,3 @@ import throwGlobalError from '../app/utils/throwGlobalError'; export function getBranches(project: string): Promise { return getJSON('/api/project_branches/list', { project }).then(r => r.branches, throwGlobalError); } - -export function getBranch(project: string, branch: string): Promise { - return getJSON('/api/project_branches/show', { component: project, branch }).then( - r => r.branch, - throwGlobalError - ); -} diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts index fd46c34d559..361713b815d 100644 --- a/server/sonar-web/src/main/js/api/components.ts +++ b/server/sonar-web/src/main/js/api/components.ts @@ -243,11 +243,11 @@ export function getSources( return getJSON('/api/sources/lines', data).then(r => r.sources); } -export function getDuplications(component: string): Promise { - return getJSON('/api/duplications/show', { key: component }); +export function getDuplications(component: string, branch?: string): Promise { + return getJSON('/api/duplications/show', { key: component, branch }); } -export function getTests(component: string, line: number | string): Promise { - const data = { sourceFileKey: component, sourceFileLineNumber: line }; +export function getTests(component: string, line: number | string, branch?: string): Promise { + const data = { sourceFileKey: component, sourceFileLineNumber: line, branch }; return getJSON('/api/tests/list', data).then(r => r.tests); } diff --git a/server/sonar-web/src/main/js/api/measures.ts b/server/sonar-web/src/main/js/api/measures.ts index 5f6ff93f40f..fd4f5259d2f 100644 --- a/server/sonar-web/src/main/js/api/measures.ts +++ b/server/sonar-web/src/main/js/api/measures.ts @@ -19,9 +19,13 @@ */ import { getJSON, RequestData } from '../helpers/request'; -export function getMeasures(componentKey: string, metrics: string[]): Promise { +export function getMeasures( + componentKey: string, + metrics: string[], + branch?: string +): Promise { const url = '/api/measures/component'; - const data = { componentKey, metricKeys: metrics.join(',') }; + const data = { componentKey, metricKeys: metrics.join(','), branch }; return getJSON(url, data).then(r => r.component.measures); } diff --git a/server/sonar-web/src/main/js/api/projectActivity.ts b/server/sonar-web/src/main/js/api/projectActivity.ts index b2dddd8c890..a01fd36dec3 100644 --- a/server/sonar-web/src/main/js/api/projectActivity.ts +++ b/server/sonar-web/src/main/js/api/projectActivity.ts @@ -30,6 +30,7 @@ interface GetProjectActivityResponse { } export function getProjectActivity(data: { + branch?: string; project: string; category?: string; p?: number; diff --git a/server/sonar-web/src/main/js/app/components/ProjectContainer.tsx b/server/sonar-web/src/main/js/app/components/ProjectContainer.tsx index ef19629d7e3..f7ae6453557 100644 --- a/server/sonar-web/src/main/js/app/components/ProjectContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/ProjectContainer.tsx @@ -18,13 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import ProjectContainerNotFound from './ProjectContainerNotFound'; import ComponentNav from './nav/component/ComponentNav'; import { Branch, Component } from '../types'; import handleRequiredAuthorization from '../utils/handleRequiredAuthorization'; -import { getBranch } from '../../api/branches'; +import { getBranches } from '../../api/branches'; import { getComponentData } from '../../api/components'; import { getComponentNavigation } from '../../api/nav'; -import { MAIN_BRANCH } from '../../helpers/branches'; interface Props { children: any; @@ -34,7 +34,7 @@ interface Props { } interface State { - branch: Branch | null; + branches: Branch[]; loading: boolean; component: Component | null; } @@ -44,7 +44,7 @@ export default class ProjectContainer extends React.PureComponent constructor(props: Props) { super(props); - this.state = { branch: null, loading: true, component: null }; + this.state = { branches: [], loading: true, component: null }; } componentDidMount() { @@ -52,19 +52,8 @@ export default class ProjectContainer extends React.PureComponent this.fetchProject(); } - componentWillReceiveProps(nextProps: Props) { - // if the current branch has been changed, reset `branch` in state - // it prevents unwanted redirect in `overview/App#componentDidMount` - if (nextProps.location.query.branch !== this.props.location.query.branch) { - this.setState({ branch: null }); - } - } - componentDidUpdate(prevProps: Props) { - if ( - prevProps.location.query.id !== this.props.location.query.id || - prevProps.location.query.branch !== this.props.location.query.branch - ) { + if (prevProps.location.query.id !== this.props.location.query.id) { this.fetchProject(); } } @@ -81,30 +70,27 @@ export default class ProjectContainer extends React.PureComponent fetchProject() { const { branch, id } = this.props.location.query; this.setState({ loading: true }); - Promise.all([ - getComponentNavigation(id), - getComponentData(id, branch), - branch && getBranch(id, branch) - ]).then( - ([nav, data, branch]) => { - if (this.mounted) { - this.setState({ - loading: false, - branch: branch || MAIN_BRANCH, - component: this.addQualifier({ ...nav, ...data }) - }); + + const onError = (error: any) => { + if (this.mounted) { + if (error.response && error.response.status === 403) { + handleRequiredAuthorization(); + } else { + this.setState({ loading: false }); } - }, - error => { + } + }; + + Promise.all([getComponentNavigation(id), getComponentData(id, branch)]).then(([nav, data]) => { + const component = this.addQualifier({ ...nav, ...data }); + const project = component.breadcrumbs.find((c: Component) => c.qualifier === 'TRK'); + const branchesRequest = project ? getBranches(project.key) : Promise.resolve([]); + branchesRequest.then(branches => { if (this.mounted) { - if (error.response && error.response.status === 403) { - handleRequiredAuthorization(); - } else { - this.setState({ loading: false }); - } + this.setState({ loading: false, branches, component }); } - } - ); + }, onError); + }, onError); } handleProjectChange = (changes: {}) => { @@ -114,10 +100,17 @@ export default class ProjectContainer extends React.PureComponent }; render() { - const { branch, component } = this.state; + const { query } = this.props.location; + const { branches, component, loading } = this.state; + + if (loading) { + return ; + } + + const branch = branches.find(b => (query.branch ? b.name === query.branch : b.isMain)); if (!component || !branch) { - return null; + return ; } const isFile = ['FIL', 'UTS'].includes(component.qualifier); @@ -127,7 +120,8 @@ export default class ProjectContainer extends React.PureComponent
{!isFile && +
+

+ {translate('dashboard.project_not_found')} +

+

+ {translate('dashboard.project_not_found.2')} +

+

+ Go back to the homepage +

+
+
+ ); + } +} diff --git a/server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx index 5400c85f212..beb6580feb8 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx @@ -17,9 +17,23 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +jest.mock('../../../api/branches', () => ({ getBranches: jest.fn() })); +jest.mock('../../../api/components', () => ({ getComponentData: jest.fn() })); +jest.mock('../../../api/nav', () => ({ getComponentNavigation: jest.fn() })); + import * as React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, mount } from 'enzyme'; import ProjectContainer from '../ProjectContainer'; +import { getBranches } from '../../../api/branches'; +import { getComponentData } from '../../../api/components'; +import { getComponentNavigation } from '../../../api/nav'; +import { doAsync } from '../../../helpers/testUtils'; + +beforeEach(() => { + (getBranches as jest.Mock).mockClear(); + (getComponentData as jest.Mock).mockClear(); + (getComponentNavigation as jest.Mock).mockClear(); +}); it('changes component', () => { const Inner = () =>
; @@ -31,7 +45,7 @@ it('changes component', () => { ); (wrapper.instance() as ProjectContainer).mounted = true; wrapper.setState({ - branch: { isMain: true }, + branches: [{ isMain: true }], component: { qualifier: 'TRK', visibility: 'public' }, loading: false }); @@ -39,3 +53,50 @@ it('changes component', () => { (wrapper.find(Inner).prop('onComponentChange') as Function)({ visibility: 'private' }); expect(wrapper.state().component).toEqual({ qualifier: 'TRK', visibility: 'private' }); }); + +it("loads branches for module's project", () => { + (getBranches as jest.Mock).mockImplementation(() => Promise.resolve([])); + (getComponentData as jest.Mock).mockImplementation(() => Promise.resolve({})); + (getComponentNavigation as jest.Mock).mockImplementation(() => + Promise.resolve({ + breadcrumbs: [ + { key: 'projectKey', name: 'project', qualifier: 'TRK' }, + { key: 'moduleKey', name: 'module', qualifier: 'BRC' } + ] + }) + ); + + mount( + +
+ + ); + + return doAsync().then(() => { + expect(getBranches).toBeCalledWith('projectKey'); + expect(getComponentData).toBeCalledWith('moduleKey', undefined); + expect(getComponentNavigation).toBeCalledWith('moduleKey'); + }); +}); + +it("doesn't load branches portfolio", () => { + (getBranches as jest.Mock).mockImplementation(() => Promise.resolve([])); + (getComponentData as jest.Mock).mockImplementation(() => Promise.resolve({})); + (getComponentNavigation as jest.Mock).mockImplementation(() => + Promise.resolve({ + breadcrumbs: [{ key: 'portfolioKey', name: 'portfolio', qualifier: 'VW' }] + }) + ); + + mount( + +
+ + ); + + return doAsync().then(() => { + expect(getBranches).not.toBeCalled(); + expect(getComponentData).toBeCalledWith('portfolioKey', undefined); + expect(getComponentNavigation).toBeCalledWith('portfolioKey'); + }); +}); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.css b/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.css index b52fc694772..74278d67573 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.css +++ b/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.css @@ -1,4 +1,6 @@ .branch-status { + min-width: 64px; + text-align: right; } .branch-status-indicator { diff --git a/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.tsx b/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.tsx index bac5d7e295f..9a7937deaba 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.tsx @@ -20,6 +20,7 @@ import * as React from 'react'; import * as classNames from 'classnames'; import { Branch } from '../../../types'; +import Level from '../../../../components/ui/Level'; import BugIcon from '../../../../components/icons-components/BugIcon'; import CodeSmellIcon from '../../../../components/icons-components/CodeSmellIcon'; import VulnerabilityIcon from '../../../../components/icons-components/VulnerabilityIcon'; @@ -32,42 +33,50 @@ interface Props { } export default function BranchStatus({ branch, concise = false }: Props) { - // TODO handle long-living branches - if (!isShortLivingBranch(branch)) { - return null; - } + if (isShortLivingBranch(branch)) { + if (!branch.status) { + return null; + } - const totalIssues = branch.status.bugs + branch.status.vulnerabilities + branch.status.codeSmells; + const totalIssues = + branch.status.bugs + branch.status.vulnerabilities + branch.status.codeSmells; - return ( -
    -
  • - 0, - 'is-passed': totalIssues === 0 - })} - /> -
  • - {concise && -
  • - {totalIssues} -
  • } - {!concise && -
  • - {branch.status.bugs} - -
  • } - {!concise && + return ( +
    • - {branch.status.vulnerabilities} - -
    • } - {!concise && -
    • - {branch.status.codeSmells} - -
    • } -
    - ); + 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 9729aea4cd6..189ace21d11 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx @@ -31,7 +31,8 @@ import { STATUSES } from '../../../../apps/background-tasks/constants'; import './ComponentNav.css'; interface Props { - branch: Branch; + branches: Branch[]; + currentBranch: Branch; component: Component; conf: ComponentConfiguration; location: {}; @@ -98,17 +99,21 @@ export default class ComponentNav extends React.PureComponent { breadcrumbs={this.props.component.breadcrumbs} /> - + diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx index 1ac3fa296ec..fbcbd1db06c 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx @@ -22,10 +22,12 @@ import * as classNames from 'classnames'; import ComponentNavBranchesMenu from './ComponentNavBranchesMenu'; import { Branch, Component } from '../../../types'; import BranchIcon from '../../../../components/icons-components/BranchIcon'; -import { getBranchDisplayName } from '../../../../helpers/branches'; +import { isShortLivingBranch } from '../../../../helpers/branches'; +import { translate } from '../../../../helpers/l10n'; interface Props { - branch: Branch; + branches: Branch[]; + currentBranch: Branch; project: Component; } @@ -42,7 +44,10 @@ export default class ComponentNavBranch extends React.PureComponent - - {getBranchDisplayName(this.props.branch)} + + {currentBranch.name} {this.state.open && } + {isShortLivingBranch(currentBranch) && + !currentBranch.isOrphan && + + {translate('from')} {currentBranch.mergeBranch} + }
); } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx index 6efe8e25b18..261abe6dd2b 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx @@ -19,83 +19,47 @@ */ import * as React from 'react'; import * as PropTypes from 'prop-types'; -import { sortBy } from 'lodash'; import ComponentNavBranchesMenuItem from './ComponentNavBranchesMenuItem'; import { Branch, Component } from '../../../types'; -import { getBranches } from '../../../../api/branches'; -import { isShortLivingBranch, getBranchDisplayName } from '../../../../helpers/branches'; +import { + sortBranchesAsTree, + isLongLivingBranch, + isShortLivingBranch +} from '../../../../helpers/branches'; import { translate } from '../../../../helpers/l10n'; import { getProjectBranchUrl } from '../../../../helpers/urls'; interface Props { - branch: Branch; + branches: Branch[]; + currentBranch: Branch; onClose: () => void; project: Component; } interface State { - branches: Branch[]; - loading: boolean; query: string; selected: string | null; } export default class ComponentNavBranchesMenu extends React.PureComponent { - private mounted: boolean; private node: HTMLElement | null; + state = { query: '', selected: null }; static contextTypes = { router: PropTypes.object }; - constructor(props: Props) { - super(props); - this.state = { - branches: [], - loading: true, - query: '', - selected: null - }; - } - componentDidMount() { - this.mounted = true; - this.fetchBranches(); window.addEventListener('click', this.handleClickOutside); } componentWillUnmount() { - this.mounted = false; window.removeEventListener('click', this.handleClickOutside); } - fetchBranches = () => { - this.setState({ loading: true }); - getBranches(this.props.project.key).then( - (branches: Branch[]) => { - if (this.mounted) { - this.setState({ branches: this.sortBranches(branches), loading: false }); - } - }, - () => { - if (this.mounted) { - this.setState({ loading: false }); - } - } - ); - }; - - sortBranches = (branches: Branch[]): Branch[] => - sortBy( - branches, - branch => !branch.isMain, // main branch first - branch => !isShortLivingBranch(branch), // then short-living branches - branch => getBranchDisplayName(branch) // then by name - ); - getFilteredBranches = () => - this.state.branches.filter(branch => - getBranchDisplayName(branch).toLowerCase().includes(this.state.query.toLowerCase()) + sortBranchesAsTree(this.props.branches).filter(branch => + branch.name.toLowerCase().includes(this.state.query.toLowerCase()) ); handleClickOutside = (event: Event) => { @@ -130,9 +94,7 @@ export default class ComponentNavBranchesMenu extends React.PureComponent { const selected = this.getSelected(); - const branch = this.getFilteredBranches().find( - branch => getBranchDisplayName(branch) === selected - ); + const branch = this.getFilteredBranches().find(branch => branch.name === selected); if (branch) { this.context.router.push(this.getProjectBranchUrl(branch)); } @@ -141,33 +103,33 @@ export default class ComponentNavBranchesMenu extends React.PureComponent { const selected = this.getSelected(); const branches = this.getFilteredBranches(); - const index = branches.findIndex(branch => getBranchDisplayName(branch) === selected); + const index = branches.findIndex(branch => branch.name === selected); if (index > 0) { - this.setState({ selected: getBranchDisplayName(branches[index - 1]) }); + this.setState({ selected: branches[index - 1].name }); } }; selectNext = () => { const selected = this.getSelected(); const branches = this.getFilteredBranches(); - const index = branches.findIndex(branch => getBranchDisplayName(branch) === selected); + const index = branches.findIndex(branch => branch.name === selected); if (index >= 0 && index < branches.length - 1) { - this.setState({ selected: getBranchDisplayName(branches[index + 1]) }); + this.setState({ selected: branches[index + 1].name }); } }; handleSelect = (branch: Branch) => { - this.setState({ selected: getBranchDisplayName(branch) }); + this.setState({ selected: branch.name }); }; getSelected = () => { const branches = this.getFilteredBranches(); - return this.state.selected || (branches.length > 0 && getBranchDisplayName(branches[0])); + return this.state.selected || (branches.length > 0 && branches[0].name); }; getProjectBranchUrl = (branch: Branch) => getProjectBranchUrl(this.props.project.key, branch); - isSelected = (branch: Branch) => getBranchDisplayName(branch) === this.getSelected(); + isSelected = (branch: Branch) => branch.name === this.getSelected(); renderSearch = () =>
@@ -187,35 +149,47 @@ export default class ComponentNavBranchesMenu extends React.PureComponent { const branches = this.getFilteredBranches(); - const selected = this.getSelected(); - return branches.length > 0 - ?
    - {branches.map(branch => - - )} -
- :
+ if (branches.length === 0) { + return ( +
{translate('no_results')} -
; +
+ ); + } + + const menu: JSX.Element[] = []; + branches.forEach((branch, index) => { + const isOrphan = isShortLivingBranch(branch) && branch.isOrphan; + const previous = index > 0 ? branches[index - 1] : null; + const isPreviousOrphan = isShortLivingBranch(previous) ? previous.isOrphan : false; + if (isLongLivingBranch(branch) || (isOrphan && !isPreviousOrphan)) { + menu.push(
  • ); + } + menu.push( + + ); + }); + + return ( +
      + {menu} +
    + ); }; render() { return (
    (this.node = node)}> - {this.state.loading - ? - :
    - {this.renderSearch()} - {this.renderBranchesList()} -
    } + {this.renderSearch()} + {this.renderBranchesList()}
    ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx index 44821562e28..9d218df274f 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx @@ -23,7 +23,7 @@ import * as classNames from 'classnames'; import BranchStatus from './BranchStatus'; import { Branch, Component } from '../../../types'; import BranchIcon from '../../../../components/icons-components/BranchIcon'; -import { isShortLivingBranch, getBranchDisplayName } from '../../../../helpers/branches'; +import { isShortLivingBranch } from '../../../../helpers/branches'; import { getProjectBranchUrl } from '../../../../helpers/urls'; interface Props { @@ -34,14 +34,12 @@ interface Props { } export default function ComponentNavBranchesMenuItem({ branch, ...props }: Props) { - const displayName = getBranchDisplayName(branch); - const handleMouseEnter = () => { props.onSelect(branch); }; return ( -
  • +
  • - {displayName} + {branch.name}
    diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx index 15c6087e4ec..f0edfde9eab 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx @@ -22,7 +22,7 @@ import { Link } from 'react-router'; import * as classNames from 'classnames'; import { Branch, Component, ComponentExtension, ComponentConfiguration } from '../../../types'; import NavBarTabs from '../../../../components/nav/NavBarTabs'; -import { isShortLivingBranch } from '../../../../helpers/branches'; +import { isShortLivingBranch, getBranchName } from '../../../../helpers/branches'; import { translate } from '../../../../helpers/l10n'; const SETTINGS_URLS = [ @@ -71,7 +71,12 @@ export default class ComponentNavMenu extends React.PureComponent { const pathname = this.isView() ? '/portfolio' : '/dashboard'; return (
  • - + {translate('overview.page')}
  • @@ -88,7 +93,7 @@ export default class ComponentNavMenu extends React.PureComponent { {this.isView() || this.isApplication() @@ -111,7 +116,10 @@ export default class ComponentNavMenu extends React.PureComponent { return (
  • {translate('project_activity.page')} @@ -126,7 +134,7 @@ export default class ComponentNavMenu extends React.PureComponent { to={{ pathname: '/project/issues', query: { - branch: this.props.branch.name, + branch: getBranchName(this.props.branch), id: this.props.component.key, resolved: 'false' } @@ -146,7 +154,10 @@ export default class ComponentNavMenu extends React.PureComponent { return (
  • {translate('layout.measures')} @@ -155,7 +166,7 @@ export default class ComponentNavMenu extends React.PureComponent { } renderAdministration() { - if (isShortLivingBranch(this.props.branch)) { + if (!this.props.branch.isMain) { return null; } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx index fae37abdabd..00c4db25561 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx @@ -25,6 +25,7 @@ import Tooltip from '../../../../components/controls/Tooltip'; import PendingIcon from '../../../../components/icons-components/PendingIcon'; import DateTimeFormatter from '../../../../components/intl/DateTimeFormatter'; import { translate, translateWithParameters } from '../../../../helpers/l10n'; +import { isShortLivingBranch } from '../../../../helpers/branches'; interface Props { branch: Branch; @@ -114,7 +115,7 @@ export default function ComponentNavMeta(props: Props) { ); } - if (!props.branch.isMain) { + if (isShortLivingBranch(props.branch)) { metaList.push(
  • diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/BranchStatus-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/BranchStatus-test.tsx index e932d89cc4c..be779854e94 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/BranchStatus-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/BranchStatus-test.tsx @@ -20,25 +20,44 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import BranchStatus from '../BranchStatus'; -import { BranchType } from '../../../../types'; +import { BranchType, LongLivingBranch } from '../../../../types'; -it('renders', () => { - check(0, 0, 0); - check(0, 1, 0); - check(7, 3, 6); +it('renders status of short-living branches', () => { + checkShort(0, 0, 0); + checkShort(0, 1, 0); + checkShort(7, 3, 6); + + function checkShort(bugs: number, codeSmells: number, vulnerabilities: number) { + expect( + shallow( + + ) + ).toMatchSnapshot(); + } }); -function check(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__/ComponentNavBranch-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx index 8b138e9aa90..5d2d86f9d24 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx @@ -24,26 +24,33 @@ import { BranchType, ShortLivingBranch, MainBranch, Component } from '../../../. import { click } from '../../../../../helpers/testUtils'; it('renders main branch', () => { - const branch: MainBranch = { isMain: true, name: undefined, type: BranchType.LONG }; + const branch: MainBranch = { isMain: true, name: 'master' }; const component = {} as Component; - expect(shallow()).toMatchSnapshot(); + expect( + shallow() + ).toMatchSnapshot(); }); it('renders short-living branch', () => { const branch: ShortLivingBranch = { isMain: false, + mergeBranch: 'master', name: 'foo', status: { bugs: 0, codeSmells: 0, vulnerabilities: 0 }, type: BranchType.SHORT }; const component = {} as Component; - expect(shallow()).toMatchSnapshot(); + expect( + shallow() + ).toMatchSnapshot(); }); it('opens menu', () => { - const branch: MainBranch = { isMain: true, name: undefined, type: BranchType.LONG }; + const branch: MainBranch = { isMain: true, name: 'master' }; const component = {} as Component; - const wrapper = shallow(); + const wrapper = shallow( + + ); expect(wrapper.find('ComponentNavBranchesMenu')).toHaveLength(0); click(wrapper.find('a')); expect(wrapper.find('ComponentNavBranchesMenu')).toHaveLength(1); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx index c6ef473552c..a4eb5bfbf82 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx @@ -29,40 +29,43 @@ import { } from '../../../../types'; import { elementKeydown } from '../../../../../helpers/testUtils'; +const project = { key: 'component' } as Component; + it('renders list', () => { - const component = { key: 'component' } as Component; - const wrapper = shallow( - - ); - wrapper.setState({ - branches: [mainBranch(), shortBranch('foo'), longBranch('bar')], - loading: false - }); - expect(wrapper).toMatchSnapshot(); + expect( + shallow( + + ) + ).toMatchSnapshot(); }); it('searches', () => { - const component = { key: 'component' } as Component; const wrapper = shallow( - + ); - wrapper.setState({ - branches: [mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')], - loading: false, - query: 'bar' - }); + wrapper.setState({ query: 'bar' }); expect(wrapper).toMatchSnapshot(); }); it('selects next & previous', () => { - const component = { key: 'component' } as Component; const wrapper = shallow( - + ); - wrapper.setState({ - branches: [mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')], - loading: false - }); elementKeydown(wrapper.find('input'), 40); wrapper.update(); expect(wrapper.state().selected).toBe('foo'); @@ -75,12 +78,14 @@ it('selects next & previous', () => { }); function mainBranch(): MainBranch { - return { isMain: true, name: undefined, type: BranchType.LONG }; + return { isMain: true, name: 'master' }; } -function shortBranch(name: string): ShortLivingBranch { +function shortBranch(name: string, isOrphan?: true): ShortLivingBranch { return { isMain: false, + isOrphan, + mergeBranch: 'master', name, status: { bugs: 0, codeSmells: 0, vulnerabilities: 0 }, type: BranchType.SHORT diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx index ba32de85805..2e1d56e2115 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx @@ -24,7 +24,7 @@ import { BranchType, MainBranch, ShortLivingBranch, Component } from '../../../. it('renders main branch', () => { const component = { key: 'component' } as Component; - const mainBranch: MainBranch = { isMain: true, name: undefined, type: BranchType.LONG }; + const mainBranch: MainBranch = { isMain: true, name: 'master' }; expect( shallow( { const component = { key: 'component' } as Component; const shortBranch: ShortLivingBranch = { isMain: false, + mergeBranch: 'master', + name: 'foo', + status: { bugs: 1, codeSmells: 2, vulnerabilities: 3 }, + type: BranchType.SHORT + }; + expect( + shallow( + + ) + ).toMatchSnapshot(); +}); + +it('renders short-living orhpan branch', () => { + const component = { key: 'component' } as Component; + const shortBranch: ShortLivingBranch = { + isMain: false, + isOrphan: true, + mergeBranch: 'master', name: 'foo', status: { bugs: 1, codeSmells: 2, vulnerabilities: 3 }, type: BranchType.SHORT diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx index dc3ce7d17e8..2b6574a85ed 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx @@ -20,7 +20,18 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import ComponentNavMenu from '../ComponentNavMenu'; -import { Branch, Component } from '../../../../types'; +import { + Component, + ShortLivingBranch, + BranchType, + LongLivingBranch, + MainBranch +} from '../../../../types'; + +const mainBranch: MainBranch = { + isMain: true, + name: 'master' +}; it('should work with extensions', () => { const component = { @@ -33,9 +44,7 @@ it('should work with extensions', () => { extensions: [{ key: 'foo', name: 'Foo' }] }; expect( - shallow( - - ) + shallow() ).toMatchSnapshot(); }); @@ -53,8 +62,30 @@ it('should work with multiple extensions', () => { extensions: [{ key: 'foo', name: 'Foo' }, { key: 'bar', name: 'Bar' }] }; expect( - shallow( - - ) + shallow() + ).toMatchSnapshot(); +}); + +it('should work for short-living branches', () => { + const branch: ShortLivingBranch = { + isMain: false, + mergeBranch: 'master', + name: 'feature', + status: { bugs: 0, codeSmells: 2, vulnerabilities: 3 }, + type: BranchType.SHORT + }; + const component = { key: 'foo', qualifier: 'TRK' } as Component; + const conf = { showSettings: true }; + expect( + shallow() + ).toMatchSnapshot(); +}); + +it('should work for long-living branches', () => { + const branch: LongLivingBranch = { isMain: false, name: 'release', type: BranchType.LONG }; + const component = { key: 'foo', qualifier: 'TRK' } as Component; + const conf = { showSettings: true }; + expect( + shallow() ).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx index 766d5aff111..0a94ecf0cfe 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx @@ -20,7 +20,13 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import ComponentNavMeta from '../ComponentNavMeta'; -import { Branch, Component } from '../../../../types'; +import { + Branch, + Component, + BranchType, + ShortLivingBranch, + LongLivingBranch +} from '../../../../types'; it('renders incremental badge', () => { check(true); @@ -39,3 +45,28 @@ it('renders incremental badge', () => { ).toHaveLength(incremental ? 1 : 0); } }); + +it('renders status of short-living branch', () => { + const branch: ShortLivingBranch = { + isMain: false, + mergeBranch: 'master', + name: 'feature', + status: { bugs: 0, codeSmells: 2, vulnerabilities: 3 }, + type: BranchType.SHORT + }; + expect( + shallow() + ).toMatchSnapshot(); +}); + +it('renders nothing for long-living branch', () => { + const branch: LongLivingBranch = { + isMain: false, + name: 'release', + status: { qualityGateStatus: 'OK' }, + type: BranchType.LONG + }; + expect( + shallow() + ).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/BranchStatus-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/BranchStatus-test.tsx.snap index ab40c58e93c..1f4ccfc4484 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/BranchStatus-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/BranchStatus-test.tsx.snap @@ -1,6 +1,22 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`renders 1`] = ` +exports[`renders status of long-living branches 1`] = `null`; + +exports[`renders status of long-living branches 2`] = ` + +`; + +exports[`renders status of long-living branches 3`] = ` + +`; + +exports[`renders status of short-living branches 1`] = `
      @@ -30,7 +46,7 @@ exports[`renders 1`] = `
    `; -exports[`renders 2`] = ` +exports[`renders status of short-living branches 2`] = `
      @@ -60,7 +76,7 @@ exports[`renders 2`] = `
    `; -exports[`renders 3`] = ` +exports[`renders status of short-living branches 3`] = `
      diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap index 3ead4391274..977dd6433c3 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap @@ -10,6 +10,12 @@ exports[`renders main branch 1`] = ` onClick={[Function]} > master @@ -30,6 +36,19 @@ exports[`renders short-living branch 1`] = ` onClick={[Function]} > foo @@ -37,5 +56,14 @@ exports[`renders short-living branch 1`] = ` className="icon-dropdown little-spacer-left" /> + + from + + + master + +
  • `; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap index dc05a411296..d80344beadd 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap @@ -4,85 +4,139 @@ exports[`renders list 1`] = `
    -
    -
    + - -
    -
      - + +
    +
      + - +
    • + - + -
    -
    + } + onSelect={[Function]} + selected={false} + /> +
  • + +
  • + +
  • `; @@ -90,68 +144,71 @@ exports[`searches 1`] = `
    -
    -
    + - -
    -
      - + +
    +
      + - +
    • + -
    -
    + } + onSelect={[Function]} + selected={false} + /> +
    `; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap index e579bfadf0c..c1aed6ae1b6 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap @@ -19,6 +19,12 @@ exports[`renders main branch 1`] = ` >
    master @@ -30,8 +36,7 @@ exports[`renders main branch 1`] = ` branch={ Object { "isMain": true, - "name": undefined, - "type": "LONG", + "name": "master", } } concise={true} @@ -62,6 +67,19 @@ exports[`renders short-living branch 1`] = ` >
    foo @@ -73,6 +91,71 @@ exports[`renders short-living branch 1`] = ` branch={ Object { "isMain": false, + "mergeBranch": "master", + "name": "foo", + "status": Object { + "bugs": 1, + "codeSmells": 2, + "vulnerabilities": 3, + }, + "type": "SHORT", + } + } + concise={true} + /> +
    + + +`; + +exports[`renders short-living orhpan branch 1`] = ` +
  • + +
    + + foo +
    +
    + +
  • + + overview.page + +
  • +
  • + + issues.page + +
  • +
  • + + layout.measures + +
  • +
  • + + code.page + +
  • +
  • + + project_activity.page + +
  • + +`; + +exports[`should work for short-living branches 1`] = ` + +
  • + + issues.page + +
  • +
  • + + code.page + +
  • +
    +`; + exports[`should work with extensions 1`] = `
  • @@ -11,6 +149,7 @@ exports[`should work with extensions 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "foo", }, } @@ -47,6 +186,7 @@ exports[`should work with extensions 1`] = ` Object { "pathname": "/component_measures", "query": Object { + "branch": undefined, "id": "foo", }, } @@ -82,6 +222,7 @@ exports[`should work with extensions 1`] = ` Object { "pathname": "/project/activity", "query": Object { + "branch": undefined, "id": "foo", }, } @@ -212,6 +353,7 @@ exports[`should work with multiple extensions 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "foo", }, } @@ -248,6 +390,7 @@ exports[`should work with multiple extensions 1`] = ` Object { "pathname": "/component_measures", "query": Object { + "branch": undefined, "id": "foo", }, } @@ -283,6 +426,7 @@ exports[`should work with multiple extensions 1`] = ` Object { "pathname": "/project/activity", "query": Object { + "branch": undefined, "id": "foo", }, } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap new file mode 100644 index 00000000000..4b296afc2bf --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders nothing for long-living branch 1`] = ` +
    +
      +
    +`; + +exports[`renders status of short-living branch 1`] = ` +
    +
      +
    • + +
    • +
    +
    +`; diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap index f09e0116d75..a99f95bc210 100644 --- a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap +++ b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap @@ -19,6 +19,7 @@ exports[`renders favorite 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "foo", }, } @@ -65,6 +66,7 @@ exports[`renders match 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "foo", }, } @@ -110,6 +112,7 @@ exports[`renders organizations 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "foo", }, } @@ -160,6 +163,7 @@ exports[`renders organizations 2`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "foo", }, } @@ -205,6 +209,7 @@ exports[`renders projects 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "qwe", }, } @@ -255,6 +260,7 @@ exports[`renders recently browsed 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "foo", }, } @@ -300,6 +306,7 @@ exports[`renders selected 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "foo", }, } @@ -344,6 +351,7 @@ exports[`renders selected 2`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "foo", }, } diff --git a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.js.snap b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.js.snap index 9d298246118..d7a5a2f3e76 100644 --- a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.js.snap +++ b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.js.snap @@ -22,6 +22,7 @@ exports[`should match snapshot 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "foo", }, } diff --git a/server/sonar-web/src/main/js/apps/code/bucket.js b/server/sonar-web/src/main/js/apps/code/bucket.js deleted file mode 100644 index 7b235e0f14b..00000000000 --- a/server/sonar-web/src/main/js/apps/code/bucket.js +++ /dev/null @@ -1,52 +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. - */ -let bucket = {}; -let childrenBucket = {}; -let breadcrumbsBucket = {}; - -export function addComponent(component) { - bucket[component.key] = component; -} - -export function getComponent(componentKey) { - return bucket[componentKey]; -} - -export function addComponentChildren(componentKey, children, total, page) { - childrenBucket[componentKey] = { children, total, page }; -} - -export function getComponentChildren(componentKey) { - return childrenBucket[componentKey]; -} - -export function addComponentBreadcrumbs(componentKey, breadcrumbs) { - breadcrumbsBucket[componentKey] = breadcrumbs; -} - -export function getComponentBreadcrumbs(componentKey) { - return breadcrumbsBucket[componentKey]; -} - -export function clearBucket() { - bucket = {}; - childrenBucket = {}; - breadcrumbsBucket = {}; -} diff --git a/server/sonar-web/src/main/js/apps/code/bucket.ts b/server/sonar-web/src/main/js/apps/code/bucket.ts new file mode 100644 index 00000000000..fd052c55002 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/code/bucket.ts @@ -0,0 +1,71 @@ +/* + * 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 { Breadcrumb, Component } from './types'; + +let bucket: { [key: string]: Component } = {}; +let childrenBucket: { + [key: string]: { + children: Component[]; + page: number; + total: number; + }; +} = {}; +let breadcrumbsBucket: { [key: string]: Breadcrumb[] } = {}; + +export function addComponent(component: Component): void { + bucket[component.key] = component; +} + +export function getComponent(componentKey: string): Component { + return bucket[componentKey]; +} + +export function addComponentChildren( + componentKey: string, + children: Component[], + total: number, + page: number +): void { + childrenBucket[componentKey] = { children, total, page }; +} + +export function getComponentChildren( + componentKey: string +): { + children: Component[]; + page: number; + total: number; +} { + return childrenBucket[componentKey]; +} + +export function addComponentBreadcrumbs(componentKey: string, breadcrumbs: Breadcrumb[]): void { + breadcrumbsBucket[componentKey] = breadcrumbs; +} + +export function getComponentBreadcrumbs(componentKey: string): Breadcrumb[] { + return breadcrumbsBucket[componentKey]; +} + +export function clearBucket(): void { + bucket = {}; + childrenBucket = {}; + breadcrumbsBucket = {}; +} diff --git a/server/sonar-web/src/main/js/apps/code/components/App.js b/server/sonar-web/src/main/js/apps/code/components/App.js deleted file mode 100644 index c59cac6bade..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/App.js +++ /dev/null @@ -1,213 +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. - */ -import classNames from 'classnames'; -import React from 'react'; -import Helmet from 'react-helmet'; -import Components from './Components'; -import Breadcrumbs from './Breadcrumbs'; -import SourceViewer from './../../../components/SourceViewer/SourceViewer'; -import Search from './Search'; -import ListFooter from '../../../components/controls/ListFooter'; -import { - retrieveComponentChildren, - retrieveComponent, - loadMoreChildren, - parseError -} from '../utils'; -import { addComponent, addComponentBreadcrumbs, clearBucket } from '../bucket'; -import { translate } from '../../../helpers/l10n'; -import '../code.css'; - -export default class App extends React.PureComponent { - state = { - loading: true, - baseComponent: null, - components: null, - breadcrumbs: [], - total: 0, - page: 0, - sourceViewer: null, - error: null - }; - - componentDidMount() { - this.mounted = true; - this.handleComponentChange(); - } - - componentDidUpdate(prevProps) { - if (prevProps.component !== this.props.component) { - this.handleComponentChange(); - } else if (prevProps.location !== this.props.location) { - this.handleUpdate(); - } - } - - componentWillUnmount() { - clearBucket(); - this.mounted = false; - } - - handleComponentChange() { - const { component } = this.props; - - // we already know component's breadcrumbs, - addComponentBreadcrumbs(component.key, component.breadcrumbs); - - this.setState({ loading: true }); - const isPortfolio = ['VW', 'SVW'].includes(component.qualifier); - retrieveComponentChildren(component.key, isPortfolio, component.branch) - .then(r => { - addComponent(r.baseComponent); - this.handleUpdate(); - }) - .catch(e => { - if (this.mounted) { - this.setState({ loading: false }); - parseError(e).then(this.handleError.bind(this)); - } - }); - } - - loadComponent(componentKey) { - this.setState({ loading: true }); - - const isPortfolio = ['VW', 'SVW'].includes(this.props.component.qualifier); - retrieveComponent(componentKey, isPortfolio, this.props.component.branch) - .then(r => { - if (this.mounted) { - if (['FIL', 'UTS'].includes(r.component.qualifier)) { - this.setState({ - loading: false, - sourceViewer: r.component, - breadcrumbs: r.breadcrumbs, - searchResults: null - }); - } else { - this.setState({ - loading: false, - baseComponent: r.component, - components: r.components, - breadcrumbs: r.breadcrumbs, - total: r.total, - page: r.page, - sourceViewer: null, - searchResults: null - }); - } - } - }) - .catch(e => { - if (this.mounted) { - this.setState({ loading: false }); - parseError(e).then(this.handleError.bind(this)); - } - }); - } - - handleUpdate() { - const { component, location } = this.props; - const { selected } = location.query; - const finalKey = selected || component.key; - - this.loadComponent(finalKey); - } - - handleLoadMore = () => { - const { baseComponent, page } = this.state; - const isPortfolio = ['VW', 'SVW'].includes(this.props.component.qualifier); - loadMoreChildren(baseComponent.key, page + 1, isPortfolio, this.props.component.branch) - .then(r => { - if (this.mounted) { - this.setState({ - components: [...this.state.components, ...r.components], - page: r.page, - total: r.total - }); - } - }) - .catch(e => { - if (this.mounted) { - this.setState({ loading: false }); - parseError(e).then(this.handleError); - } - }); - }; - - handleError = error => { - if (this.mounted) { - this.setState({ error }); - } - }; - - render() { - const { component, location } = this.props; - const { - loading, - error, - baseComponent, - components, - breadcrumbs, - total, - sourceViewer - } = this.state; - - const shouldShowSourceViewer = !!sourceViewer; - const shouldShowComponents = !shouldShowSourceViewer && components; - const shouldShowBreadcrumbs = Array.isArray(breadcrumbs) && breadcrumbs.length > 1; - - const componentsClassName = classNames('spacer-top', { 'new-loading': loading }); - - return ( -
    - - - {error && -
    - {error} -
    } - - - -
    - {shouldShowBreadcrumbs && - } - - {shouldShowComponents && -
    - -
    } - - {shouldShowComponents && - } - - {shouldShowSourceViewer && -
    - -
    } -
    -
    - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/code/components/App.tsx b/server/sonar-web/src/main/js/apps/code/components/App.tsx new file mode 100644 index 00000000000..0df83e23dde --- /dev/null +++ b/server/sonar-web/src/main/js/apps/code/components/App.tsx @@ -0,0 +1,241 @@ +/* + * 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 classNames from 'classnames'; +import * as React from 'react'; +import Helmet from 'react-helmet'; +import Components from './Components'; +import Breadcrumbs from './Breadcrumbs'; +import { Component as CodeComponent } from '../types'; +import SourceViewer from './../../../components/SourceViewer/SourceViewer'; +import Search from './Search'; +import ListFooter from '../../../components/controls/ListFooter'; +import { + retrieveComponentChildren, + retrieveComponent, + loadMoreChildren, + parseError +} from '../utils'; +import { addComponent, addComponentBreadcrumbs, clearBucket } from '../bucket'; +import { getBranchName } from '../../../helpers/branches'; +import { translate } from '../../../helpers/l10n'; +import '../code.css'; +import { Component, Branch } from '../../../app/types'; + +interface Props { + branch: Branch; + component: Component; + location: { query: { [x: string]: string } }; +} + +interface State { + baseComponent?: CodeComponent; + breadcrumbs: Array; + components?: Array; + error?: string; + loading: boolean; + page: number; + searchResults?: Array; + sourceViewer?: CodeComponent; + total: number; +} + +export default class App extends React.PureComponent { + mounted: boolean; + state: State = { + loading: true, + breadcrumbs: [], + total: 0, + page: 0 + }; + + componentDidMount() { + this.mounted = true; + this.handleComponentChange(); + } + + componentDidUpdate(prevProps: Props) { + if (prevProps.component !== this.props.component || prevProps.branch !== this.props.branch) { + this.handleComponentChange(); + } else if (prevProps.location !== this.props.location) { + this.handleUpdate(); + } + } + + componentWillUnmount() { + clearBucket(); + this.mounted = false; + } + + handleComponentChange() { + const { branch, component } = this.props; + + // we already know component's breadcrumbs, + addComponentBreadcrumbs(component.key, component.breadcrumbs); + + this.setState({ loading: true }); + const isPortfolio = ['VW', 'SVW'].includes(component.qualifier); + retrieveComponentChildren(component.key, isPortfolio, getBranchName(branch)) + .then(() => { + addComponent(component); + this.handleUpdate(); + }) + .catch(e => { + if (this.mounted) { + this.setState({ loading: false }); + parseError(e).then(this.handleError); + } + }); + } + + loadComponent(componentKey: string) { + this.setState({ loading: true }); + + const isPortfolio = ['VW', 'SVW'].includes(this.props.component.qualifier); + retrieveComponent(componentKey, isPortfolio, getBranchName(this.props.branch)) + .then(r => { + if (this.mounted) { + if (['FIL', 'UTS'].includes(r.component.qualifier)) { + this.setState({ + loading: false, + sourceViewer: r.component, + breadcrumbs: r.breadcrumbs, + searchResults: undefined + }); + } else { + this.setState({ + loading: false, + baseComponent: r.component, + components: r.components, + breadcrumbs: r.breadcrumbs, + total: r.total, + page: r.page, + sourceViewer: undefined, + searchResults: undefined + }); + } + } + }) + .catch(e => { + if (this.mounted) { + this.setState({ loading: false }); + parseError(e).then(this.handleError); + } + }); + } + + handleUpdate() { + const { component, location } = this.props; + const { selected } = location.query; + const finalKey = selected || component.key; + + this.loadComponent(finalKey); + } + + handleLoadMore = () => { + const { baseComponent, components, page } = this.state; + if (!baseComponent || !components) { + return; + } + const isPortfolio = ['VW', 'SVW'].includes(this.props.component.qualifier); + loadMoreChildren(baseComponent.key, page + 1, isPortfolio, getBranchName(this.props.branch)) + .then(r => { + if (this.mounted) { + this.setState({ + components: [...components, ...r.components], + page: r.page, + total: r.total + }); + } + }) + .catch(e => { + if (this.mounted) { + this.setState({ loading: false }); + parseError(e).then(this.handleError); + } + }); + }; + + handleError = (error: string) => { + if (this.mounted) { + this.setState({ error }); + } + }; + + render() { + const { branch, component, location } = this.props; + const { + loading, + error, + baseComponent, + components, + breadcrumbs, + total, + sourceViewer + } = this.state; + const branchName = getBranchName(branch); + + const shouldShowBreadcrumbs = breadcrumbs.length > 1; + + const componentsClassName = classNames('spacer-top', { 'new-loading': loading }); + + return ( +
    + + + {error && +
    + {error} +
    } + + + +
    + {shouldShowBreadcrumbs && + } + + {sourceViewer == undefined && + components != undefined && +
    + +
    } + + {sourceViewer == undefined && + components != undefined && + } + + {sourceViewer != undefined && +
    + +
    } +
    +
    + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/code/components/Breadcrumb.js b/server/sonar-web/src/main/js/apps/code/components/Breadcrumb.js deleted file mode 100644 index 3e0ce74f384..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/Breadcrumb.js +++ /dev/null @@ -1,27 +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. - */ -import React from 'react'; -import ComponentName from './ComponentName'; - -export default function Breadcrumb({ rootComponent, component, canBrowse }) { - return ( - - ); -} diff --git a/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.js b/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.js deleted file mode 100644 index a6f5a0aedfa..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.js +++ /dev/null @@ -1,37 +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. - */ -import React from 'react'; -import Breadcrumb from './Breadcrumb'; - -export default function Breadcrumbs({ rootComponent, breadcrumbs }) { - return ( -
      - {breadcrumbs.map((component, index) => -
    • - -
    • - )} -
    - ); -} diff --git a/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.tsx b/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.tsx new file mode 100644 index 00000000000..3c8feee7b2b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.tsx @@ -0,0 +1,45 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import ComponentName from './ComponentName'; +import { Component } from '../types'; + +interface Props { + branch?: string; + breadcrumbs: Component[]; + rootComponent: Component; +} + +export default function Breadcrumbs({ branch, breadcrumbs, rootComponent }: Props) { + return ( +
      + {breadcrumbs.map((component, index) => +
    • + +
    • + )} +
    + ); +} diff --git a/server/sonar-web/src/main/js/apps/code/components/Component.js b/server/sonar-web/src/main/js/apps/code/components/Component.js deleted file mode 100644 index 4fe40dfa618..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/Component.js +++ /dev/null @@ -1,128 +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. - */ -import classNames from 'classnames'; -import React from 'react'; -import ReactDOM from 'react-dom'; -import ComponentName from './ComponentName'; -import ComponentMeasure from './ComponentMeasure'; -import ComponentDetach from './ComponentDetach'; -import ComponentPin from './ComponentPin'; - -const TOP_OFFSET = 200; -const BOTTOM_OFFSET = 10; - -export default class Component extends React.PureComponent { - componentDidMount() { - this.handleUpdate(); - } - - componentDidUpdate() { - this.handleUpdate(); - } - - handleUpdate() { - const { selected } = this.props; - - // scroll viewport so the current selected component is visible - if (selected) { - setTimeout(() => { - this.handleScroll(); - }, 0); - } - } - - handleScroll() { - const node = ReactDOM.findDOMNode(this); - const position = node.getBoundingClientRect(); - const { top, bottom } = position; - if (bottom > window.innerHeight - BOTTOM_OFFSET) { - window.scrollTo(0, bottom - window.innerHeight + window.scrollY + BOTTOM_OFFSET); - } else if (top < TOP_OFFSET) { - window.scrollTo(0, top + window.scrollY - TOP_OFFSET); - } - } - - render() { - const { component, rootComponent, selected, previous, canBrowse } = this.props; - const isPortfolio = ['VW', 'SVW'].includes(rootComponent.qualifier); - const isApplication = rootComponent.qualifier === 'APP'; - - let componentAction = null; - - if (!component.refKey || component.qualifier === 'SVW') { - switch (component.qualifier) { - case 'FIL': - case 'UTS': - componentAction = ; - break; - default: - componentAction = ; - } - } - - const columns = isPortfolio - ? [ - { metric: 'releasability_rating', type: 'RATING' }, - { metric: 'reliability_rating', type: 'RATING' }, - { metric: 'security_rating', type: 'RATING' }, - { metric: 'sqale_rating', type: 'RATING' }, - { metric: 'ncloc', type: 'SHORT_INT' } - ] - : [ - isApplication && { metric: 'alert_status', type: 'LEVEL' }, - { metric: 'ncloc', type: 'SHORT_INT' }, - { metric: 'bugs', type: 'SHORT_INT' }, - { metric: 'vulnerabilities', type: 'SHORT_INT' }, - { metric: 'code_smells', type: 'SHORT_INT' }, - { metric: 'coverage', type: 'PERCENT' }, - { metric: 'duplicated_lines_density', type: 'PERCENT' } - ].filter(Boolean); - - return ( - - - - {componentAction} - - - - - - - {columns.map(column => - -
    - -
    - - )} - - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/code/components/Component.tsx b/server/sonar-web/src/main/js/apps/code/components/Component.tsx new file mode 100644 index 00000000000..5294779fd66 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/code/components/Component.tsx @@ -0,0 +1,146 @@ +/* + * 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 classNames from 'classnames'; +import * as React from 'react'; +import ComponentName from './ComponentName'; +import ComponentMeasure from './ComponentMeasure'; +import ComponentDetach from './ComponentDetach'; +import ComponentPin from './ComponentPin'; +import { Component as IComponent } from '../types'; + +const TOP_OFFSET = 200; +const BOTTOM_OFFSET = 10; + +interface Props { + branch?: string; + canBrowse?: boolean; + component: IComponent; + previous?: IComponent; + rootComponent: IComponent; + selected?: boolean; +} + +export default class Component extends React.PureComponent { + node: HTMLElement; + + componentDidMount() { + this.handleUpdate(); + } + + componentDidUpdate() { + this.handleUpdate(); + } + + handleUpdate() { + const { selected } = this.props; + + // scroll viewport so the current selected component is visible + if (selected) { + setTimeout(() => { + this.handleScroll(); + }, 0); + } + } + + handleScroll() { + const position = this.node.getBoundingClientRect(); + const { top, bottom } = position; + if (bottom > window.innerHeight - BOTTOM_OFFSET) { + window.scrollTo(0, bottom - window.innerHeight + window.scrollY + BOTTOM_OFFSET); + } else if (top < TOP_OFFSET) { + window.scrollTo(0, top + window.scrollY - TOP_OFFSET); + } + } + + render() { + const { + branch, + component, + rootComponent, + selected = false, + previous, + canBrowse = false + } = this.props; + const isPortfolio = ['VW', 'SVW'].includes(rootComponent.qualifier); + const isApplication = rootComponent.qualifier === 'APP'; + + let componentAction = null; + + if (!component.refKey || component.qualifier === 'SVW') { + switch (component.qualifier) { + case 'FIL': + case 'UTS': + componentAction = ; + break; + default: + componentAction = ; + } + } + + const columns = isPortfolio + ? [ + { metric: 'releasability_rating', type: 'RATING' }, + { metric: 'reliability_rating', type: 'RATING' }, + { metric: 'security_rating', type: 'RATING' }, + { metric: 'sqale_rating', type: 'RATING' }, + { metric: 'ncloc', type: 'SHORT_INT' } + ] + : [ + isApplication && { metric: 'alert_status', type: 'LEVEL' }, + { metric: 'ncloc', type: 'SHORT_INT' }, + { metric: 'bugs', type: 'SHORT_INT' }, + { metric: 'vulnerabilities', type: 'SHORT_INT' }, + { metric: 'code_smells', type: 'SHORT_INT' }, + { metric: 'coverage', type: 'PERCENT' }, + { metric: 'duplicated_lines_density', type: 'PERCENT' } + ].filter(Boolean) as Array<{ metric: string; type: string }>; + + return ( + (this.node = node as HTMLElement)}> + + + {componentAction} + + + + + + + {columns.map(column => + +
    + +
    + + )} + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentDetach.js b/server/sonar-web/src/main/js/apps/code/components/ComponentDetach.js deleted file mode 100644 index 30fccdfa7bf..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentDetach.js +++ /dev/null @@ -1,32 +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. - */ -import React from 'react'; -import { Link } from 'react-router'; -import { translate } from '../../../helpers/l10n'; - -export default function ComponentDetach({ component, branch }) { - return ( - - ); -} diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentDetach.tsx b/server/sonar-web/src/main/js/apps/code/components/ComponentDetach.tsx new file mode 100644 index 00000000000..0a237c54bb8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentDetach.tsx @@ -0,0 +1,38 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { Link } from 'react-router'; +import { translate } from '../../../helpers/l10n'; +import { Component } from '../types'; + +interface Props { + branch?: string; + component: Component; +} + +export default function ComponentDetach({ component, branch }: Props) { + return ( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.js b/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.js deleted file mode 100644 index b0e2db4adc1..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.js +++ /dev/null @@ -1,43 +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. - */ -import React from 'react'; -import Measure from '../../../components/measure/Measure'; - -const ComponentMeasure = ({ component, metricKey, metricType }) => { - const isProject = component.qualifier === 'TRK'; - const isReleasability = metricKey === 'releasability_rating'; - - const finalMetricKey = isProject && isReleasability ? 'alert_status' : metricKey; - const finalMetricType = isProject && isReleasability ? 'LEVEL' : metricType; - - const measure = - Array.isArray(component.measures) && - component.measures.find(measure => measure.metric === finalMetricKey); - - if (!measure) { - return ; - } - - return ( - - ); -}; - -export default ComponentMeasure; diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx b/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx new file mode 100644 index 00000000000..c765ff3eb59 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx @@ -0,0 +1,51 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import Measure from '../../../components/measure/Measure'; +import { Component } from '../types'; + +interface Props { + component: Component; + metricKey: string; + metricType: string; +} + +export default function ComponentMeasure({ component, metricKey, metricType }: Props) { + const isProject = component.qualifier === 'TRK'; + const isReleasability = metricKey === 'releasability_rating'; + + const finalMetricKey = isProject && isReleasability ? 'alert_status' : metricKey; + const finalMetricType = isProject && isReleasability ? 'LEVEL' : metricType; + + const measure = + Array.isArray(component.measures) && + component.measures.find(measure => measure.metric === finalMetricKey); + + if (!measure) { + return ; + } + + // TODO + const AnyMeasure = Measure as any; + + return ( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentName.js b/server/sonar-web/src/main/js/apps/code/components/ComponentName.js deleted file mode 100644 index 921aa3f724c..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentName.js +++ /dev/null @@ -1,98 +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. - */ -import React from 'react'; -import { Link } from 'react-router'; -import Truncated from './Truncated'; -import QualifierIcon from '../../../components/shared/QualifierIcon'; - -function getTooltip(component) { - const isFile = component.qualifier === 'FIL' || component.qualifier === 'UTS'; - if (isFile && component.path) { - return component.path + '\n\n' + component.key; - } else { - return component.name + '\n\n' + component.key; - } -} - -function mostCommitPrefix(strings) { - const sortedStrings = strings.slice(0).sort(); - const firstString = sortedStrings[0]; - const firstStringLength = firstString.length; - const lastString = sortedStrings[sortedStrings.length - 1]; - let i = 0; - while (i < firstStringLength && firstString.charAt(i) === lastString.charAt(i)) { - i++; - } - const prefix = firstString.substr(0, i); - const prefixTokens = prefix.split(/[\s\\\/]/); - const lastPrefixPart = prefixTokens[prefixTokens.length - 1]; - return prefix.substr(0, prefix.length - lastPrefixPart.length); -} - -const ComponentName = ({ component, rootComponent, previous, canBrowse }) => { - const areBothDirs = component.qualifier === 'DIR' && previous && previous.qualifier === 'DIR'; - const prefix = areBothDirs ? mostCommitPrefix([component.name + '/', previous.name + '/']) : ''; - const name = prefix - ? - - {prefix} - - - {component.name.substr(prefix.length)} - - - : component.name; - - let inner = null; - - if (component.refKey && component.qualifier !== 'SVW') { - inner = ( - - {name} - - ); - } else if (canBrowse) { - const query = { id: rootComponent.key, branch: rootComponent.branch }; - if (component.key !== rootComponent.key) { - Object.assign(query, { selected: component.key }); - } - inner = ( - - {name} - - ); - } else { - inner = ( - - {name} - - ); - } - - return ( - - {inner} - - ); -}; - -export default ComponentName; diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx b/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx new file mode 100644 index 00000000000..f6f2474ff42 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx @@ -0,0 +1,109 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { Link } from 'react-router'; +import Truncated from './Truncated'; +import QualifierIcon from '../../../components/shared/QualifierIcon'; +import { Component } from '../types'; + +function getTooltip(component: Component) { + const isFile = component.qualifier === 'FIL' || component.qualifier === 'UTS'; + if (isFile && component.path) { + return component.path + '\n\n' + component.key; + } else { + return component.name + '\n\n' + component.key; + } +} + +function mostCommitPrefix(strings: string[]) { + const sortedStrings = strings.slice(0).sort(); + const firstString = sortedStrings[0]; + const firstStringLength = firstString.length; + const lastString = sortedStrings[sortedStrings.length - 1]; + let i = 0; + while (i < firstStringLength && firstString.charAt(i) === lastString.charAt(i)) { + i++; + } + const prefix = firstString.substr(0, i); + const prefixTokens = prefix.split(/[\s\\\/]/); + const lastPrefixPart = prefixTokens[prefixTokens.length - 1]; + return prefix.substr(0, prefix.length - lastPrefixPart.length); +} + +interface Props { + branch?: string; + canBrowse?: boolean; + component: Component; + previous?: Component; + rootComponent: Component; +} + +export default function ComponentName(props: Props) { + const { branch, component, rootComponent, previous, canBrowse = false } = props; + const areBothDirs = component.qualifier === 'DIR' && previous && previous.qualifier === 'DIR'; + const prefix = + areBothDirs && previous != undefined + ? mostCommitPrefix([component.name + '/', previous.name + '/']) + : ''; + const name = prefix + ? + + {prefix} + + + {component.name.substr(prefix.length)} + + + : component.name; + + let inner = null; + + if (component.refKey && component.qualifier !== 'SVW') { + inner = ( + + {name} + + ); + } else if (canBrowse) { + const query = { id: rootComponent.key, branch }; + if (component.key !== rootComponent.key) { + Object.assign(query, { selected: component.key }); + } + inner = ( + + {name} + + ); + } else { + inner = ( + + {name} + + ); + } + + return ( + + {inner} + + ); +} diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentPin.js b/server/sonar-web/src/main/js/apps/code/components/ComponentPin.js deleted file mode 100644 index 641017207d7..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentPin.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. - */ -import React from 'react'; -import Workspace from '../../../components/workspace/main'; -import PinIcon from '../../../components/shared/pin-icon'; -import { translate } from '../../../helpers/l10n'; - -const ComponentPin = ({ branch, component }) => { - const handleClick = e => { - e.preventDefault(); - Workspace.openComponent({ branch, key: component.key }); - }; - - return ( - - - - ); -}; - -export default ComponentPin; diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentPin.tsx b/server/sonar-web/src/main/js/apps/code/components/ComponentPin.tsx new file mode 100644 index 00000000000..785ce1640b2 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentPin.tsx @@ -0,0 +1,46 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import Workspace from '../../../components/workspace/main'; +import PinIcon from '../../../components/shared/pin-icon'; +import { translate } from '../../../helpers/l10n'; +import { Component } from '../types'; + +interface Props { + branch?: string; + component: Component; +} + +export default function ComponentPin({ branch, component }: Props) { + const handleClick = (event: React.SyntheticEvent) => { + event.preventDefault(); + Workspace.openComponent({ branch, key: component.key }); + }; + + return ( + + + + ); +} diff --git a/server/sonar-web/src/main/js/apps/code/components/Components.js b/server/sonar-web/src/main/js/apps/code/components/Components.js deleted file mode 100644 index 8e1451ff79e..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/Components.js +++ /dev/null @@ -1,56 +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. - */ -import React from 'react'; -import Component from './Component'; -import ComponentsEmpty from './ComponentsEmpty'; -import ComponentsHeader from './ComponentsHeader'; - -export default function Components({ rootComponent, baseComponent, components, selected }) { - return ( - - - {baseComponent && - - - - - - } - - {components.length - ? components.map((component, index, list) => - 0 ? list[index - 1] : null} - canBrowse={true} - /> - ) - : } - -
     
    - ); -} diff --git a/server/sonar-web/src/main/js/apps/code/components/Components.tsx b/server/sonar-web/src/main/js/apps/code/components/Components.tsx new file mode 100644 index 00000000000..89decb3012c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/code/components/Components.tsx @@ -0,0 +1,68 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import Component from './Component'; +import ComponentsEmpty from './ComponentsEmpty'; +import ComponentsHeader from './ComponentsHeader'; +import { Component as IComponent } from '../types'; + +interface Props { + baseComponent?: IComponent; + branch?: string; + components: IComponent[]; + rootComponent: IComponent; + selected?: IComponent; +} + +export default function Components(props: Props) { + const { baseComponent, branch, components, rootComponent, selected } = props; + return ( + + + {baseComponent && + + + + + + } + + {components.length + ? components.map((component, index, list) => + 0 ? list[index - 1] : undefined} + rootComponent={rootComponent} + selected={component === selected} + /> + ) + : } + +
     
    + ); +} diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.js b/server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.js deleted file mode 100644 index 02169f172db..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.js +++ /dev/null @@ -1,32 +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. - */ -import React from 'react'; -import { translate } from '../../../helpers/l10n'; - -export default function ComponentsEmpty() { - return ( - - - {translate('no_results')} - - - - ); -} diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.tsx b/server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.tsx new file mode 100644 index 00000000000..8ea0b1a11b6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.tsx @@ -0,0 +1,32 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { translate } from '../../../helpers/l10n'; + +export default function ComponentsEmpty() { + return ( + + + {translate('no_results')} + + + + ); +} diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.js b/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.js deleted file mode 100644 index a2977ae960b..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.js +++ /dev/null @@ -1,60 +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. - */ -import React from 'react'; -import { translate } from '../../../helpers/l10n'; - -const ComponentsHeader = ({ baseComponent, rootComponent }) => { - const isPortfolio = rootComponent.qualifier === 'VW' || rootComponent.qualifier === 'SVW'; - const isApplication = rootComponent.qualifier === 'APP'; - - const columns = isPortfolio - ? [ - translate('metric_domain.Releasability'), - translate('metric_domain.Reliability'), - translate('metric_domain.Security'), - translate('metric_domain.Maintainability'), - translate('metric', 'ncloc', 'name') - ] - : [ - isApplication && translate('metric.alert_status.name'), - translate('metric', 'ncloc', 'name'), - translate('metric', 'bugs', 'name'), - translate('metric', 'vulnerabilities', 'name'), - translate('metric', 'code_smells', 'name'), - translate('metric', 'coverage', 'name'), - translate('metric', 'duplicated_lines_density', 'short_name') - ].filter(Boolean); - - return ( - - -   -   - {columns.map(column => - - {baseComponent && column} - - )} - - - ); -}; - -export default ComponentsHeader; diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.tsx b/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.tsx new file mode 100644 index 00000000000..4717af5957f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.tsx @@ -0,0 +1,64 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { translate } from '../../../helpers/l10n'; +import { Component } from '../types'; + +interface Props { + baseComponent?: Component; + rootComponent: Component; +} + +export default function ComponentsHeader({ baseComponent, rootComponent }: Props) { + const isPortfolio = rootComponent.qualifier === 'VW' || rootComponent.qualifier === 'SVW'; + const isApplication = rootComponent.qualifier === 'APP'; + + const columns = isPortfolio + ? [ + translate('metric_domain.Releasability'), + translate('metric_domain.Reliability'), + translate('metric_domain.Security'), + translate('metric_domain.Maintainability'), + translate('metric', 'ncloc', 'name') + ] + : [ + isApplication && translate('metric.alert_status.name'), + translate('metric', 'ncloc', 'name'), + translate('metric', 'bugs', 'name'), + translate('metric', 'vulnerabilities', 'name'), + translate('metric', 'code_smells', 'name'), + translate('metric', 'coverage', 'name'), + translate('metric', 'duplicated_lines_density', 'short_name') + ].filter(Boolean) as string[]; + + return ( + + +   +   + {columns.map(column => + + {baseComponent && column} + + )} + + + ); +} diff --git a/server/sonar-web/src/main/js/apps/code/components/Search.js b/server/sonar-web/src/main/js/apps/code/components/Search.js deleted file mode 100644 index 4d772e7a3b1..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/Search.js +++ /dev/null @@ -1,228 +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. - */ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import { debounce } from 'lodash'; -import Components from './Components'; -import { getTree } from '../../../api/components'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { parseError } from '../utils'; -import { getComponentUrl } from '../../../helpers/urls'; - -export default class Search extends React.PureComponent { - static contextTypes = { - router: PropTypes.object.isRequired - }; - - static propTypes = { - component: PropTypes.object.isRequired, - location: PropTypes.object.isRequired, - onError: PropTypes.func.isRequired - }; - - state = { - query: '', - loading: false, - results: null, - selectedIndex: null - }; - - componentWillMount() { - this.handleSearch = debounce(this.handleSearch, 250); - } - - componentDidMount() { - this.mounted = true; - this.refs.input.focus(); - } - - componentWillReceiveProps(nextProps) { - // if the url has change, reset the current state - if (nextProps.location !== this.props.location) { - this.setState({ - query: '', - loading: false, - results: null, - selectedIndex: null - }); - } - } - - componentWillUnmount() { - this.mounted = false; - } - - checkInputValue(query) { - return this.refs.input.value === query; - } - - handleSelectNext() { - const { selectedIndex, results } = this.state; - if (results != null && selectedIndex != null && selectedIndex < results.length - 1) { - this.setState({ selectedIndex: selectedIndex + 1 }); - } - } - - handleSelectPrevious() { - const { selectedIndex, results } = this.state; - if (results != null && selectedIndex != null && selectedIndex > 0) { - this.setState({ selectedIndex: selectedIndex - 1 }); - } - } - - handleSelectCurrent() { - const { component } = this.props; - const { results, selectedIndex } = this.state; - if (results != null && selectedIndex != null) { - const selected = results[selectedIndex]; - - if (selected.refKey) { - window.location = getComponentUrl(selected.refKey); - } else { - this.context.router.push({ - pathname: '/code', - query: { - branch: component.branch, - id: component.key, - selected: selected.key - } - }); - } - } - } - - handleKeyDown(e) { - switch (e.keyCode) { - case 13: - e.preventDefault(); - this.handleSelectCurrent(); - break; - case 38: - e.preventDefault(); - this.handleSelectPrevious(); - break; - case 40: - e.preventDefault(); - this.handleSelectNext(); - break; - default: // do nothing - } - } - - handleSearch = query => { - // first time check if value has changed due to debounce - if (this.mounted && this.checkInputValue(query)) { - const { component, onError } = this.props; - this.setState({ loading: true }); - - const isPortfolio = ['VW', 'SVW', 'APP'].includes(component.qualifier); - const qualifiers = isPortfolio ? 'SVW,TRK' : 'BRC,UTS,FIL'; - - getTree(component.key, { - branch: component.branch, - q: query, - s: 'qualifier,name', - qualifiers - }) - .then(r => { - // second time check if value has change due to api request - if (this.mounted && this.checkInputValue(query)) { - this.setState({ - results: r.components, - selectedIndex: r.components.length > 0 ? 0 : null, - loading: false - }); - } - }) - .catch(e => { - // second time check if value has change due to api request - if (this.mounted && this.checkInputValue(query)) { - this.setState({ loading: false }); - parseError(e).then(onError); - } - }); - } - }; - - handleQueryChange(query) { - this.setState({ query }); - if (query.length < 3) { - this.setState({ results: null }); - } else { - this.handleSearch(query); - } - } - - handleInputChange(e) { - const query = e.target.value; - this.handleQueryChange(query); - } - - handleSubmit(e) { - e.preventDefault(); - const query = this.refs.input.value; - this.handleQueryChange(query); - } - - render() { - const { component } = this.props; - const { query, loading, selectedIndex, results } = this.state; - const selected = selectedIndex != null && results != null ? results[selectedIndex] : null; - const containerClassName = classNames('code-search', { - 'code-search-with-results': results != null - }); - const inputClassName = classNames('search-box-input', { - touched: query.length > 0 && query.length < 3 - }); - - return ( - - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/code/components/Search.tsx b/server/sonar-web/src/main/js/apps/code/components/Search.tsx new file mode 100644 index 00000000000..a66320d85d9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/code/components/Search.tsx @@ -0,0 +1,234 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import * as PropTypes from 'prop-types'; +import * as classNames from 'classnames'; +import { debounce } from 'lodash'; +import Components from './Components'; +import { getTree } from '../../../api/components'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { parseError } from '../utils'; +import { getProjectUrl } from '../../../helpers/urls'; +import { Component } from '../types'; + +interface Props { + branch?: string; + component: Component; + location: {}; + onError: (error: string) => void; +} + +interface State { + query: string; + loading: boolean; + results?: Component[]; + selectedIndex?: number; +} + +export default class Search extends React.PureComponent { + input: HTMLInputElement; + mounted: boolean; + + static contextTypes = { + router: PropTypes.object.isRequired + }; + + state: State = { + query: '', + loading: false + }; + + componentWillMount() { + this.handleSearch = debounce(this.handleSearch, 250); + } + + componentDidMount() { + this.mounted = true; + this.input.focus(); + } + + componentWillReceiveProps(nextProps: Props) { + // if the url has change, reset the current state + if (nextProps.location !== this.props.location) { + this.setState({ + query: '', + loading: false, + results: undefined, + selectedIndex: undefined + }); + } + } + + componentWillUnmount() { + this.mounted = false; + } + + checkInputValue(query: string) { + return this.input.value === query; + } + + handleSelectNext() { + const { selectedIndex, results } = this.state; + if (results != null && selectedIndex != null && selectedIndex < results.length - 1) { + this.setState({ selectedIndex: selectedIndex + 1 }); + } + } + + handleSelectPrevious() { + const { selectedIndex, results } = this.state; + if (results != null && selectedIndex != null && selectedIndex > 0) { + this.setState({ selectedIndex: selectedIndex - 1 }); + } + } + + handleSelectCurrent() { + const { branch, component } = this.props; + const { results, selectedIndex } = this.state; + if (results != null && selectedIndex != null) { + const selected = results[selectedIndex]; + + if (selected.refKey) { + this.context.router.push(getProjectUrl(selected.refKey)); + } else { + this.context.router.push({ + pathname: '/code', + query: { branch, id: component.key, selected: selected.key } + }); + } + } + } + + handleKeyDown(e: React.KeyboardEvent) { + switch (e.keyCode) { + case 13: + e.preventDefault(); + this.handleSelectCurrent(); + break; + case 38: + e.preventDefault(); + this.handleSelectPrevious(); + break; + case 40: + e.preventDefault(); + this.handleSelectNext(); + break; + default: // do nothing + } + } + + handleSearch = (query: string) => { + // first time check if value has changed due to debounce + if (this.mounted && this.checkInputValue(query)) { + const { branch, component, onError } = this.props; + this.setState({ loading: true }); + + const isPortfolio = ['VW', 'SVW', 'APP'].includes(component.qualifier); + const qualifiers = isPortfolio ? 'SVW,TRK' : 'BRC,UTS,FIL'; + + getTree(component.key, { branch, q: query, s: 'qualifier,name', qualifiers }) + .then(r => { + // second time check if value has change due to api request + if (this.mounted && this.checkInputValue(query)) { + this.setState({ + results: r.components, + selectedIndex: r.components.length > 0 ? 0 : undefined, + loading: false + }); + } + }) + .catch(e => { + // second time check if value has change due to api request + if (this.mounted && this.checkInputValue(query)) { + this.setState({ loading: false }); + parseError(e).then(onError); + } + }); + } + }; + + handleQueryChange(query: string) { + this.setState({ query }); + if (query.length < 3) { + this.setState({ results: undefined }); + } else { + this.handleSearch(query); + } + } + + handleInputChange(event: React.SyntheticEvent) { + const query = event.currentTarget.value; + this.handleQueryChange(query); + } + + handleSubmit(event: React.SyntheticEvent) { + event.preventDefault(); + const query = this.input.value; + this.handleQueryChange(query); + } + + render() { + const { component } = this.props; + const { query, loading, selectedIndex, results } = this.state; + const selected = selectedIndex != null && results != null ? results[selectedIndex] : undefined; + const containerClassName = classNames('code-search', { + 'code-search-with-results': results != null + }); + const inputClassName = classNames('search-box-input', { + touched: query.length > 0 && query.length < 3 + }); + + return ( + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/code/components/Truncated.js b/server/sonar-web/src/main/js/apps/code/components/Truncated.js deleted file mode 100644 index c6e380859db..00000000000 --- a/server/sonar-web/src/main/js/apps/code/components/Truncated.js +++ /dev/null @@ -1,28 +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. - */ -import React from 'react'; - -export default function Truncated({ children, title }) { - return ( - - {children} - - ); -} diff --git a/server/sonar-web/src/main/js/apps/code/components/Truncated.tsx b/server/sonar-web/src/main/js/apps/code/components/Truncated.tsx new file mode 100644 index 00000000000..5409db09ea5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/code/components/Truncated.tsx @@ -0,0 +1,33 @@ +/* + * 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 { + children: React.ReactNode; + title: string; +} + +export default function Truncated({ children, title }: Props) { + return ( + + {children} + + ); +} diff --git a/server/sonar-web/src/main/js/apps/code/types.ts b/server/sonar-web/src/main/js/apps/code/types.ts new file mode 100644 index 00000000000..3bf912f0a35 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/code/types.ts @@ -0,0 +1,41 @@ +/* + * 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. + */ +interface Measure { + metric: string; + value: string; + periods?: Period[]; +} + +interface Period { + index: number; + value: string; +} + +export interface Component extends Breadcrumb { + measures?: Measure[]; + path?: string; + refKey?: string; +} + +export interface Breadcrumb { + key: string; + name: string; + qualifier: string; +} diff --git a/server/sonar-web/src/main/js/apps/code/utils.js b/server/sonar-web/src/main/js/apps/code/utils.js deleted file mode 100644 index 52975be78ec..00000000000 --- a/server/sonar-web/src/main/js/apps/code/utils.js +++ /dev/null @@ -1,228 +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. - */ -import { without } from 'lodash'; -import { - addComponent, - getComponent as getComponentFromBucket, - addComponentChildren, - getComponentChildren, - addComponentBreadcrumbs, - getComponentBreadcrumbs -} from './bucket'; -import { getChildren, getComponent, getBreadcrumbs } from '../../api/components'; -import { translate } from '../../helpers/l10n'; - -const METRICS = [ - 'ncloc', - 'code_smells', - 'bugs', - 'vulnerabilities', - 'coverage', - 'duplicated_lines_density', - 'alert_status' -]; - -const PORTFOLIO_METRICS = [ - 'releasability_rating', - 'alert_status', - 'reliability_rating', - 'security_rating', - 'sqale_rating', - 'ncloc' -]; - -const PAGE_SIZE = 100; - -function requestChildren(componentKey, metrics, page) { - return getChildren(componentKey, metrics, { p: page, ps: PAGE_SIZE }).then(r => { - if (r.paging.total > r.paging.pageSize * r.paging.pageIndex) { - return requestChildren(componentKey, metrics, page + 1).then(moreComponents => { - return [...r.components, ...moreComponents]; - }); - } - return r.components; - }); -} - -function requestAllChildren(componentKey, metrics) { - return requestChildren(componentKey, metrics, 1); -} - -function expandRootDir(metrics) { - return function({ components, total, ...other }) { - const rootDir = components.find( - component => component.qualifier === 'DIR' && component.name === '/' - ); - if (rootDir) { - return requestAllChildren(rootDir.key, metrics).then(rootDirComponents => { - const nextComponents = without([...rootDirComponents, ...components], rootDir); - const nextTotal = total + rootDirComponents.length - /* root dir */ 1; - return { components: nextComponents, total: nextTotal, ...other }; - }); - } else { - return { components, total, ...other }; - } - }; -} - -function prepareChildren(r) { - return { - components: r.components, - total: r.paging.total, - page: r.paging.pageIndex, - baseComponent: r.baseComponent - }; -} - -function skipRootDir(breadcrumbs) { - return breadcrumbs.filter(component => { - return !(component.qualifier === 'DIR' && component.name === '/'); - }); -} - -function storeChildrenBase(children) { - children.forEach(addComponent); -} - -function storeChildrenBreadcrumbs(parentComponentKey, children) { - const parentBreadcrumbs = getComponentBreadcrumbs(parentComponentKey); - if (parentBreadcrumbs) { - children.forEach(child => { - const breadcrumbs = [...parentBreadcrumbs, child]; - addComponentBreadcrumbs(child.key, breadcrumbs); - }); - } -} - -function getMetrics(isPortfolio) { - return isPortfolio ? PORTFOLIO_METRICS : METRICS; -} - -/** - * @param {string} componentKey - * @param {boolean} isPortfolio - * @returns {Promise} - */ -function retrieveComponentBase(componentKey, isPortfolio, branch) { - const existing = getComponentFromBucket(componentKey); - if (existing) { - return Promise.resolve(existing); - } - - const metrics = getMetrics(isPortfolio); - - return getComponent(componentKey, metrics, branch).then(component => { - addComponent(component); - return component; - }); -} - -/** - * @param {string} componentKey - * @param {boolean} isPortfolio - * @returns {Promise} - */ -export function retrieveComponentChildren(componentKey, isPortfolio, branch) { - const existing = getComponentChildren(componentKey); - if (existing) { - return Promise.resolve({ - components: existing.children, - total: existing.total, - page: existing.page - }); - } - - const metrics = getMetrics(isPortfolio); - - return getChildren(componentKey, metrics, { branch, ps: PAGE_SIZE, s: 'qualifier,name' }) - .then(prepareChildren) - .then(expandRootDir(metrics)) - .then(r => { - addComponentChildren(componentKey, r.components, r.total, r.page); - storeChildrenBase(r.components); - storeChildrenBreadcrumbs(componentKey, r.components); - return r; - }); -} - -function retrieveComponentBreadcrumbs(componentKey, branch) { - const existing = getComponentBreadcrumbs(componentKey); - if (existing) { - return Promise.resolve(existing); - } - - return getBreadcrumbs(componentKey, branch).then(skipRootDir).then(breadcrumbs => { - addComponentBreadcrumbs(componentKey, breadcrumbs); - return breadcrumbs; - }); -} - -/** - * @param {string} componentKey - * @param {boolean} isPortfolio - * @returns {Promise} - */ -export function retrieveComponent(componentKey, isPortfolio, branch) { - return Promise.all([ - retrieveComponentBase(componentKey, isPortfolio, branch), - retrieveComponentChildren(componentKey, isPortfolio, branch), - retrieveComponentBreadcrumbs(componentKey, branch) - ]).then(r => { - return { - component: r[0], - components: r[1].components, - total: r[1].total, - page: r[1].page, - breadcrumbs: r[2] - }; - }); -} - -export function loadMoreChildren(componentKey, page, isPortfolio, branch) { - const metrics = getMetrics(isPortfolio); - - return getChildren(componentKey, metrics, { branch, ps: PAGE_SIZE, p: page }) - .then(prepareChildren) - .then(expandRootDir(metrics)) - .then(r => { - addComponentChildren(componentKey, r.components, r.total, r.page); - storeChildrenBase(r.components); - storeChildrenBreadcrumbs(componentKey, r.components); - return r; - }); -} - -/** - * Parse response of failed request - * @param {Error} error - * @returns {Promise} - */ -export function parseError(error) { - const DEFAULT_MESSAGE = translate('default_error_message'); - - try { - return error.response - .json() - .then(r => r.errors.map(error => error.msg).join('. ')) - .catch(() => DEFAULT_MESSAGE); - } catch (ex) { - return Promise.resolve(DEFAULT_MESSAGE); - } -} diff --git a/server/sonar-web/src/main/js/apps/code/utils.ts b/server/sonar-web/src/main/js/apps/code/utils.ts new file mode 100644 index 00000000000..4904ed4c50f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/code/utils.ts @@ -0,0 +1,245 @@ +/* + * 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 { without } from 'lodash'; +import { + addComponent, + getComponent as getComponentFromBucket, + addComponentChildren, + getComponentChildren, + addComponentBreadcrumbs, + getComponentBreadcrumbs +} from './bucket'; +import { Breadcrumb, Component } from './types'; +import { getChildren, getComponent, getBreadcrumbs } from '../../api/components'; +import { translate } from '../../helpers/l10n'; + +const METRICS = [ + 'ncloc', + 'code_smells', + 'bugs', + 'vulnerabilities', + 'coverage', + 'duplicated_lines_density', + 'alert_status' +]; + +const PORTFOLIO_METRICS = [ + 'releasability_rating', + 'alert_status', + 'reliability_rating', + 'security_rating', + 'sqale_rating', + 'ncloc' +]; + +const PAGE_SIZE = 100; + +function requestChildren( + componentKey: string, + metrics: string[], + page: number +): Promise { + return getChildren(componentKey, metrics, { p: page, ps: PAGE_SIZE }).then(r => { + if (r.paging.total > r.paging.pageSize * r.paging.pageIndex) { + return requestChildren(componentKey, metrics, page + 1).then(moreComponents => { + return [...r.components, ...moreComponents]; + }); + } + return r.components; + }); +} + +function requestAllChildren(componentKey: string, metrics: string[]): Promise { + return requestChildren(componentKey, metrics, 1); +} + +interface Children { + components: Component[]; + page: number; + total: number; +} + +interface ExpandRootDirFunc { + (children: Children): Promise; +} + +function expandRootDir(metrics: string[]): ExpandRootDirFunc { + return function({ components, total, ...other }) { + const rootDir = components.find( + (component: Component) => component.qualifier === 'DIR' && component.name === '/' + ); + if (rootDir) { + return requestAllChildren(rootDir.key, metrics).then(rootDirComponents => { + const nextComponents = without([...rootDirComponents, ...components], rootDir); + const nextTotal = total + rootDirComponents.length - /* root dir */ 1; + return { components: nextComponents, total: nextTotal, ...other }; + }); + } else { + return Promise.resolve({ components, total, ...other }); + } + }; +} + +function prepareChildren(r: any): Children { + return { + components: r.components, + total: r.paging.total, + page: r.paging.pageIndex + }; +} + +function skipRootDir(breadcrumbs: Component[]) { + return breadcrumbs.filter(component => { + return !(component.qualifier === 'DIR' && component.name === '/'); + }); +} + +function storeChildrenBase(children: Component[]) { + children.forEach(addComponent); +} + +function storeChildrenBreadcrumbs(parentComponentKey: string, children: Breadcrumb[]) { + const parentBreadcrumbs = getComponentBreadcrumbs(parentComponentKey); + if (parentBreadcrumbs) { + children.forEach(child => { + const breadcrumbs = [...parentBreadcrumbs, child]; + addComponentBreadcrumbs(child.key, breadcrumbs); + }); + } +} + +function getMetrics(isPortfolio: boolean) { + return isPortfolio ? PORTFOLIO_METRICS : METRICS; +} + +function retrieveComponentBase(componentKey: string, isPortfolio: boolean, branch?: string) { + const existing = getComponentFromBucket(componentKey); + if (existing) { + return Promise.resolve(existing); + } + + const metrics = getMetrics(isPortfolio); + + return getComponent(componentKey, metrics, branch).then(component => { + addComponent(component); + return component; + }); +} + +export function retrieveComponentChildren( + componentKey: string, + isPortfolio: boolean, + branch?: string +): Promise<{ components: Component[]; page: number; total: number }> { + const existing = getComponentChildren(componentKey); + if (existing) { + return Promise.resolve({ + components: existing.children, + total: existing.total, + page: existing.page + }); + } + + const metrics = getMetrics(isPortfolio); + + return getChildren(componentKey, metrics, { branch, ps: PAGE_SIZE, s: 'qualifier,name' }) + .then(prepareChildren) + .then(expandRootDir(metrics)) + .then(r => { + addComponentChildren(componentKey, r.components, r.total, r.page); + storeChildrenBase(r.components); + storeChildrenBreadcrumbs(componentKey, r.components); + return r; + }); +} + +function retrieveComponentBreadcrumbs( + componentKey: string, + branch?: string +): Promise { + const existing = getComponentBreadcrumbs(componentKey); + if (existing) { + return Promise.resolve(existing); + } + + return getBreadcrumbs(componentKey, branch).then(skipRootDir).then(breadcrumbs => { + addComponentBreadcrumbs(componentKey, breadcrumbs); + return breadcrumbs; + }); +} + +export function retrieveComponent( + componentKey: string, + isPortfolio: boolean, + branch?: string +): Promise<{ + breadcrumbs: Component[]; + component: Component; + components: Component[]; + page: number; + total: number; +}> { + return Promise.all([ + retrieveComponentBase(componentKey, isPortfolio, branch), + retrieveComponentChildren(componentKey, isPortfolio, branch), + retrieveComponentBreadcrumbs(componentKey, branch) + ]).then(r => { + return { + component: r[0], + components: r[1].components, + total: r[1].total, + page: r[1].page, + breadcrumbs: r[2] + }; + }); +} + +export function loadMoreChildren( + componentKey: string, + page: number, + isPortfolio: boolean, + branch?: string +): Promise { + const metrics = getMetrics(isPortfolio); + + return getChildren(componentKey, metrics, { branch, ps: PAGE_SIZE, p: page }) + .then(prepareChildren) + .then(expandRootDir(metrics)) + .then(r => { + addComponentChildren(componentKey, r.components, r.total, r.page); + storeChildrenBase(r.components); + storeChildrenBreadcrumbs(componentKey, r.components); + return r; + }); +} + +/** Parse response of failed request */ +export function parseError(error: { response: Response }): Promise { + const DEFAULT_MESSAGE = translate('default_error_message'); + + try { + return error.response + .json() + .then(r => r.errors.map((error: any) => error.msg).join('. ')) + .catch(() => DEFAULT_MESSAGE); + } catch (ex) { + return Promise.resolve(DEFAULT_MESSAGE); + } +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/App.js b/server/sonar-web/src/main/js/apps/component-measures/components/App.js index d75feac2ca2..e6545eea8ea 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/App.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/App.js @@ -25,6 +25,7 @@ import MeasureContentContainer from './MeasureContentContainer'; import MeasureOverviewContainer from './MeasureOverviewContainer'; import Sidebar from '../sidebar/Sidebar'; import { hasBubbleChart, parseQuery, serializeQuery } from '../utils'; +import { getBranchName } from '../../../helpers/branches'; import { translate } from '../../../helpers/l10n'; /*:: import type { Component, Query, Period } from '../types'; */ /*:: import type { RawQuery } from '../../../helpers/query'; */ @@ -33,12 +34,14 @@ import { translate } from '../../../helpers/l10n'; import '../style.css'; /*:: type Props = {| + branch: {}, component: Component, currentUser: { isLoggedIn: boolean }, location: { pathname: string, query: RawQuery }, fetchMeasures: ( component: string, - metricsKey: Array + metricsKey: Array, + branch: string | null ) => Promise<{ component: Component, measures: Array, leakPeriod: ?Period }>, fetchMetrics: () => void, metrics: { [string]: Metric }, @@ -81,6 +84,7 @@ export default class App extends React.PureComponent { componentWillReceiveProps(nextProps /*: Props */) { if ( + nextProps.branch !== this.props.branch || nextProps.component.key !== this.props.component.key || nextProps.metrics !== this.props.metrics ) { @@ -97,12 +101,12 @@ export default class App extends React.PureComponent { } } - fetchMeasures = ({ component, fetchMeasures, metrics, metricsKey } /*: Props */) => { + fetchMeasures = ({ branch, component, fetchMeasures, metrics, metricsKey } /*: Props */) => { this.setState({ loading: true }); const filteredKeys = metricsKey.filter( key => !metrics[key].hidden && !['DATA', 'DISTRIB'].includes(metrics[key].type) ); - fetchMeasures(component.key, filteredKeys).then( + fetchMeasures(component.key, filteredKeys, getBranchName(branch)).then( ({ measures, leakPeriod }) => { if (this.mounted) { this.setState({ @@ -125,6 +129,7 @@ export default class App extends React.PureComponent { pathname: this.props.location.pathname, query: { ...query, + branch: getBranchName(this.props.branch), id: this.props.component.key } }); @@ -135,7 +140,7 @@ export default class App extends React.PureComponent { if (isLoading) { return ; } - const { component, fetchMeasures, metrics } = this.props; + const { branch, component, fetchMeasures, metrics } = this.props; const { leakPeriod } = this.state; const query = parseQuery(this.props.location.query); const metric = metrics[query.metric]; @@ -159,6 +164,7 @@ export default class App extends React.PureComponent { {metric != null && */ { return component.measures.filter(measure => !bannedMetrics.includes(measure.metric)); } -const fetchMeasures = (component /*: string */, metricsKey /*: Array */) => ( - dispatch, - getState -) => { +const fetchMeasures = ( + component /*: string */, + metricsKey /*: Array */, + branch /*: string | null */ +) => (dispatch, getState) => { if (metricsKey.length <= 0) { return Promise.resolve({ component: {}, measures: [], leakPeriod: null }); } - return getMeasuresAndMeta(component, metricsKey, { additionalFields: 'periods' }).then(r => { + return getMeasuresAndMeta(component, metricsKey, { + additionalFields: 'periods', + branch + }).then(r => { const measures = banQualityGate(r.component).map(measure => enhanceMeasure(measure, getMetrics(getState())) ); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js index 0a5920f55c9..7de031dee8a 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js @@ -22,10 +22,12 @@ import React from 'react'; import key from 'keymaster'; import Breadcrumb from './Breadcrumb'; import { getBreadcrumbs } from '../../../api/components'; +import { getBranchName } from '../../../helpers/branches'; /*:: import type { Component } from '../types'; */ /*:: type Props = {| backToFirst: boolean, + branch: {}, className?: string, component: Component, handleSelect: string => void, @@ -75,7 +77,7 @@ export default class Breadcrumbs extends React.PureComponent { key.unbind('left', 'measures-files'); } - fetchBreadcrumbs = ({ component, rootComponent } /*: Props */) => { + fetchBreadcrumbs = ({ branch, component, rootComponent } /*: Props */) => { const isRoot = component.key === rootComponent.key; if (isRoot) { if (this.mounted) { @@ -83,7 +85,7 @@ export default class Breadcrumbs extends React.PureComponent { } return; } - getBreadcrumbs(component.key).then(breadcrumbs => { + getBreadcrumbs(component.key, getBranchName(branch)).then(breadcrumbs => { if (this.mounted) { this.setState({ breadcrumbs }); } diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js index c5dedfe59b6..64411b028c1 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js @@ -32,6 +32,7 @@ import TreeMapView from '../drilldown/TreeMapView'; import { getComponentTree } from '../../../api/components'; import { complementary } from '../config/complementary'; import { enhanceComponent, isFileType, isViewType } from '../utils'; +import { getBranchName } from '../../../helpers/branches'; import { getProjectUrl } from '../../../helpers/urls'; import { isDiffMetric } from '../../../helpers/measures'; import { parseDate } from '../../../helpers/dates'; @@ -43,6 +44,7 @@ import { parseDate } from '../../../helpers/dates'; // https://github.com/facebook/flow/issues/3147 // router: { push: ({ pathname: string, query?: RawQuery }) => void } /*:: type Props = {| + branch: {}, className?: string, component: Component, currentUser: { isLoggedIn: boolean }, @@ -86,7 +88,11 @@ export default class MeasureContent extends React.PureComponent { } componentWillReceiveProps(nextProps /*: Props */) { - if (nextProps.component !== this.props.component || nextProps.metric !== this.props.metric) { + if ( + nextProps.branch !== this.props.branch || + nextProps.component !== this.props.component || + nextProps.metric !== this.props.metric + ) { this.fetchComponents(nextProps); } } @@ -110,7 +116,10 @@ export default class MeasureContent extends React.PureComponent { ) => { const strategy = view === 'list' ? 'leaves' : 'children'; const metricKeys = [metric.key]; - const opts /*: Object */ = { metricSortFilter: 'withMeasuresOnly' }; + const opts /*: Object */ = { + branch: getBranchName(this.props.branch), + metricSortFilter: 'withMeasuresOnly' + }; const isDiff = isDiffMetric(metric.key); if (isDiff) { opts.metricPeriodSort = 1; @@ -215,7 +224,7 @@ export default class MeasureContent extends React.PureComponent { onSelectComponent = (componentKey /*: string */) => this.setState({ selected: componentKey }); renderCode() { - const { component, leakPeriod } = this.props; + const { branch, component, leakPeriod } = this.props; const leakPeriodDate = isDiffMetric(this.props.metric.key) && leakPeriod != null ? parseDate(leakPeriod.date) : null; @@ -232,7 +241,11 @@ export default class MeasureContent extends React.PureComponent { } return (
    - +
    ); } @@ -244,6 +257,7 @@ export default class MeasureContent extends React.PureComponent { const selectedIdx = this.getSelectedIndex(); return ( + metricsKey: Array, + branch: string | null ) => Promise<{ component: Component, measures: Array }>, leakPeriod?: Period, metric: Metric, @@ -87,7 +90,7 @@ export default class MeasureContentContainer extends React.PureComponent { this.mounted = false; } - fetchMeasure = ({ rootComponent, fetchMeasures, metric, selected } /*: Props */) => { + fetchMeasure = ({ branch, rootComponent, fetchMeasures, metric, selected } /*: Props */) => { this.updateLoading({ measure: true }); const metricKeys = [metric.key]; @@ -99,7 +102,7 @@ export default class MeasureContentContainer extends React.PureComponent { metricKeys.push('file_complexity_distribution'); } - fetchMeasures(selected || rootComponent.key, metricKeys).then( + fetchMeasures(selected || rootComponent.key, metricKeys, getBranchName(branch)).then( ({ component, measures }) => { if (this.mounted) { const measure = measures.find(measure => measure.metric.key === metric.key); @@ -132,6 +135,7 @@ export default class MeasureContentContainer extends React.PureComponent { return ( { - const { component, domain, metrics } = props; + const { branch, component, domain, metrics } = props; if (isFileType(component)) { return this.setState({ components: [], paging: null }); } @@ -88,6 +90,7 @@ export default class MeasureOverview extends React.PureComponent { metricsKey.push(colors.map(metric => metric.key)); } const options = { + branch: getBranchName(branch), s: 'metric', metricSort: size.key, asc: false, @@ -112,11 +115,11 @@ export default class MeasureOverview extends React.PureComponent { }; renderContent() { - const { component } = this.props; + const { branch, component } = this.props; if (isFileType(component)) { return (
    - +
    ); } @@ -133,7 +136,7 @@ export default class MeasureOverview extends React.PureComponent { } render() { - const { component, currentUser, leakPeriod, rootComponent } = this.props; + const { branch, component, currentUser, leakPeriod, rootComponent } = this.props; const isLoggedIn = currentUser && currentUser.isLoggedIn; const isFile = isFileType(component); return ( @@ -143,6 +146,7 @@ export default class MeasureOverview extends React.PureComponent {
    { + fetchComponent = ({ branch, rootComponent, selected } /*: Props */) => { if (!selected || rootComponent.key === selected) { this.setState({ component: rootComponent }); this.updateLoading({ component: false }); return; } this.updateLoading({ component: true }); - getComponentShow(selected).then( + getComponentShow(selected, getBranchName(branch)).then( ({ component }) => { if (this.mounted) { this.setState({ component }); @@ -121,6 +123,7 @@ export default class MeasureOverviewContainer extends React.PureComponent { return ( ({ it('should display correctly for the list view', () => { const wrapper = mount( {}} rootComponent={{ key: 'foo', name: 'Foo' }} @@ -46,6 +47,7 @@ it('should display correctly for the list view', () => { it('should display only the root component', () => { const wrapper = mount( {}} rootComponent={{ key: 'foo', name: 'Foo' }} @@ -58,6 +60,7 @@ it('should display only the root component', () => { it.only('should load the breadcrumb from the api', () => { const wrapper = mount( {}} rootComponent={{ key: 'foo', name: 'Foo' }} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap index fae4fa473aa..e9ff866768e 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap @@ -148,6 +148,7 @@ exports[`should render correctly 1`] = ` Object { "pathname": "/project/activity", "query": Object { + "branch": undefined, "custom_metrics": "reliability_rating", "graph": "custom", "id": "foo", diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.js index d38768555a9..a8f042e7bf0 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.js @@ -20,11 +20,13 @@ // @flow import React from 'react'; import QualifierIcon from '../../../components/icons-components/QualifierIcon'; +import { getBranchName } from '../../../helpers/branches'; import { splitPath } from '../../../helpers/path'; import { getComponentUrl } from '../../../helpers/urls'; /*:: import type { ComponentEnhanced } from '../types'; */ /*:: type Props = { + branch: {}, component: ComponentEnhanced, onClick: string => void }; */ @@ -66,22 +68,22 @@ export default class ComponentCell extends React.PureComponent { } render() { - const { component } = this.props; + const { branch, component } = this.props; return (
    - {component.refId == null + {component.refKey == null ? {this.renderInner()} : + href={getComponentUrl(component.refKey, getBranchName(branch))}> diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.js index 2d28cb63de7..53175f19e06 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.js @@ -27,6 +27,7 @@ import { getLocalizedMetricName } from '../../../helpers/l10n'; /*:: import type { Metric } from '../../../store/metrics/actions'; */ /*:: type Props = {| + branch: {}, components: Array, onClick: string => void, metric: Metric, @@ -35,7 +36,7 @@ import { getLocalizedMetricName } from '../../../helpers/l10n'; |}; */ export default function ComponentsList( - { components, onClick, metrics, metric, selectedComponent } /*: Props */ + { branch, components, onClick, metrics, metric, selectedComponent } /*: Props */ ) { if (!components.length) { return ; @@ -67,6 +68,7 @@ export default function ComponentsList( {components.map(component => void, @@ -34,7 +35,7 @@ import MeasureCell from './MeasureCell'; |}; */ export default function ComponentsListRow(props /*: Props */) { - const { component } = props; + const { branch, component } = props; const otherMeasures = props.otherMetrics.map(metric => { const measure = component.measures.find(measure => measure.metric.key === metric.key); return { ...measure, metric }; @@ -44,7 +45,7 @@ export default function ComponentsListRow(props /*: Props */) { }); return ( - + diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js index b1f8aebad51..6fe0bb181b0 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js @@ -28,6 +28,7 @@ import { scrollToElement } from '../../../helpers/scrolling'; /*:: import type { Metric } from '../../../store/metrics/actions'; */ /*:: type Props = {| + branch: {}, components: Array, fetchMore: () => void, handleSelect: string => void, @@ -117,6 +118,7 @@ export default class ListView extends React.PureComponent { return (
    (this.listContainer = elem)}> , handleSelect: string => void, metric: Metric @@ -62,7 +64,7 @@ export default class TreeMapView extends React.PureComponent { } } - getTreemapComponents = ({ components, metric } /*: Props */) => { + getTreemapComponents = ({ branch, components, metric } /*: Props */) => { const colorScale = this.getColorScale(metric); return components .map(component => { @@ -93,7 +95,7 @@ export default class TreeMapView extends React.PureComponent { sizeValue ), label: component.name, - link: getComponentUrl(component.refKey || component.key) + link: getComponentUrl(component.refKey || component.key, getBranchName(branch)) }; }) .filter(Boolean); diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.js b/server/sonar-web/src/main/js/apps/issues/components/App.js index 509a69dddce..87191bace08 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/App.js +++ b/server/sonar-web/src/main/js/apps/issues/components/App.js @@ -55,6 +55,7 @@ import { } from '../utils'; */ import ListFooter from '../../../components/controls/ListFooter'; import EmptySearch from '../../../components/common/EmptySearch'; +import { getBranchName } from '../../../helpers/branches'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { scrollToElement } from '../../../helpers/scrolling'; /*:: import type { Issue } from '../../../components/issue/types'; */ @@ -173,6 +174,7 @@ export default class App extends React.PureComponent { const { query: prevQuery } = prevProps.location; if ( prevProps.component !== this.props.component || + prevProps.branch !== this.props.branch || !areQueriesEqual(prevQuery, query) || areMyIssuesSelected(prevQuery) !== areMyIssuesSelected(query) ) { @@ -308,7 +310,7 @@ export default class App extends React.PureComponent { pathname: this.props.location.pathname, query: { ...serializeQuery(this.state.query), - branch: this.props.branch && this.props.branch.name, + branch: this.props.branch && getBranchName(this.props.branch), id: this.props.component && this.props.component.key, myIssues: this.state.myIssues ? 'true' : undefined, open: issue @@ -327,7 +329,7 @@ export default class App extends React.PureComponent { pathname: this.props.location.pathname, query: { ...serializeQuery(this.state.query), - branch: this.props.branch && this.props.branch.name, + branch: this.props.branch && getBranchName(this.props.branch), id: this.props.component && this.props.component.key, myIssues: this.state.myIssues ? 'true' : undefined, open: undefined @@ -363,7 +365,7 @@ export default class App extends React.PureComponent { : undefined; const parameters = { - branch: this.props.branch && this.props.branch.name, + branch: this.props.branch && getBranchName(this.props.branch), componentKeys: component && component.key, s: 'FILE_LINE', ...serializeQuery(query), @@ -554,7 +556,7 @@ export default class App extends React.PureComponent { pathname: this.props.location.pathname, query: { ...serializeQuery({ ...this.state.query, ...changes }), - branch: this.props.branch && this.props.branch.name, + branch: this.props.branch && getBranchName(this.props.branch), id: this.props.component && this.props.component.key, myIssues: this.state.myIssues ? 'true' : undefined } @@ -570,7 +572,7 @@ export default class App extends React.PureComponent { pathname: this.props.location.pathname, query: { ...serializeQuery({ ...this.state.query, assigned: true, assignees: [] }), - branch: this.props.branch && this.props.branch.name, + branch: this.props.branch && getBranchName(this.props.branch), id: this.props.component && this.props.component.key, myIssues: myIssues ? 'true' : undefined } @@ -597,7 +599,7 @@ export default class App extends React.PureComponent { pathname: this.props.location.pathname, query: { ...DEFAULT_QUERY, - branch: this.props.branch && this.props.branch.name, + branch: this.props.branch && getBranchName(this.props.branch), id: this.props.component && this.props.component.key, myIssues: this.state.myIssues ? 'true' : undefined } @@ -890,7 +892,7 @@ export default class App extends React.PureComponent {
    {openIssue ? Promise<*>, onIssueChange: Issue => void, @@ -86,7 +86,7 @@ export default class IssuesSourceViewer extends React.PureComponent {
    (this.node = node)}> ; } - return ; + return ( + + ); } } diff --git a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js index 9e28bf48386..907bc8c95c1 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js +++ b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js @@ -36,11 +36,13 @@ import { getLeakPeriod } from '../../../helpers/periods'; import { getCustomGraph, getGraph } from '../../../helpers/storage'; import { METRICS, HISTORY_METRICS_LIST } from '../utils'; import { DEFAULT_GRAPH, getDisplayedHistoryMetrics } from '../../projectActivity/utils'; +import { getBranchName } from '../../../helpers/branches'; /*:: import type { Component, History, MeasuresList, Period } from '../types'; */ import '../styles.css'; /*:: type Props = { + branch: { name: string }, component: Component, onComponentChange: {} => void }; @@ -70,14 +72,12 @@ export default class OverviewApp extends React.PureComponent { if (domElement) { domElement.classList.add('dashboard-page'); } - this.loadMeasures(this.props.component.key).then(() => this.loadHistory(this.props.component)); + this.loadMeasures().then(this.loadHistory); } componentDidUpdate(prevProps /*: Props */) { - if (this.props.component.key !== prevProps.component.key) { - this.loadMeasures(this.props.component.key).then(() => - this.loadHistory(this.props.component) - ); + if (this.props.component.key !== prevProps.component.key || this.props.branch !== prevProps.branch) { + this.loadMeasures().then(this.loadHistory); } } @@ -89,11 +89,13 @@ export default class OverviewApp extends React.PureComponent { } } - loadMeasures(componentKey /*: string */) { + loadMeasures() { + const { branch, component } = this.props; this.setState({ loading: true }); - return getMeasuresAndMeta(componentKey, METRICS, { - additionalFields: 'metrics,periods' + return getMeasuresAndMeta(component.key, METRICS, { + additionalFields: 'metrics,periods', + branch: getBranchName(branch) }).then( r => { if (this.mounted) { @@ -113,14 +115,18 @@ export default class OverviewApp extends React.PureComponent { ); } - loadHistory(component /*: Component */) { + loadHistory = () => { + const { branch, component } = this.props; + let graphMetrics = getDisplayedHistoryMetrics(getGraph(), getCustomGraph()); if (!graphMetrics || graphMetrics.length <= 0) { graphMetrics = getDisplayedHistoryMetrics(DEFAULT_GRAPH, []); } const metrics = uniq(HISTORY_METRICS_LIST.concat(graphMetrics)); - return getAllTimeMachineData(component.key, metrics).then(r => { + return getAllTimeMachineData(component.key, metrics, { + branch: getBranchName(branch) + }).then(r => { if (this.mounted) { const history /*: History */ = {}; r.measures.forEach(measure => { @@ -134,7 +140,7 @@ export default class OverviewApp extends React.PureComponent { this.setState({ history, historyStartDate }); } }, throwGlobalError); - } + }; getApplicationLeakPeriod = () => this.state.measures.find(measure => measure.metric.key === 'new_bugs') ? { index: 1 } : null; @@ -148,7 +154,7 @@ export default class OverviewApp extends React.PureComponent { } render() { - const { component } = this.props; + const { branch, component } = this.props; const { loading, measures, periods, history, historyStartDate } = this.state; if (loading) { @@ -157,7 +163,7 @@ export default class OverviewApp extends React.PureComponent { const leakPeriod = component.qualifier === 'APP' ? this.getApplicationLeakPeriod() : getLeakPeriod(periods); - const domainProps = { component, measures, leakPeriod, history, historyStartDate }; + const domainProps = { branch, component, measures, leakPeriod, history, historyStartDate }; return (
    @@ -165,7 +171,7 @@ export default class OverviewApp extends React.PureComponent {
    {component.qualifier === 'APP' ? - : } + : }
    @@ -177,6 +183,7 @@ export default class OverviewApp extends React.PureComponent {
    { if (this.mounted) { @@ -111,6 +117,7 @@ export default class AnalysesList extends React.PureComponent { - + {translate('show_more')}
    diff --git a/server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js b/server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js index 03c890da665..0c52daceb1a 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js +++ b/server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js @@ -31,6 +31,7 @@ import { hasHistoryDataValue, splitSeriesInGraphs } from '../../projectActivity/utils'; +import { getBranchName } from '../../../helpers/branches'; import { getCustomGraph, getGraph } from '../../../helpers/storage'; import { formatMeasure, getShortType } from '../../../helpers/measures'; import { translate } from '../../../helpers/l10n'; @@ -39,6 +40,7 @@ import { translate } from '../../../helpers/l10n'; /*:: type Props = { + branch: {}, history: ?History, metrics: Array, project: string, @@ -137,7 +139,10 @@ export default class PreviewGraph extends React.PureComponent { }; handleClick = () => { - this.props.router.push({ pathname: '/project/activity', query: { id: this.props.project } }); + this.props.router.push({ + pathname: '/project/activity', + query: { id: this.props.project, branch: getBranchName(this.props.branch) } + }); }; updateTooltip = ( diff --git a/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.js b/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.js index 7311941cc3a..05d2f89d658 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.js +++ b/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.js @@ -26,12 +26,14 @@ import BugIcon from '../../../components/icons-components/BugIcon'; import LeakPeriodLegend from '../components/LeakPeriodLegend'; import VulnerabilityIcon from '../../../components/icons-components/VulnerabilityIcon'; import { getMetricName } from '../helpers/metrics'; +import { getBranchName } from '../../../helpers/branches'; import { getComponentDrilldownUrl } from '../../../helpers/urls'; import { translate } from '../../../helpers/l10n'; class BugsAndVulnerabilities extends React.PureComponent { renderHeader() { - const { component } = this.props; + const { branch, component } = this.props; + const branchName = getBranchName(branch); return (
    @@ -41,7 +43,7 @@ class BugsAndVulnerabilities extends React.PureComponent { + to={getComponentDrilldownUrl(component.key, 'Reliability', branchName)}> @@ -49,7 +51,7 @@ class BugsAndVulnerabilities extends React.PureComponent { + to={getComponentDrilldownUrl(component.key, 'Security', branchName)}>
    diff --git a/server/sonar-web/src/main/js/apps/overview/main/CodeSmells.js b/server/sonar-web/src/main/js/apps/overview/main/CodeSmells.js index 01e98995e21..a9ab208ea17 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/CodeSmells.js +++ b/server/sonar-web/src/main/js/apps/overview/main/CodeSmells.js @@ -28,6 +28,7 @@ import { translate, translateWithParameters } from '../../../helpers/l10n'; import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; import { getComponentIssuesUrl } from '../../../helpers/urls'; import CodeSmellIcon from '../../../components/icons-components/CodeSmellIcon'; +import { getBranchName } from '../../../helpers/branches'; class CodeSmells extends React.PureComponent { renderHeader() { @@ -35,10 +36,15 @@ class CodeSmells extends React.PureComponent { } renderDebt(metric, type) { - const { measures, component } = this.props; + const { branch, measures, component } = this.props; const measure = measures.find(measure => measure.metric.key === metric); const value = this.props.getValue(measure); - const params = { resolved: 'false', facetMode: 'effort', types: type }; + const params = { + branch: getBranchName(branch), + resolved: 'false', + facetMode: 'effort', + types: type + }; if (isDiffMetric(metric)) { Object.assign(params, { sinceLeakPeriod: 'true' }); diff --git a/server/sonar-web/src/main/js/apps/overview/main/Coverage.js b/server/sonar-web/src/main/js/apps/overview/main/Coverage.js index 0fcf8dd1668..90cac0105d4 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/Coverage.js +++ b/server/sonar-web/src/main/js/apps/overview/main/Coverage.js @@ -20,6 +20,7 @@ import React from 'react'; import enhance from './enhance'; import { DrilldownLink } from '../../../components/shared/drilldown-link'; +import { getBranchName } from '../../../helpers/branches'; import { getMetricName } from '../helpers/metrics'; import { formatMeasure, getPeriodValue } from '../../../helpers/measures'; import { translate } from '../../../helpers/l10n'; @@ -55,7 +56,7 @@ class Coverage extends React.PureComponent { } renderCoverage() { - const { component } = this.props; + const { branch, component } = this.props; const metric = 'coverage'; const coverage = this.getCoverage(); @@ -67,7 +68,7 @@ class Coverage extends React.PureComponent {
    - + {formatMeasure(coverage, 'PERCENT')} @@ -84,7 +85,8 @@ class Coverage extends React.PureComponent { } renderNewCoverage() { - const { component, leakPeriod } = this.props; + const { branch, component, leakPeriod } = this.props; + const branchName = getBranchName(branch); const newCoverageMeasure = this.getNewCoverageMeasure(); const newLinesToCover = this.getNewLinesToCover(); @@ -98,7 +100,10 @@ class Coverage extends React.PureComponent { const formattedValue = newCoverageValue != null ?
    - + {formatMeasure(newCoverageValue, 'PERCENT')} @@ -111,6 +116,7 @@ class Coverage extends React.PureComponent { {translate('overview.coverage_on')}
    diff --git a/server/sonar-web/src/main/js/apps/overview/main/Duplications.js b/server/sonar-web/src/main/js/apps/overview/main/Duplications.js index 9d549b18a9f..acad0d75090 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/Duplications.js +++ b/server/sonar-web/src/main/js/apps/overview/main/Duplications.js @@ -20,6 +20,7 @@ import React from 'react'; import enhance from './enhance'; import { DrilldownLink } from '../../../components/shared/drilldown-link'; +import { getBranchName } from '../../../helpers/branches'; import { getMetricName } from '../helpers/metrics'; import { formatMeasure, getPeriodValue } from '../../../helpers/measures'; import { translate } from '../../../helpers/l10n'; @@ -39,7 +40,7 @@ class Duplications extends React.PureComponent { } renderDuplications() { - const { component, measures } = this.props; + const { branch, component, measures } = this.props; const measure = measures.find(measure => measure.metric.key === 'duplicated_lines_density'); const duplications = Number(measure.value); @@ -51,7 +52,10 @@ class Duplications extends React.PureComponent {
    - + {formatMeasure(duplications, 'PERCENT')}
    @@ -66,7 +70,8 @@ class Duplications extends React.PureComponent { } renderNewDuplications() { - const { component, measures, leakPeriod } = this.props; + const { branch, component, measures, leakPeriod } = this.props; + const branchName = getBranchName(branch); const newDuplicationsMeasure = measures.find( measure => measure.metric.key === 'new_duplicated_lines_density' ); @@ -82,7 +87,10 @@ class Duplications extends React.PureComponent { const formattedValue = newDuplicationsValue != null ?
    - + {formatMeasure(newDuplicationsValue, 'PERCENT')} @@ -95,6 +103,7 @@ class Duplications extends React.PureComponent { {translate('overview.duplications_on')}
    diff --git a/server/sonar-web/src/main/js/apps/overview/main/enhance.js b/server/sonar-web/src/main/js/apps/overview/main/enhance.js index 7d0b11432b5..a6ef18f6f8b 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/enhance.js +++ b/server/sonar-web/src/main/js/apps/overview/main/enhance.js @@ -26,6 +26,7 @@ import HistoryIcon from '../../../components/icons-components/HistoryIcon'; import Rating from './../../../components/ui/Rating'; import Timeline from '../components/Timeline'; import Tooltip from '../../../components/controls/Tooltip'; +import { getBranchName } from '../../../helpers/branches'; import { formatMeasure, formatMeasureVariation, @@ -59,7 +60,7 @@ export default function enhance(ComposedComponent) { }; renderHeader = (domain, label) => { - const { component } = this.props; + const { branch, component } = this.props; return (
    @@ -68,7 +69,7 @@ export default function enhance(ComposedComponent) { + to={getComponentDrilldownUrl(component.key, domain, getBranchName(branch))}>
    @@ -77,7 +78,7 @@ export default function enhance(ComposedComponent) { }; renderMeasure = metricKey => { - const { measures, component } = this.props; + const { branch, measures, component } = this.props; const measure = measures.find(measure => measure.metric.key === metricKey); if (measure == null) { @@ -87,7 +88,10 @@ export default function enhance(ComposedComponent) { return (
    - + {formatMeasure(measure.value, getShortType(measure.metric.type))} @@ -125,7 +129,7 @@ export default function enhance(ComposedComponent) { }; renderRating = metricKey => { - const { component, measures } = this.props; + const { branch, component, measures } = this.props; const measure = measures.find(measure => measure.metric.key === metricKey); if (!measure) { return null; @@ -136,6 +140,7 @@ export default function enhance(ComposedComponent) {
    @@ -147,10 +152,11 @@ export default function enhance(ComposedComponent) { }; renderIssues = (metric, type) => { - const { measures, component } = this.props; + const { branch, measures, component } = this.props; const measure = measures.find(measure => measure.metric.key === metric); const value = this.getValue(measure); const params = { + branch: getBranchName(branch), resolved: 'false', types: type }; @@ -182,7 +188,11 @@ export default function enhance(ComposedComponent) { return ( + to={getComponentMeasureHistory( + this.props.component.key, + metricKey, + getBranchName(this.props.branch) + )}> ); diff --git a/server/sonar-web/src/main/js/apps/overview/meta/Meta.js b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js index a79b44dbd41..8bf9ed046ba 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/Meta.js +++ b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js @@ -31,6 +31,7 @@ import MetaTags from './MetaTags'; import { areThereCustomOrganizations } from '../../../store/rootReducer'; const Meta = ({ + branch, component, history, measures, @@ -58,12 +59,13 @@ const Meta = ({ {description}
    } - + {isProject && } {(isProject || isApplication) && - + {formatMeasure(ncloc.value, 'SHORT_INT')}
    @@ -69,7 +74,10 @@ export default class MetaSize extends React.PureComponent { ?
    - + {formatMeasure(projects.value, 'SHORT_INT')}
    diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js index cb5795332f9..bb4e7041003 100644 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js @@ -35,12 +35,13 @@ function isProject(component /*: Component */) { /*:: type Props = { + branch: { name: string }, component: Component, measures: MeasuresList }; */ -export default function QualityGate({ component, measures } /*: Props */) { +export default function QualityGate({ branch, component, measures } /*: Props */) { const statusMeasure = measures.find(measure => measure.metric.key === 'alert_status'); const detailsMeasure = measures.find(measure => measure.metric.key === 'quality_gate_details'); @@ -63,7 +64,7 @@ export default function QualityGate({ component, measures } /*: Props */) { {conditions.length > 0 && - } + }
    ); } diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.js index 66ea85d9289..ddf81063fd6 100644 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.js +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.js @@ -24,6 +24,7 @@ import { Link } from 'react-router'; import { DrilldownLink } from '../../../components/shared/drilldown-link'; import Measure from '../../../components/measure/Measure'; import IssueTypeIcon from '../../../components/ui/IssueTypeIcon'; +import { getBranchName } from '../../../helpers/branches'; import { getPeriodValue, isDiffMetric, formatMeasure } from '../../../helpers/measures'; import { translate } from '../../../helpers/l10n'; import { getComponentIssuesUrl } from '../../../helpers/urls'; @@ -32,6 +33,7 @@ import { getComponentIssuesUrl } from '../../../helpers/urls'; export default class QualityGateCondition extends React.PureComponent { /*:: props: { + branch: { name: string }, component: Component, condition: { level: string, @@ -52,16 +54,17 @@ export default class QualityGateCondition extends React.PureComponent { } } - getIssuesUrl(sinceLeakPeriod /*: boolean */, customQuery /*: {} */) { + getIssuesUrl = (sinceLeakPeriod /*: boolean */, customQuery /*: {} */) => { const query /*: Object */ = { resolved: 'false', + branch: getBranchName(this.props.branch), ...customQuery }; if (sinceLeakPeriod) { Object.assign(query, { sinceLeakPeriod: 'true' }); } return getComponentIssuesUrl(this.props.component.key, query); - } + }; getUrlForCodeSmells(sinceLeakPeriod /*: boolean */) { return this.getIssuesUrl(sinceLeakPeriod, { types: 'CODE_SMELL' }); @@ -91,7 +94,7 @@ export default class QualityGateCondition extends React.PureComponent { } wrapWithLink(children /*: React.Element<*> */) { - const { component, condition } = this.props; + const { branch, component, condition } = this.props; const className = classNames( 'overview-quality-gate-condition', @@ -115,6 +118,7 @@ export default class QualityGateCondition extends React.PureComponent { {children} : c.level !== 'OK'); if (failedConditions.length > 0) { const metrics = failedConditions.map(condition => condition.metric); - getMeasuresAndMeta(component.key, metrics, { additionalFields: 'metrics' }).then(r => { + getMeasuresAndMeta(component.key, metrics, { + additionalFields: 'metrics', + branch: getBranchName(branch) + }).then(r => { if (this.mounted) { const measures = enhanceMeasuresWithMetrics(r.component.measures, r.metrics); this.setState({ @@ -81,7 +87,7 @@ export default class QualityGateConditions extends React.PureComponent { } render() { - const { component } = this.props; + const { branch, component } = this.props; const { loading, conditions } = this.state; if (loading) { @@ -101,6 +107,7 @@ export default class QualityGateConditions extends React.PureComponent { {sortedConditions.map(condition => diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/QualityGateCondition-test.js b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/QualityGateCondition-test.js index 449ceff035b..1eee087a42f 100644 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/QualityGateCondition-test.js +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/QualityGateCondition-test.js @@ -56,7 +56,13 @@ it('open_issues', () => { op: 'GT' }; expect( - shallow() + shallow( + + ) ).toMatchSnapshot(); }); @@ -79,28 +85,52 @@ it('new_open_issues', () => { period: 1 }; expect( - shallow() + shallow( + + ) ).toMatchSnapshot(); }); it('reliability_rating', () => { const condition = mockRatingCondition('reliability_rating'); expect( - shallow() + shallow( + + ) ).toMatchSnapshot(); }); it('security_rating', () => { const condition = mockRatingCondition('security_rating'); expect( - shallow() + shallow( + + ) ).toMatchSnapshot(); }); it('sqale_rating', () => { const condition = mockRatingCondition('sqale_rating'); expect( - shallow() + shallow( + + ) ).toMatchSnapshot(); }); @@ -109,7 +139,13 @@ it('new_reliability_rating', () => { condition.period = 1; condition.measure.periods = periods; expect( - shallow() + shallow( + + ) ).toMatchSnapshot(); }); @@ -118,7 +154,13 @@ it('new_security_rating', () => { condition.period = 1; condition.measure.periods = periods; expect( - shallow() + shallow( + + ) ).toMatchSnapshot(); }); @@ -127,14 +169,24 @@ it('new_maintainability_rating', () => { condition.period = 1; condition.measure.periods = periods; expect( - shallow() + shallow( + + ) ).toMatchSnapshot(); }); it('should be able to correctly decide how much decimals to show', () => { const condition = mockRatingCondition('new_maintainability_rating'); const instance = shallow( - + ).instance(); expect(instance.getDecimalsNumber(85, 80)).toBe(undefined); expect(instance.getDecimalsNumber(85, 85)).toBe(undefined); @@ -144,3 +196,16 @@ it('should be able to correctly decide how much decimals to show', () => { expect(instance.getDecimalsNumber(85, 85.0000000000000954)).toBe('00000000000009'.length); expect(instance.getDecimalsNumber(85, 85.00000000000000009)).toBe(undefined); }); + +it('should work with branch', () => { + const condition = mockRatingCondition('new_maintainability_rating'); + expect( + shallow( + + ) + ).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.js.snap b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.js.snap index c887cc5062e..aac6af124c6 100644 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.js.snap +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.js.snap @@ -9,6 +9,7 @@ exports[`renders 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "foo", }, } diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap index cf6049b83cb..2b4d6d19c62 100644 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap @@ -9,6 +9,7 @@ exports[`new_maintainability_rating 1`] = ` Object { "pathname": "/project/issues", "query": Object { + "branch": undefined, "id": "abcd-key", "resolved": "false", "sinceLeakPeriod": "true", @@ -131,6 +132,7 @@ exports[`new_reliability_rating 1`] = ` Object { "pathname": "/project/issues", "query": Object { + "branch": undefined, "id": "abcd-key", "resolved": "false", "severities": "BLOCKER,CRITICAL,MAJOR,MINOR", @@ -198,6 +200,7 @@ exports[`new_security_rating 1`] = ` Object { "pathname": "/project/issues", "query": Object { + "branch": undefined, "id": "abcd-key", "resolved": "false", "severities": "BLOCKER,CRITICAL,MAJOR,MINOR", @@ -315,6 +318,7 @@ exports[`reliability_rating 1`] = ` Object { "pathname": "/project/issues", "query": Object { + "branch": undefined, "id": "abcd-key", "resolved": "false", "severities": "BLOCKER,CRITICAL,MAJOR,MINOR", @@ -375,6 +379,7 @@ exports[`security_rating 1`] = ` Object { "pathname": "/project/issues", "query": Object { + "branch": undefined, "id": "abcd-key", "resolved": "false", "severities": "BLOCKER,CRITICAL,MAJOR,MINOR", @@ -426,6 +431,67 @@ exports[`security_rating 1`] = ` `; +exports[`should work with branch 1`] = ` + +
    +
    + +
    +
    +
    + + new_maintainability_rating +
    +
    + quality_gates.operator.GT.rating + + A +
    +
    +
    + +`; + exports[`sqale_rating 1`] = ` { - const parameters = { project, p, ps }; + const parameters = { project, p, ps, branch: getBranchName(this.props.branch) }; return api .getProjectActivity({ ...parameters, ...additional }) .then(({ analyses, paging }) => ({ @@ -180,7 +182,9 @@ export default class ProjectActivityAppContainer extends React.PureComponent { if (metrics.length <= 0) { return Promise.resolve([]); } - return getAllTimeMachineData(this.props.component.key, metrics).then( + return getAllTimeMachineData(this.props.component.key, metrics, { + branch: getBranchName(this.props.branch) + }).then( ({ measures }) => measures.map(measure => ({ metric: measure.metric, @@ -281,6 +285,7 @@ export default class ProjectActivityAppContainer extends React.PureComponent { pathname: this.props.location.pathname, query: { ...query, + branch: getBranchName(this.props.branch), id: this.props.component.key } }); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-test.js index 2dcf1dd4d61..850526fe3d9 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-test.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-test.js @@ -63,6 +63,7 @@ const DEFAULT_PROPS = { addVersion: () => {}, analyses: ANALYSES, analysesLoading: false, + branch: { isMain: true }, changeEvent: () => {}, deleteAnalysis: () => {}, deleteEvent: () => {}, diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js index 8c72221dd3c..9f41c7a8a8e 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js @@ -419,7 +419,7 @@ export default class SourceViewerBase extends React.PureComponent { }; loadDuplications = (line /*: SourceLine */) => { - getDuplications(this.props.component).then(r => { + getDuplications(this.props.component, this.props.branch).then(r => { if (this.mounted) { this.setState( { @@ -440,13 +440,22 @@ export default class SourceViewerBase extends React.PureComponent { }; showMeasures = () => { - const measuresOverlay = new MeasuresOverlay({ component: this.state.component, large: true }); + const measuresOverlay = new MeasuresOverlay({ + branch: this.props.branch, + component: this.state.component, + large: true + }); measuresOverlay.render(); }; handleCoverageClick = (line /*: SourceLine */, element /*: HTMLElement */) => { - getTests(this.props.component, line.line).then(tests => { - const popup = new CoveragePopupView({ line, tests, triggerEl: element }); + getTests(this.props.component, line.line, this.props.branch).then(tests => { + const popup = new CoveragePopupView({ + line, + tests, + triggerEl: element, + branch: this.props.branch + }); popup.render(); }); }; @@ -477,7 +486,8 @@ export default class SourceViewerBase extends React.PureComponent { inRemovedComponent, component: this.state.component, files: this.state.duplicatedFiles, - triggerEl: element + triggerEl: element, + branch: this.props.branch }); popup.render(); } @@ -500,7 +510,8 @@ export default class SourceViewerBase extends React.PureComponent { const popup = new LineActionsPopupView({ line, triggerEl: element, - component: this.state.component + component: this.state.component, + branch: this.props.branch }); popup.render(); } @@ -637,7 +648,11 @@ export default class SourceViewerBase extends React.PureComponent { return (
    (this.node = node)}> - + {notAccessible &&
    {translate('code_viewer.no_source_code_displayed_due_to_security')} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js index 9391f3dedf9..991ec78a765 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js @@ -29,6 +29,7 @@ import { formatMeasure } from '../../helpers/measures'; export default class SourceViewerHeader extends React.PureComponent { /*:: props: { + branch?: string, component: { canMarkAsFavorite: boolean, key: string, @@ -60,7 +61,7 @@ export default class SourceViewerHeader extends React.PureComponent { e.preventDefault(); const { key } = this.props.component; const Workspace = require('../workspace/main').default; - Workspace.openComponent({ key }); + Workspace.openComponent({ key, branch: this.props.branch }); }; render() { @@ -78,8 +79,11 @@ export default class SourceViewerHeader extends React.PureComponent { const isUnitTest = q === 'UTS'; // TODO check if source viewer is displayed inside workspace const workspace = false; - const rawSourcesLink = + let rawSourcesLink = window.baseUrl + `/api/sources/raw?key=${encodeURIComponent(this.props.component.key)}`; + if (this.props.branch) { + rawSourcesLink += `&branch=${encodeURIComponent(this.props.branch)}`; + } // TODO favorite return ( @@ -87,14 +91,14 @@ export default class SourceViewerHeader extends React.PureComponent {
    - + {projectName}
    {subProject != null &&
    - + {subProjectName}
    } @@ -124,7 +128,10 @@ export default class SourceViewerHeader extends React.PureComponent { + to={{ + pathname: '/component', + query: { branch: this.props.branch, id: this.props.component.key } + }}> {translate('component_viewer.new_window')}
  • @@ -166,7 +173,11 @@ export default class SourceViewerHeader extends React.PureComponent {
    {measures.issues != null ? formatMeasure(measures.issues, 'SHORT_INT') : 0}{' '} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js index 3206805adc1..4100e0a5439 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js @@ -38,7 +38,7 @@ export default Popup.extend({ e.stopPropagation(); const key = $(e.currentTarget).data('key'); const Workspace = require('../../workspace/main').default; - Workspace.openComponent({ key }); + Workspace.openComponent({ key, branch: this.options.branch }); }, serializeData() { diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js index fec8023fc77..e5fc6ec87f2 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js @@ -34,7 +34,7 @@ export default Popup.extend({ const key = $(e.currentTarget).data('key'); const line = $(e.currentTarget).data('line'); const Workspace = require('../../workspace/main').default; - Workspace.openComponent({ key, line }); + Workspace.openComponent({ key, line, branch: this.options.branch }); }, serializeData() { diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/line-actions-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/popups/line-actions-popup.js index 1f3cc3edeb2..46c9c81428d 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/popups/line-actions-popup.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/popups/line-actions-popup.js @@ -24,9 +24,12 @@ export default Popup.extend({ template: Template, serializeData() { - const { component, line } = this.options; - return { - permalink: window.baseUrl + `/component?id=${encodeURIComponent(component.key)}&line=${line}` - }; + const { component, line, branch } = this.options; + let permalink = + window.baseUrl + `/component?id=${encodeURIComponent(component.key)}&line=${line}`; + if (branch) { + permalink += `&branch=${encodeURIComponent(branch)}`; + } + return { permalink }; } }); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js b/server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js index 23190cd383a..d9d7eb25c31 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js @@ -141,7 +141,11 @@ export default ModalView.extend({ .filter(metric => metric.type !== 'DATA' && !metric.hidden) .map(metric => metric.key); - return getMeasures(this.options.component.key, metricsToRequest).then(measures => { + return getMeasures( + this.options.component.key, + metricsToRequest, + this.options.branch + ).then(measures => { let nextMeasures = this.options.component.measures || {}; measures.forEach(measure => { const metric = metrics.find(metric => metric.key === measure.metric); @@ -160,6 +164,7 @@ export default ModalView.extend({ return new Promise(resolve => { const url = window.baseUrl + '/api/issues/search'; const options = { + branch: this.options.branch, componentKeys: this.options.component.key, resolved: false, ps: 1, @@ -191,7 +196,7 @@ export default ModalView.extend({ requestTests() { return new Promise(resolve => { const url = window.baseUrl + '/api/tests/list'; - const options = { testFileKey: this.options.component.key }; + const options = { branch: this.options.branch, testFileKey: this.options.component.key }; $.get(url, options).done(data => { this.tests = data.tests; diff --git a/server/sonar-web/src/main/js/components/icons-components/BranchIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/BranchIcon.tsx index 420d94ebd6a..a1b1a363b3d 100644 --- a/server/sonar-web/src/main/js/components/icons-components/BranchIcon.tsx +++ b/server/sonar-web/src/main/js/components/icons-components/BranchIcon.tsx @@ -18,28 +18,21 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import ShortLivingBranchIcon from './ShortLivingBranchIcon'; +import LongLivingBranchIcon from './LongLivingBranchIcon'; +// import PullRequestIcon from './PullRequestIcon'; +import { Branch } from '../../app/types'; +import { isShortLivingBranch } from '../../helpers/branches'; interface Props { + branch: Branch; className?: string; color?: string; size?: number; } -export default function BranchIcon({ className, color = '#4b9fd5', size = 14 }: Props) { - /* eslint-disable max-len */ - return ( - - - - - - ); +export default function BranchIcon({ branch, ...props }: Props) { + return isShortLivingBranch(branch) + ? + : ; } diff --git a/server/sonar-web/src/main/js/components/icons-components/LongLivingBranchIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/LongLivingBranchIcon.tsx new file mode 100644 index 00000000000..bba5d0de101 --- /dev/null +++ b/server/sonar-web/src/main/js/components/icons-components/LongLivingBranchIcon.tsx @@ -0,0 +1,45 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; + +interface Props { + className?: string; + color?: string; + size?: number; +} + +export default function LongLivingBranchIcon({ className, color = '#4b9fd5', size = 16 }: Props) { + /* eslint-disable max-len */ + return ( + + + + + + ); +} diff --git a/server/sonar-web/src/main/js/components/icons-components/PullRequestIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/PullRequestIcon.tsx new file mode 100644 index 00000000000..e3432619cb8 --- /dev/null +++ b/server/sonar-web/src/main/js/components/icons-components/PullRequestIcon.tsx @@ -0,0 +1,43 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; + +interface Props { + className?: string; + color?: string; + size?: number; +} + +export default function PullRequestIcon({ className, color = '#4b9fd5', size = 16 }: Props) { + /* eslint-disable max-len */ + return ( + + + + ); +} diff --git a/server/sonar-web/src/main/js/components/icons-components/ShortLivingBranchIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/ShortLivingBranchIcon.tsx new file mode 100644 index 00000000000..f1eaa7a3cfc --- /dev/null +++ b/server/sonar-web/src/main/js/components/icons-components/ShortLivingBranchIcon.tsx @@ -0,0 +1,45 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; + +interface Props { + className?: string; + color?: string; + size?: number; +} + +export default function ShortLivingBranchIcon({ className, color = '#4b9fd5', size = 16 }: Props) { + /* eslint-disable max-len */ + return ( + + + + + + ); +} diff --git a/server/sonar-web/src/main/js/components/shared/drilldown-link.js b/server/sonar-web/src/main/js/components/shared/drilldown-link.js index 13b0afeb0c3..87aadeea190 100644 --- a/server/sonar-web/src/main/js/components/shared/drilldown-link.js +++ b/server/sonar-web/src/main/js/components/shared/drilldown-link.js @@ -49,6 +49,7 @@ const ISSUE_MEASURES = [ export class DrilldownLink extends React.PureComponent { static propTypes = { + branch: PropTypes.string, children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]), className: PropTypes.string, component: PropTypes.string.isRequired, @@ -118,7 +119,10 @@ export class DrilldownLink extends React.PureComponent { }; renderIssuesLink = () => { - const url = getComponentIssuesUrl(this.props.component, this.propsToIssueParams()); + const url = getComponentIssuesUrl(this.props.component, { + ...this.propsToIssueParams(), + branch: this.props.branch + }); return ( @@ -132,7 +136,11 @@ export class DrilldownLink extends React.PureComponent { return this.renderIssuesLink(); } - const url = getComponentDrilldownUrl(this.props.component, this.props.metric); + const url = getComponentDrilldownUrl( + this.props.component, + this.props.metric, + this.props.branch + ); return ( {this.props.children} diff --git a/server/sonar-web/src/main/js/helpers/__tests__/branches-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/branches-test.ts new file mode 100644 index 00000000000..640b5a05d4e --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/__tests__/branches-test.ts @@ -0,0 +1,71 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { sortBranchesAsTree } from '../branches'; +import { MainBranch, BranchType, ShortLivingBranch, LongLivingBranch } from '../../app/types'; + +describe('#sortBranchesAsTree', () => { + it('sorts main branch and short-living branches', () => { + const main = mainBranch(); + const foo = shortLivingBranch('foo', 'master'); + const bar = shortLivingBranch('bar', 'master'); + expect(sortBranchesAsTree([main, foo, bar])).toEqual([main, bar, foo]); + }); + + it('sorts main branch and long-living branches', () => { + const main = mainBranch(); + const foo = longLivingBranch('foo'); + const bar = longLivingBranch('bar'); + expect(sortBranchesAsTree([main, foo, bar])).toEqual([main, bar, foo]); + }); + + it('sorts all types of branches', () => { + const main = mainBranch(); + const shortFoo = shortLivingBranch('shortFoo', 'master'); + const shortBar = shortLivingBranch('shortBar', 'longBaz'); + const shortPre = shortLivingBranch('shortPre', 'shortFoo'); + const longBaz = longLivingBranch('longBaz'); + const longQux = longLivingBranch('longQux'); + const longQwe = longLivingBranch('longQwe'); + // - main - main + // - shortFoo - shortFoo + // - shortPre - shortPre + // - longBaz ----> - longBaz + // - shortBar - shortBar + // - longQwe - longQwe + // - longQux - longQux + expect( + sortBranchesAsTree([main, shortFoo, shortBar, shortPre, longBaz, longQux, longQwe]) + ).toEqual([main, shortFoo, shortPre, longBaz, shortBar, longQux, longQwe]); + }); +}); + +function mainBranch(): MainBranch { + return { isMain: true, name: 'master' }; +} + +function shortLivingBranch(name: string, mergeBranch: string): ShortLivingBranch { + const status = { bugs: 0, codeSmells: 0, vulnerabilities: 0 }; + return { isMain: false, mergeBranch, name, status, type: BranchType.SHORT }; +} + +function longLivingBranch(name: string): LongLivingBranch { + const status = { qualityGateStatus: 'OK' }; + return { isMain: false, name, status, type: BranchType.LONG }; +} diff --git a/server/sonar-web/src/main/js/helpers/branches.ts b/server/sonar-web/src/main/js/helpers/branches.ts index f447020e58d..d9eb71449a9 100644 --- a/server/sonar-web/src/main/js/helpers/branches.ts +++ b/server/sonar-web/src/main/js/helpers/branches.ts @@ -17,20 +17,55 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { Branch, BranchType, ShortLivingBranch } from '../app/types'; +import { sortBy } from 'lodash'; +import { Branch, BranchType, ShortLivingBranch, LongLivingBranch } from '../app/types'; -export const MAIN_BRANCH: Branch = { - isMain: true, - name: undefined, - type: BranchType.LONG -}; +export function isShortLivingBranch(branch: Branch | null): branch is ShortLivingBranch { + return branch != null && !branch.isMain && branch.type === BranchType.SHORT; +} -const MAIN_BRANCH_DISPLAY_NAME = 'master'; +export function isLongLivingBranch(branch: Branch | null): branch is LongLivingBranch { + return branch != null && !branch.isMain && branch.type === BranchType.LONG; +} -export function isShortLivingBranch(branch: Branch | null): branch is ShortLivingBranch { - return branch != null && branch.type === BranchType.SHORT; +export function getBranchName(branch: Branch): string | undefined { + return branch.isMain ? undefined : branch.name; } -export function getBranchDisplayName(branch: Branch): string { - return branch.isMain ? MAIN_BRANCH_DISPLAY_NAME : branch.name; +export function sortBranchesAsTree(branches: Branch[]): Branch[] { + const result: Branch[] = []; + + const shortLivingBranches = branches.filter(isShortLivingBranch); + + // main branch is always first + const mainBranch = branches.find(branch => branch.isMain); + if (mainBranch) { + result.push(mainBranch, ...getNestedShortLivingBranches(mainBranch.name)); + } + + // the all long-living branches + sortBy(branches.filter(isLongLivingBranch), 'name').forEach(longLivingBranch => { + result.push(longLivingBranch, ...getNestedShortLivingBranches(longLivingBranch.name)); + }); + + // finally all orhpan branches + result.push(...shortLivingBranches.filter(branch => branch.isOrphan)); + + return result; + + /** Get all short-living branches (possibly nested) which should be merged to a given branch */ + function getNestedShortLivingBranches(mergeBranch: string): ShortLivingBranch[] { + const found: ShortLivingBranch[] = shortLivingBranches.filter( + branch => branch.mergeBranch === mergeBranch + ); + + let i = 0; + while (i < found.length) { + const current = found[i]; + found.push(...shortLivingBranches.filter(branch => branch.mergeBranch === current.name)); + i++; + } + + return sortBy(found, 'name'); + } } diff --git a/server/sonar-web/src/main/js/helpers/testUtils.ts b/server/sonar-web/src/main/js/helpers/testUtils.ts index 6675a6791b1..507fbc44ffb 100644 --- a/server/sonar-web/src/main/js/helpers/testUtils.ts +++ b/server/sonar-web/src/main/js/helpers/testUtils.ts @@ -63,12 +63,14 @@ export function elementKeydown(element: ShallowWrapper, keyCode: number): void { }); } -export function doAsync(fn: Function): Promise { +export function doAsync(fn?: Function): Promise { return new Promise(resolve => { - setTimeout(() => { - fn(); + setImmediate(() => { + if (fn) { + fn(); + } resolve(); - }, 0); + }); }); } diff --git a/server/sonar-web/src/main/js/helpers/urls.ts b/server/sonar-web/src/main/js/helpers/urls.ts index 4f7f62e72fc..378d89bb0f9 100644 --- a/server/sonar-web/src/main/js/helpers/urls.ts +++ b/server/sonar-web/src/main/js/helpers/urls.ts @@ -23,7 +23,7 @@ import { getProfilePath } from '../apps/quality-profiles/utils'; import { Branch } from '../app/types'; interface Query { - [x: string]: string; + [x: string]: string | undefined; } interface Location { @@ -34,12 +34,15 @@ interface Location { /** * Generate URL for a component's home page */ -export function getComponentUrl(componentKey: string): string { - return (window as any).baseUrl + '/dashboard?id=' + encodeURIComponent(componentKey); +export function getComponentUrl(componentKey: string, branch?: string): string { + const branchQuery = branch ? `&branch=${encodeURIComponent(branch)}` : ''; + return ( + (window as any).baseUrl + '/dashboard?id=' + encodeURIComponent(componentKey) + branchQuery + ); } -export function getProjectUrl(key: string): Location { - return { pathname: '/dashboard', query: { id: key } }; +export function getProjectUrl(key: string, branch?: string): Location { + return { pathname: '/dashboard', query: { id: key, branch } }; } export function getProjectBranchUrl(key: string, branch: Branch) { @@ -48,6 +51,8 @@ export function getProjectBranchUrl(key: string, branch: Branch) { pathname: '/project/issues', query: { branch: branch.name, id: key, resolved: 'false' } }; + } else if (!branch.isMain) { + return { pathname: '/dashboard', query: { branch: branch.name, id: key } }; } else { return { pathname: '/dashboard', query: { id: key } }; } @@ -75,17 +80,21 @@ export function getComponentIssuesUrlAsString(componentKey: string, query?: Quer /** * Generate URL for a component's drilldown page */ -export function getComponentDrilldownUrl(componentKey: string, metric: string): Location { - return { pathname: '/component_measures', query: { id: componentKey, metric } }; +export function getComponentDrilldownUrl(componentKey: string, metric: string, branch?: string) { + return { pathname: '/component_measures', query: { id: componentKey, metric, branch } }; } /** * Generate URL for a component's measure history */ -export function getComponentMeasureHistory(componentKey: string, metric: string): Location { +export function getComponentMeasureHistory( + componentKey: string, + metric: string, + branch?: string +): Location { return { pathname: '/project/activity', - query: { id: componentKey, graph: 'custom', custom_metrics: metric } + query: { id: componentKey, graph: 'custom', custom_metrics: metric, branch } }; } -- cgit v1.2.3