]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10813 Add project branches
authorPascal Mugnier <pascal.mugnier@sonarsource.com>
Thu, 21 Jun 2018 09:08:48 +0000 (11:08 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 29 Jun 2018 07:10:16 +0000 (09:10 +0200)
34 files changed:
server/sonar-web/src/main/js/api/application.ts
server/sonar-web/src/main/js/api/quality-gates.ts
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavHeader-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap
server/sonar-web/src/main/js/app/types.ts
server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.tsx.snap
server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskComponent-test.tsx.snap
server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx
server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx
server/sonar-web/src/main/js/apps/code/types.ts
server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.js
server/sonar-web/src/main/js/apps/component/components/App.tsx
server/sonar-web/src/main/js/apps/issues/components/App.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap
server/sonar-web/src/main/js/apps/overview/components/ApplicationLeakPeriodLegend.tsx
server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx
server/sonar-web/src/main/js/apps/overview/components/__tests__/ApplicationLeakPeriodLegend-test.tsx
server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.tsx
server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.tsx
server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.tsx.snap
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap
server/sonar-web/src/main/js/helpers/branches.ts
server/sonar-web/src/main/js/helpers/urls.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 5821d5705bec197c72e9a58e328756cafe118bf7..6fb3876a4865080182e1beecfffc5b4043a3cc1e 100644 (file)
@@ -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
+  );
 }
index d2f212bd457d26d2ebe6753f521336da82be6abd..af49578283e94d2c1199e7f552ce840af9cb564a 100644 (file)
@@ -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);
index 8cba10a5dbcda86bc3d32246ab9aa6f0cbdba5df..d02389be63dbc4bba19503d740cc6f00fa4f7978 100644 (file)
@@ -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 (
index 1f3045cfc52ee184c8d076be210308ad92eb6353..53009cc4d40fd4aa0634f7de7a36dcc2cde40e02 100644 (file)
@@ -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>
index 6b52b5eefbf21773b4653e88b3e33cfb03cf6ed1..7b2f2e2c2219e60f6d086abb7a917b05ea1ca1d3 100644 (file)
@@ -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;
index 9a805b74c8212e94e094cdc5cfaa02282fdba6ed..3fd20dbc41ec7e1b08cfdfbf5e637cfdb9a44294 100644 (file)
@@ -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();
 });
index b4c9f37a5d33e6ba32c387a65d7bc36d70bfa6ee..25528a5523c017bae0e00a15f42d49a52e08b706 100644 (file)
@@ -13,6 +13,7 @@ exports[`renders main branch 1`] = `
       Object {
         "pathname": "/dashboard",
         "query": Object {
+          "branch": undefined,
           "id": "component",
         },
       }
index 4d85fb56b116227f8252e2bdbdcc9f9918f8707a..b8498689bdab9d7be03cf08f661f2000cfb6a03a 100644 (file)
@@ -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",
           },
         }
index 3abe30c25f4ef1ffedea0966a637797d1aa74b08..a62c3ce9e0ead04ec1df6587fb1d9659b0ed721e 100644 (file)
@@ -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",
               },
             }
           }
index 4ad1c1e2e2dc668d59336f7b91a8065681e65ba0..0a9d6781bf0eecc8738c884e7edc5bf6bcfd3705 100644 (file)
@@ -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",
           },
         }
index 11e29502c06be798e7bd6606f640a802b448e814..75ac42d3237e2923f007fbb404c367c79c333fa1 100644 (file)
@@ -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;
index 90b25b16cf1d4f0e3be99c1a4ead26f29db3cf51..11b25e6dd9dfaab7dcf6909a0624add4a9ac374a 100644 (file)
@@ -25,6 +25,7 @@ exports[`should match snapshot 1`] = `
               Object {
                 "pathname": "/dashboard",
                 "query": Object {
+                  "branch": undefined,
                   "id": "foo",
                 },
               }
