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
+ );
}
export function getApplicationQualityGate(data: {
application: string;
+ branch?: string;
organization?: string;
}): Promise<ApplicationQualityGate> {
return getJSON('/api/qualitygates/application_status', data).catch(throwGlobalError);
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';
mounted = false;
static contextTypes = {
- branchesEnabled: PropTypes.bool.isRequired
+ branchesEnabled: PropTypes.bool.isRequired,
+ canAdmin: PropTypes.bool.isRequired
};
state: 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
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 (
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>
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;
component={component}
currentBranchLike={mainBranch}
/>,
- { context: { branchesEnabled: true } }
+ { context: { branchesEnabled: true, canAdmin: true } }
)
).toMatchSnapshot();
});
component={component}
currentBranchLike={branch}
/>,
- { context: { branchesEnabled: true } }
+ { context: { branchesEnabled: true, canAdmin: true } }
)
).toMatchSnapshot();
});
component={component}
currentBranchLike={pullRequest}
/>,
- { context: { branchesEnabled: true } }
+ { context: { branchesEnabled: true, canAdmin: true } }
)
).toMatchSnapshot();
});
component={component}
currentBranchLike={mainBranch}
/>,
- { context: { branchesEnabled: true } }
+ { context: { branchesEnabled: true, canAdmin: true } }
);
expect(wrapper.find('Toggler').prop('open')).toBe(false);
click(wrapper.find('a'));
component={component}
currentBranchLike={mainBranch}
/>,
- { context: { branchesEnabled: true } }
+ { context: { branchesEnabled: true, canAdmin: true } }
);
expect(wrapper.find('DocTooltip')).toMatchSnapshot();
});
component={component}
currentBranchLike={mainBranch}
/>,
- { context: { branchesEnabled: false } }
+ { context: { branchesEnabled: false, canAdmin: true } }
);
expect(wrapper.find('DocTooltip')).toMatchSnapshot();
});
component={component}
currentBranchLike={mainBranch}
/>,
- { context: { branchesEnabled: false, onSonarCloud: true } }
+ { context: { branchesEnabled: false, onSonarCloud: true, canAdmin: true } }
);
expect(wrapper.type()).toBeNull();
});
Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "component",
},
}
Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "my-project",
},
}
Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "my-project",
},
}
"pathname": "/project/extension/component-foo",
"query": Object {
"id": "foo",
+ "qualifier": "TRK",
},
}
}
"pathname": "/project/admin/extension/foo",
"query": Object {
"id": "foo",
+ "qualifier": "TRK",
},
}
}
"pathname": "/project/extension/component-foo",
"query": Object {
"id": "foo",
+ "qualifier": "TRK",
},
}
}
"pathname": "/project/extension/component-bar",
"query": Object {
"id": "foo",
+ "qualifier": "TRK",
},
}
}
"pathname": "/project/admin/extension/foo",
"query": Object {
"id": "foo",
+ "qualifier": "TRK",
},
}
}
"pathname": "/project/admin/extension/bar",
"query": Object {
"id": "foo",
+ "qualifier": "TRK",
},
}
}
Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "foo",
},
}
Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "foo",
},
}
Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "foo",
},
}
Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "foo",
},
}
Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "qwe",
},
}
Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "foo",
},
}
Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "foo",
},
}
Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "foo",
},
}
}
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 }
assigneeLogin?: string;
assigneeName?: string;
author?: string;
+ branch?: string;
comments?: IssueComment[];
component: string;
componentLongName: string;
projectName: string;
projectOrganization: string;
projectUuid: string;
+ pullRequest?: string;
resolution?: string;
rule: string;
ruleName: string;
Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "foo",
},
}
Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "foo",
},
}
return <span />;
}
- return <Measure value={measure.value} metricKey={finalMetricKey} metricType={finalMetricType} />;
+ return <Measure metricKey={finalMetricKey} metricType={finalMetricType} value={measure.value} />;
}
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';
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>
);
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>
);
);
}
+ 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>;
}
import { Measure } from '../../helpers/measures';
export interface Component extends Breadcrumb {
+ branch?: string;
measures?: Measure[];
path?: string;
refKey?: string;
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'; */
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">
<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>
* 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: {
// 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">
isShortLivingBranch,
isSameBranchLike,
getBranchLikeQuery,
- isPullRequest
+ isPullRequest,
+ fillBranchLike
} from '../../../helpers/branches';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { RawQuery } from '../../../helpers/query';
<div>
{openIssue ? (
<IssuesSourceViewer
- branchLike={this.props.branchLike}
+ branchLike={fillBranchLike(openIssue.branch, openIssue.pullRequest)}
loadIssues={this.fetchIssuesForComponent}
locationsNavigator={this.state.locationsNavigator}
onIssueChange={this.handleIssueChange}
Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "proj",
},
}
Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "proj",
},
}
Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "sub-proj",
},
}
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 {
}
componentWillReceiveProps(nextProps: Props) {
- if (nextProps.component !== this.props.component) {
+ if (nextProps.component.key !== this.props.component.key) {
this.setState({ leaks: undefined });
}
}
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({
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';
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} />
)}
}));
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);
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">
}
renderLeak() {
- const { component, leakPeriod } = this.props;
+ const { branchLike, component, leakPeriod } = this.props;
if (!leakPeriod) {
return null;
}
return (
<div className="overview-domain-leak">
{component.qualifier === 'APP' ? (
- <ApplicationLeakPeriodLegend component={component.key} />
+ <ApplicationLeakPeriodLegend
+ branch={isLongLivingBranch(branchLike) ? branchLike : undefined}
+ component={component}
+ />
) : (
<LeakPeriodLegend period={leakPeriod} />
)}
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;
}
}
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 }) => {
Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "foo",
},
}
Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "foo",
},
}
Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "barbar",
},
}
Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "bazbaz",
},
}
"link": Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "foo",
},
},
"link": Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "foo",
},
},
Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "name",
},
}
Object {
"pathname": "/dashboard",
"query": Object {
+ "branch": undefined,
"id": "project:src/file.js",
},
}
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;
+}
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 {
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)
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.