diff options
34 files changed, 220 insertions, 63 deletions
diff --git a/server/sonar-web/src/main/js/api/application.ts b/server/sonar-web/src/main/js/api/application.ts index 5821d5705be..6fb3876a486 100644 --- a/server/sonar-web/src/main/js/api/application.ts +++ b/server/sonar-web/src/main/js/api/application.ts @@ -26,6 +26,12 @@ export interface ApplicationLeak { projectName: string; } -export function getApplicationLeak(application: string): Promise<Array<ApplicationLeak>> { - return getJSON('/api/applications/show_leak', { application }).then(r => r.leaks, throwGlobalError); +export function getApplicationLeak( + application: string, + branch?: string +): Promise<Array<ApplicationLeak>> { + return getJSON('/api/applications/show_leak', { application, branch }).then( + r => r.leaks, + throwGlobalError + ); } diff --git a/server/sonar-web/src/main/js/api/quality-gates.ts b/server/sonar-web/src/main/js/api/quality-gates.ts index d2f212bd457..af49578283e 100644 --- a/server/sonar-web/src/main/js/api/quality-gates.ts +++ b/server/sonar-web/src/main/js/api/quality-gates.ts @@ -160,6 +160,7 @@ export interface ApplicationQualityGate { export function getApplicationQualityGate(data: { application: string; + branch?: string; organization?: string; }): Promise<ApplicationQualityGate> { return getJSON('/api/qualitygates/application_status', data).catch(throwGlobalError); 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 8cba10a5dbc..d02389be63d 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 @@ -20,6 +20,7 @@ import * as React from 'react'; import * as PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; +import { Link } from 'react-router'; import ComponentNavBranchesMenu from './ComponentNavBranchesMenu'; import DocTooltip from '../../../../components/docs/DocTooltip'; import { BranchLike, Component } from '../../../types'; @@ -53,7 +54,8 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State mounted = false; static contextTypes = { - branchesEnabled: PropTypes.bool.isRequired + branchesEnabled: PropTypes.bool.isRequired, + canAdmin: PropTypes.bool.isRequired }; state: State = { @@ -125,17 +127,34 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State } }; + renderOverlay = () => { + const adminLink = { + pathname: '/project/admin/extension/governance/console', + query: { id: this.props.component.breadcrumbs[0].key, qualifier: 'APP' } + }; + return ( + <> + <p>{translate('application.branches.help')}</p> + <hr className="spacer-top spacer-bottom" /> + <Link className="spacer-left link-no-underline" to={adminLink}> + {translate('application.branches.link')} + </Link> + </> + ); + }; + render() { const { branchLikes, currentBranchLike } = this.props; - const { configuration } = this.props.component; + const { configuration, breadcrumbs } = this.props.component; if (isSonarCloud() && !this.context.branchesEnabled) { return null; } const displayName = getBranchLikeDisplayName(currentBranchLike); + const isApp = breadcrumbs && breadcrumbs[0] && breadcrumbs[0].qualifier === 'APP'; - if (!this.context.branchesEnabled) { + if (isApp && branchLikes.length < 2) { return ( <div className="navbar-context-branches"> <BranchIcon @@ -144,23 +163,42 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State fill={theme.gray80} /> <span className="note">{displayName}</span> - <DocTooltip className="spacer-left" doc="branches/no-branch-support"> - <PlusCircleIcon fill={theme.gray71} size={12} /> - </DocTooltip> - </div> - ); - } - - if (branchLikes.length < 2) { - return ( - <div className="navbar-context-branches"> - <BranchIcon branchLike={currentBranchLike} className="little-spacer-right" /> - <span className="note">{displayName}</span> - <DocTooltip className="spacer-left" doc="branches/single-branch"> - <PlusCircleIcon fill={theme.blue} size={12} /> - </DocTooltip> + {configuration && + configuration.showSettings && ( + <HelpTooltip className="spacer-left" overlay={this.renderOverlay()}> + <PlusCircleIcon className="vertical-middle" fill={theme.blue} size={12} /> + </HelpTooltip> + )} </div> ); + } else { + if (!this.context.branchesEnabled) { + return ( + <div className="navbar-context-branches"> + <BranchIcon + branchLike={currentBranchLike} + className="little-spacer-right" + fill={theme.gray80} + /> + <span className="note">{displayName}</span> + <DocTooltip className="spacer-left" doc="branches/no-branch-support"> + <PlusCircleIcon fill={theme.gray71} size={12} /> + </DocTooltip> + </div> + ); + } + + if (branchLikes.length < 2) { + return ( + <div className="navbar-context-branches"> + <BranchIcon branchLike={currentBranchLike} className="little-spacer-right" /> + <span className="note">{displayName}</span> + <DocTooltip className="spacer-left" doc="branches/single-branch"> + <PlusCircleIcon fill={theme.blue} size={12} /> + </DocTooltip> + </div> + ); + } } return ( 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 1f3045cfc52..53009cc4d40 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 @@ -400,9 +400,10 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { renderExtension = ({ key, name }: Extension, isAdmin: boolean) => { const pathname = isAdmin ? `/project/admin/extension/${key}` : `/project/extension/${key}`; + const query = { id: this.props.component.key, qualifier: this.props.component.qualifier }; return ( <li key={key}> - <Link to={{ pathname, query: { id: this.props.component.key } }} activeClassName="active"> + <Link activeClassName="active" to={{ pathname, query }}> {name} </Link> </li> 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 6b52b5eefbf..7b2f2e2c221 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 @@ -111,7 +111,8 @@ function getCurrentPage(component: Component, branchLike: BranchLike | undefined if (component.qualifier === 'VW' || component.qualifier === 'SVW') { currentPage = { type: HomePageType.Portfolio, component: component.key }; } else if (component.qualifier === 'APP') { - currentPage = { type: HomePageType.Application, component: component.key }; + const branch = isLongLivingBranch(branchLike) ? branchLike.name : undefined; + currentPage = { type: HomePageType.Application, component: component.key, branch }; } else if (component.qualifier === 'TRK') { // when home page is set to the default branch of a project, its name is returned as `undefined` const branch = isLongLivingBranch(branchLike) ? branchLike.name : undefined; 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 9a805b74c82..3fd20dbc41e 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 @@ -49,7 +49,7 @@ it('renders main branch', () => { component={component} currentBranchLike={mainBranch} />, - { context: { branchesEnabled: true } } + { context: { branchesEnabled: true, canAdmin: true } } ) ).toMatchSnapshot(); }); @@ -70,7 +70,7 @@ it('renders short-living branch', () => { component={component} currentBranchLike={branch} />, - { context: { branchesEnabled: true } } + { context: { branchesEnabled: true, canAdmin: true } } ) ).toMatchSnapshot(); }); @@ -91,7 +91,7 @@ it('renders pull request', () => { component={component} currentBranchLike={pullRequest} />, - { context: { branchesEnabled: true } } + { context: { branchesEnabled: true, canAdmin: true } } ) ).toMatchSnapshot(); }); @@ -104,7 +104,7 @@ it('opens menu', () => { component={component} currentBranchLike={mainBranch} />, - { context: { branchesEnabled: true } } + { context: { branchesEnabled: true, canAdmin: true } } ); expect(wrapper.find('Toggler').prop('open')).toBe(false); click(wrapper.find('a')); @@ -119,7 +119,7 @@ it('renders single branch popup', () => { component={component} currentBranchLike={mainBranch} />, - { context: { branchesEnabled: true } } + { context: { branchesEnabled: true, canAdmin: true } } ); expect(wrapper.find('DocTooltip')).toMatchSnapshot(); }); @@ -132,7 +132,7 @@ it('renders no branch support popup', () => { component={component} currentBranchLike={mainBranch} />, - { context: { branchesEnabled: false } } + { context: { branchesEnabled: false, canAdmin: true } } ); expect(wrapper.find('DocTooltip')).toMatchSnapshot(); }); @@ -146,7 +146,7 @@ it('renders nothing on SonarCloud without branch support', () => { component={component} currentBranchLike={mainBranch} />, - { context: { branchesEnabled: false, onSonarCloud: true } } + { context: { branchesEnabled: false, onSonarCloud: true, canAdmin: true } } ); expect(wrapper.type()).toBeNull(); }); 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 b4c9f37a5d3..25528a5523c 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 @@ -13,6 +13,7 @@ exports[`renders main branch 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "component", }, } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavHeader-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavHeader-test.tsx.snap index 4d85fb56b11..b8498689bda 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavHeader-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavHeader-test.tsx.snap @@ -23,6 +23,7 @@ exports[`should not render breadcrumbs with one element 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "my-project", }, } @@ -90,6 +91,7 @@ exports[`should render organization 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "my-project", }, } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap index 3abe30c25f4..a62c3ce9e0e 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap @@ -873,6 +873,7 @@ exports[`should work with extensions 1`] = ` "pathname": "/project/extension/component-foo", "query": Object { "id": "foo", + "qualifier": "TRK", }, } } @@ -954,6 +955,7 @@ exports[`should work with extensions 2`] = ` "pathname": "/project/admin/extension/foo", "query": Object { "id": "foo", + "qualifier": "TRK", }, } } @@ -1001,6 +1003,7 @@ exports[`should work with multiple extensions 1`] = ` "pathname": "/project/extension/component-foo", "query": Object { "id": "foo", + "qualifier": "TRK", }, } } @@ -1018,6 +1021,7 @@ exports[`should work with multiple extensions 1`] = ` "pathname": "/project/extension/component-bar", "query": Object { "id": "foo", + "qualifier": "TRK", }, } } @@ -1099,6 +1103,7 @@ exports[`should work with multiple extensions 2`] = ` "pathname": "/project/admin/extension/foo", "query": Object { "id": "foo", + "qualifier": "TRK", }, } } @@ -1116,6 +1121,7 @@ exports[`should work with multiple extensions 2`] = ` "pathname": "/project/admin/extension/bar", "query": Object { "id": "foo", + "qualifier": "TRK", }, } } 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 4ad1c1e2e2d..0a9d6781bf0 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 @@ -21,6 +21,7 @@ exports[`renders favorite 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "foo", }, } @@ -69,6 +70,7 @@ exports[`renders match 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "foo", }, } @@ -116,6 +118,7 @@ exports[`renders organizations 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "foo", }, } @@ -168,6 +171,7 @@ exports[`renders organizations 2`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "foo", }, } @@ -215,6 +219,7 @@ exports[`renders projects 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "qwe", }, } @@ -267,6 +272,7 @@ exports[`renders recently browsed 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "foo", }, } @@ -314,6 +320,7 @@ exports[`renders selected 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "foo", }, } @@ -359,6 +366,7 @@ exports[`renders selected 2`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "foo", }, } diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts index 11e29502c06..75ac42d3237 100644 --- a/server/sonar-web/src/main/js/app/types.ts +++ b/server/sonar-web/src/main/js/app/types.ts @@ -169,7 +169,7 @@ export interface Group { } export type HomePage = - | { type: HomePageType.Application; component: string } + | { type: HomePageType.Application; branch: string | undefined; component: string } | { type: HomePageType.Issues } | { type: HomePageType.MyIssues } | { type: HomePageType.MyProjects } @@ -220,6 +220,7 @@ export interface Issue { assigneeLogin?: string; assigneeName?: string; author?: string; + branch?: string; comments?: IssueComment[]; component: string; componentLongName: string; @@ -237,6 +238,7 @@ export interface Issue { projectName: string; projectOrganization: string; projectUuid: string; + pullRequest?: string; resolution?: string; rule: string; ruleName: string; diff --git a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.tsx.snap b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.tsx.snap index 90b25b16cf1..11b25e6dd9d 100644 --- a/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.tsx.snap @@ -25,6 +25,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/background-tasks/components/__tests__/__snapshots__/TaskComponent-test.tsx.snap b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskComponent-test.tsx.snap index 88328a93b61..b054cb915e8 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskComponent-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskComponent-test.tsx.snap @@ -20,6 +20,7 @@ exports[`renders correctly 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "foo", }, } 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 index bc900211a0e..bb3cbe506a2 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx @@ -42,5 +42,5 @@ export default function ComponentMeasure({ component, metricKey, metricType }: P return <span />; } - return <Measure value={measure.value} metricKey={finalMetricKey} metricType={finalMetricType} />; + return <Measure metricKey={finalMetricKey} metricType={finalMetricType} value={measure.value} />; } 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 index 57473532b8a..391c1862c04 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx @@ -25,6 +25,8 @@ import * as theme from '../../../app/theme'; import { BranchLike } from '../../../app/types'; import QualifierIcon from '../../../components/icons-components/QualifierIcon'; import { getBranchLikeQuery } from '../../../helpers/branches'; +import LongLivingBranchIcon from '../../../components/icons-components/LongLivingBranchIcon'; +import { translate } from '../../../helpers/l10n'; function getTooltip(component: Component) { const isFile = component.qualifier === 'FIL' || component.qualifier === 'UTS'; @@ -77,10 +79,11 @@ export default function ComponentName(props: Props) { let inner = null; if (component.refKey && component.qualifier !== 'SVW') { + const branch = rootComponent.qualifier === 'APP' ? { branch: component.branch } : {}; inner = ( <Link - to={{ pathname: '/dashboard', query: { id: component.refKey } }} - className="link-with-icon"> + className="link-with-icon" + to={{ pathname: '/dashboard', query: { id: component.refKey, ...branch } }}> <QualifierIcon qualifier={component.qualifier} /> <span>{name}</span> </Link> ); @@ -90,7 +93,7 @@ export default function ComponentName(props: Props) { Object.assign(query, { selected: component.key }); } inner = ( - <Link to={{ pathname: '/code', query }} className="link-with-icon"> + <Link className="link-with-icon" to={{ pathname: '/code', query }}> <QualifierIcon qualifier={component.qualifier} /> <span>{name}</span> </Link> ); @@ -102,5 +105,21 @@ export default function ComponentName(props: Props) { ); } + if (rootComponent.qualifier === 'APP') { + inner = ( + <> + {inner} + {component.branch ? ( + <> + <LongLivingBranchIcon className="spacer-left little-spacer-right" /> + <span className="note">{component.branch}</span> + </> + ) : ( + <span className="spacer-left outline-badge">{translate('branches.main_branch')}</span> + )} + </> + ); + } + return <Truncated title={getTooltip(component)}>{inner}</Truncated>; } diff --git a/server/sonar-web/src/main/js/apps/code/types.ts b/server/sonar-web/src/main/js/apps/code/types.ts index 3a226f8127d..a0b6459a3d3 100644 --- a/server/sonar-web/src/main/js/apps/code/types.ts +++ b/server/sonar-web/src/main/js/apps/code/types.ts @@ -21,6 +21,7 @@ import { Measure } from '../../helpers/measures'; export interface Component extends Breadcrumb { + branch?: string; measures?: Measure[]; path?: string; refKey?: string; 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 9b2ce2e9b6e..fc268fa3425 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 @@ -22,12 +22,15 @@ import React from 'react'; import { Link } from 'react-router'; import LinkIcon from '../../../components/icons-components/LinkIcon'; import QualifierIcon from '../../../components/icons-components/QualifierIcon'; +import LongLivingBranchIcon from '../../../components/icons-components/LongLivingBranchIcon'; import { splitPath } from '../../../helpers/path'; import { getPathUrlAsString, getBranchLikeUrl, + getLongLivingBranchUrl, getComponentDrilldownUrlWithSelection } from '../../../helpers/urls'; +import { translate } from '../../../helpers/l10n'; /*:: import type { Component, ComponentEnhanced } from '../types'; */ /*:: import type { Metric } from '../../../store/metrics/actions'; */ @@ -56,23 +59,44 @@ export default class ComponentCell extends React.PureComponent { const { component } = this.props; let head = ''; let tail = component.name; + let branch = null; if (['DIR', 'FIL', 'UTS'].includes(component.qualifier)) { const parts = splitPath(component.path); ({ head, tail } = parts); } + + if (this.props.rootComponent.qualifier === 'APP') { + branch = ( + <React.Fragment> + {component.branch ? ( + <React.Fragment> + <LongLivingBranchIcon className="spacer-left little-spacer-right" /> + <span className="note">{component.branch}</span> + </React.Fragment> + ) : ( + <span className="spacer-left outline-badge">{translate('branches.main_branch')}</span> + )} + </React.Fragment> + ); + } return ( <span title={component.refKey || component.key}> <QualifierIcon qualifier={component.qualifier} /> {head.length > 0 && <span className="note">{head}/</span>} <span>{tail}</span> + {branch} </span> ); } render() { const { branchLike, component, metric, rootComponent } = this.props; + const to = + this.props.rootComponent.qualifier === 'APP' + ? getLongLivingBranchUrl(component.refKey, component.branch) + : getBranchLikeUrl(component.refKey, branchLike); return ( <td className="measure-details-component-cell"> <div className="text-ellipsis"> @@ -95,7 +119,7 @@ export default class ComponentCell extends React.PureComponent { <Link className="link-no-underline" id={'component-measures-component-link-' + component.key} - to={getBranchLikeUrl(component.refKey, branchLike)}> + to={to}> <span className="big-spacer-right"> <LinkIcon /> </span> diff --git a/server/sonar-web/src/main/js/apps/component/components/App.tsx b/server/sonar-web/src/main/js/apps/component/components/App.tsx index 71fc859ea2f..d70fc1c92e6 100644 --- a/server/sonar-web/src/main/js/apps/component/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/component/components/App.tsx @@ -18,8 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { PullRequest, BranchType, ShortLivingBranch } from '../../../app/types'; import SourceViewer from '../../../components/SourceViewer/SourceViewer'; +import { fillBranchLike } from '../../../helpers/branches'; interface Props { location: { @@ -54,17 +54,7 @@ export default class App extends React.PureComponent<Props> { // TODO find a way to avoid creating this fakeBranchLike // probably the best way would be to drop this page completely // and redirect to the Code page - let fakeBranchLike: ShortLivingBranch | PullRequest | undefined = undefined; - if (branch) { - fakeBranchLike = { - isMain: false, - mergeBranch: '', - name: branch, - type: BranchType.SHORT - } as ShortLivingBranch; - } else if (pullRequest) { - fakeBranchLike = { base: '', branch: '', key: pullRequest, title: '' } as PullRequest; - } + const fakeBranchLike = fillBranchLike(branch, pullRequest); return ( <div className="page page-limited"> diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.tsx b/server/sonar-web/src/main/js/apps/issues/components/App.tsx index 2ae38d862de..4e22af0bfd3 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/App.tsx @@ -62,7 +62,8 @@ import { isShortLivingBranch, isSameBranchLike, getBranchLikeQuery, - isPullRequest + isPullRequest, + fillBranchLike } from '../../../helpers/branches'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { RawQuery } from '../../../helpers/query'; @@ -1046,7 +1047,7 @@ export default class App extends React.PureComponent<Props, State> { <div> {openIssue ? ( <IssuesSourceViewer - branchLike={this.props.branchLike} + branchLike={fillBranchLike(openIssue.branch, openIssue.pullRequest)} loadIssues={this.fetchIssuesForComponent} locationsNavigator={this.state.locationsNavigator} onIssueChange={this.handleIssueChange} diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap index 522da78f5a8..c0182df2b97 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap @@ -20,6 +20,7 @@ exports[`renders 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "proj", }, } @@ -155,6 +156,7 @@ exports[`renders with sub-project 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "proj", }, } @@ -177,6 +179,7 @@ exports[`renders with sub-project 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "sub-proj", }, } diff --git a/server/sonar-web/src/main/js/apps/overview/components/ApplicationLeakPeriodLegend.tsx b/server/sonar-web/src/main/js/apps/overview/components/ApplicationLeakPeriodLegend.tsx index eb230682fcb..0ac7464bcb3 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/ApplicationLeakPeriodLegend.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/ApplicationLeakPeriodLegend.tsx @@ -25,9 +25,11 @@ import DateTooltipFormatter from '../../../components/intl/DateTooltipFormatter' import { getApplicationLeak } from '../../../api/application'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import DateFromNow from '../../../components/intl/DateFromNow'; +import { LightComponent, LongLivingBranch } from '../../../app/types'; interface Props { - component: string; + branch?: LongLivingBranch; + component: LightComponent; } interface State { @@ -44,7 +46,7 @@ export default class ApplicationLeakPeriodLegend extends React.Component<Props, } componentWillReceiveProps(nextProps: Props) { - if (nextProps.component !== this.props.component) { + if (nextProps.component.key !== this.props.component.key) { this.setState({ leaks: undefined }); } } @@ -55,7 +57,10 @@ export default class ApplicationLeakPeriodLegend extends React.Component<Props, fetchLeaks = () => { if (!this.state.leaks) { - getApplicationLeak(this.props.component).then( + getApplicationLeak( + this.props.component.key, + this.props.branch ? this.props.branch.name : undefined + ).then( leaks => { if (this.mounted) { this.setState({ diff --git a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx index 68a7d1bbb1d..df32445c412 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx @@ -43,7 +43,11 @@ import { PROJECT_ACTIVITY_GRAPH, PROJECT_ACTIVITY_GRAPH_CUSTOM } from '../../projectActivity/utils'; -import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches'; +import { + isSameBranchLike, + getBranchLikeQuery, + isLongLivingBranch +} from '../../../helpers/branches'; import { fetchMetrics } from '../../../store/rootActions'; import { getMetrics } from '../../../store/rootReducer'; import { BranchLike, Component, Metric } from '../../../app/types'; @@ -213,7 +217,10 @@ export class OverviewApp extends React.PureComponent<Props, State> { return ( <div className="overview-main page-main"> {component.qualifier === 'APP' ? ( - <ApplicationQualityGate component={component} /> + <ApplicationQualityGate + branch={isLongLivingBranch(branchLike) ? branchLike : undefined} + component={component} + /> ) : ( <QualityGate branchLike={branchLike} component={component} measures={measures} /> )} diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/ApplicationLeakPeriodLegend-test.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/ApplicationLeakPeriodLegend-test.tsx index 27a71ac1851..1dba9c67b4b 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/ApplicationLeakPeriodLegend-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/ApplicationLeakPeriodLegend-test.tsx @@ -32,7 +32,11 @@ jest.mock('../../../../api/application', () => ({ })); it('renders', async () => { - const wrapper = shallow(<ApplicationLeakPeriodLegend component="foo" />); + const wrapper = shallow( + <ApplicationLeakPeriodLegend + component={{ key: 'foo', organization: 'bar', qualifier: 'APP' }} + /> + ); expect(wrapper).toMatchSnapshot(); await waitAndUpdate(wrapper); diff --git a/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.tsx b/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.tsx index 3ce147ba718..4715c66a2ff 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.tsx +++ b/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.tsx @@ -28,11 +28,11 @@ import VulnerabilityIcon from '../../../components/icons-components/Vulnerabilit import { getMetricName } from '../helpers/metrics'; import { getComponentDrilldownUrl } from '../../../helpers/urls'; import { translate } from '../../../helpers/l10n'; +import { isLongLivingBranch } from '../../../helpers/branches'; export class BugsAndVulnerabilities extends React.PureComponent<ComposedProps> { renderHeader() { const { branchLike, component } = this.props; - return ( <div className="overview-card-header"> <div className="overview-title"> @@ -62,7 +62,7 @@ export class BugsAndVulnerabilities extends React.PureComponent<ComposedProps> { } renderLeak() { - const { component, leakPeriod } = this.props; + const { branchLike, component, leakPeriod } = this.props; if (!leakPeriod) { return null; } @@ -70,7 +70,10 @@ export class BugsAndVulnerabilities extends React.PureComponent<ComposedProps> { return ( <div className="overview-domain-leak"> {component.qualifier === 'APP' ? ( - <ApplicationLeakPeriodLegend component={component.key} /> + <ApplicationLeakPeriodLegend + branch={isLongLivingBranch(branchLike) ? branchLike : undefined} + component={component} + /> ) : ( <LeakPeriodLegend period={leakPeriod} /> )} diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.tsx b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.tsx index d50bc8a42f1..e4771d74e76 100644 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.tsx +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.tsx @@ -23,10 +23,11 @@ import ApplicationQualityGateProject from './ApplicationQualityGateProject'; import Level from '../../../components/ui/Level'; import { getApplicationQualityGate, ApplicationProject } from '../../../api/quality-gates'; import { translate } from '../../../helpers/l10n'; -import { LightComponent, Metric } from '../../../app/types'; +import { LightComponent, Metric, LongLivingBranch } from '../../../app/types'; import DocTooltip from '../../../components/docs/DocTooltip'; interface Props { + branch?: LongLivingBranch; component: LightComponent; } @@ -57,10 +58,11 @@ export default class ApplicationQualityGate extends React.PureComponent<Props, S } fetchDetails = () => { - const { component } = this.props; + const { branch, component } = this.props; this.setState({ loading: true }); getApplicationQualityGate({ application: component.key, + branch: branch ? branch.name : undefined, organization: component.organization }).then( ({ status, projects, metrics }) => { diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.tsx.snap index b4c85030625..8af25b25049 100644 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.tsx.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/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap index ceb66108e3c..736ab248060 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap @@ -53,6 +53,7 @@ exports[`renders 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "foo", }, } @@ -140,6 +141,7 @@ exports[`renders 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "barbar", }, } @@ -227,6 +229,7 @@ exports[`renders 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "bazbaz", }, } diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap index c1aac43c7d7..38408926a5b 100644 --- a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap @@ -18,6 +18,7 @@ exports[`renders 1`] = ` "link": Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "foo", }, }, diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap index 0c07a84920e..fd133225a39 100644 --- a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap @@ -18,6 +18,7 @@ exports[`renders 1`] = ` "link": Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "foo", }, }, diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap index 98ca96135f8..aead0a2ce17 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap @@ -352,6 +352,7 @@ exports[`creates project 4`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "name", }, } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap index b053a1bf9af..5552a961caa 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap @@ -55,6 +55,7 @@ exports[`should render OK test 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "project:src/file.js", }, } diff --git a/server/sonar-web/src/main/js/helpers/branches.ts b/server/sonar-web/src/main/js/helpers/branches.ts index 6b4507eddcd..86dc7e10671 100644 --- a/server/sonar-web/src/main/js/helpers/branches.ts +++ b/server/sonar-web/src/main/js/helpers/branches.ts @@ -168,3 +168,21 @@ export function getBranchLikeQuery(branchLike?: BranchLike): BranchParameters { return {}; } } + +// Create branch object from branch name or pull request key +export function fillBranchLike( + branch?: string, + pullRequest?: string +): ShortLivingBranch | PullRequest | undefined { + if (branch) { + return { + isMain: false, + mergeBranch: '', + name: branch, + type: BranchType.SHORT + } as ShortLivingBranch; + } else if (pullRequest) { + return { base: '', branch: '', key: pullRequest, title: '' } as PullRequest; + } + return undefined; +} diff --git a/server/sonar-web/src/main/js/helpers/urls.ts b/server/sonar-web/src/main/js/helpers/urls.ts index 93e531c52df..4d17bde0b09 100644 --- a/server/sonar-web/src/main/js/helpers/urls.ts +++ b/server/sonar-web/src/main/js/helpers/urls.ts @@ -53,8 +53,8 @@ export function getSonarCloudUrlAsString(location: Location) { return 'https://sonarcloud.io' + getPathUrlAsString(location); } -export function getProjectUrl(project: string): Location { - return { pathname: '/dashboard', query: { id: project } }; +export function getProjectUrl(project: string, branch?: string): Location { + return { pathname: '/dashboard', query: { id: project, branch } }; } export function getPortfolioUrl(key: string): Location { @@ -231,7 +231,9 @@ export function getOrganizationUrl(organization: string) { export function getHomePageUrl(homepage: HomePage) { switch (homepage.type) { case HomePageType.Application: - return getProjectUrl(homepage.component); + return homepage.branch + ? getProjectUrl(homepage.component, homepage.branch) + : getProjectUrl(homepage.component); case HomePageType.Project: return homepage.branch ? getLongLivingBranchUrl(homepage.component, homepage.branch) diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 65e77118171..79d87fab775 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -498,6 +498,8 @@ deletion.page=Deletion project_deletion.page.description=Delete this project. The operation cannot be undone. portfolio_deletion.page.description=This portfolio and its sub-portfolios will be deleted. If this portfolio is referenced by other entities, it will be removed from them. Independent entities referenced by this portfolio, such as projects and other top-level portfolios will not be deleted. This operation cannot be undone. application_deletion.page.description=Delete this application. Application projects will not be deleted. Projects referenced by this application will not be deleted. This operation cannot be undone. +application.branches.help=Easily create Application branches composed of the branches of projects in your application. +application.branches.link=Create Branch project_branches.page=Branches & Pull Requests project_branches.page.description=Use this page to manage project branches and pull requests. project_branches.page.life_time=Short-lived branches and pull requests are permanently deleted after {days} days without analysis. |