index 88328a93b61d0cfa4f1063ebf3f945c042597650..b054cb915e8ed1f3303694322f62cf36d0dc328d 100644 (file)
@@ -20,6 +20,7 @@ exports[`renders correctly 1`] = `
       Object {
         "pathname": "/dashboard",
         "query": Object {
+          "branch": undefined,
           "id": "foo",
         },
       }
index bc900211a0ee8932154610108fc55e2f744cbe0b..bb3cbe506a20fd8bf4b30830d17208811aa05f93 100644 (file)
@@ -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} />;
 }
index 57473532b8ab5601b70ad31a6b55f9ba72882a95..391c1862c04835f26ac66e3931b9a646c899ead4 100644 (file)
@@ -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>;
 }
index 3a226f8127d6df649ce72632ee8b4b21ca517c44..a0b6459a3d3aa2596cf0137fde14b763efd2ccc4 100644 (file)
@@ -21,6 +21,7 @@
 import { Measure } from '../../helpers/measures';
 
 export interface Component extends Breadcrumb {
+  branch?: string;
   measures?: Measure[];
   path?: string;
   refKey?: string;
index 9b2ce2e9b6ee06c09a8ca8d6a1304f38ee289311..fc268fa342512b140b3ec1fd1783768e5c569604 100644 (file)
@@ -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} />
         &nbsp;
         {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>
index 71fc859ea2f7d9e7831c0d0d91c665d56750f1ef..d70fc1c92e6205975a0362d155176666460ed0f8 100644 (file)
@@ -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">
index 2ae38d862de5bd4c2edfa4fb1b5d5681b4b8d795..4e22af0bfd398de804221e6e2df69a8620694868 100644 (file)
@@ -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}
index 522da78f5a8d6b802111fd09de65bd6fef2a4ffd..c0182df2b97a64488654ec76cdba1b78f86257b7 100644 (file)
@@ -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",
           },
         }
index eb230682fcb8ee9dbcb466493a878f3cbaa7e1b2..0ac7464bcb38e1a1f9aa90bf1286a35d57ba3117 100644 (file)
@@ -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({
index 68a7d1bbb1db3cd62db22e2c60daf6e420b028b3..df32445c4121aec77ba57f5778efefa6943cbebe 100644 (file)
@@ -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} />
         )}
index 27a71ac1851f90969cd699cdf1cb333da01e65ee..1dba9c67b4b203879ca6808415c5e5c884b62b30 100644 (file)
@@ -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);
index 3ce147ba7185f31567ffc9ec3f0ed58ab6b866df..4715c66a2ffdd9e75cd47181b01bd3ed532e1dd3 100644 (file)
@@ -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} />
         )}
index d50bc8a42f1e8fd440929d26a4deb006986a1af4..e4771d74e76ac32a019461d3f6d68a8afe7f7845 100644 (file)
@@ -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 }) => {
index b4c8503062555850fe52b18f47815989821ae1ee..8af25b25049bde58680063f4d4dfa82f31284d52 100644 (file)
@@ -9,6 +9,7 @@ exports[`renders 1`] = `
     Object {
       "pathname": "/dashboard",
       "query": Object {
+        "branch": undefined,
         "id": "foo",
       },
     }
index ceb66108e3c53d62f568a60da497a1ac24979bd4..736ab24806037a3c2bc3c6a2b5acb82176635d18 100644 (file)
@@ -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",
                 },
               }
index c1aac43c7d7b160efa9b32595e2d341fead78568..38408926a5b2f135739994caa5fee4ec217028b6 100644 (file)
@@ -18,6 +18,7 @@ exports[`renders 1`] = `
           "link": Object {
             "pathname": "/dashboard",
             "query": Object {
+              "branch": undefined,
               "id": "foo",
             },
           },
index 0c07a84920e4ae4b100810fa3b5e8149d0d7d383..fd133225a39c1cd98420e368cb566418fbd915d3 100644 (file)
@@ -18,6 +18,7 @@ exports[`renders 1`] = `
           "link": Object {
             "pathname": "/dashboard",
             "query": Object {
+              "branch": undefined,
               "id": "foo",
             },
           },
index 98ca96135f8c882d71d4fddad80a84d2885c8bba..aead0a2ce176a4cf8fb27af9bfe6033de0591239 100644 (file)
@@ -352,6 +352,7 @@ exports[`creates project 4`] = `
                   Object {
                     "pathname": "/dashboard",
                     "query": Object {
+                      "branch": undefined,
                       "id": "name",
                     },
                   }
index b053a1bf9af1168ab97bf4ca18fe08e34649a094..5552a961caa9f98b54444e7c58b97ea0b48ed9b7 100644 (file)
@@ -55,6 +55,7 @@ exports[`should render OK test 1`] = `
               Object {
                 "pathname": "/dashboard",
                 "query": Object {
+                  "branch": undefined,
                   "id": "project:src/file.js",
                 },
               }
index 6b4507eddcd04fc5a24e02e85e77c0405f3b15fb..86dc7e10671cb541e70c52584a3e165a128a0a05 100644 (file)
@@ -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;
+}
index 93e531c52df70c4449455225b586006e052c0fb8..4d17bde0b09b990874bfd2cfb0c5d447cf0e49d3 100644 (file)
@@ -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)
index 65e77118171b09b40f41bc41a5e0a4f596b7e81e..79d87fab775e530612517a0d32e0e781d8869d57 100644 (file)
@@ -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.