]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12718 User can get a permalink to the hotspot
authorPhilippe Perrin <philippe.perrin@sonarsource.com>
Tue, 18 Feb 2020 16:30:25 +0000 (17:30 +0100)
committerSonarTech <sonartech@sonarsource.com>
Fri, 21 Feb 2020 19:46:19 +0000 (20:46 +0100)
15 files changed:
server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap
server/sonar-web/src/main/js/apps/security-hotspots/components/FilterBar.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/FilterBar-test.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewer-test.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerRenderer-test.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap
server/sonar-web/src/main/js/helpers/testMocks.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index c2a81755b4d4ca72f4b2dc60b4ce6ec2386b201c..5367fb99884132b052181317f11a7c005c4f43dc 100644 (file)
@@ -30,7 +30,6 @@ import { getBranchLikeQuery, isPullRequest, isSameBranchLike } from '../../helpe
 import { getStandards } from '../../helpers/security-standard';
 import { isLoggedIn } from '../../helpers/users';
 import { BranchLike } from '../../types/branch-like';
-import { ComponentQualifier } from '../../types/component';
 import {
   HotspotFilters,
   HotspotResolution,
@@ -330,11 +329,11 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
     return (
       <SecurityHotspotsAppRenderer
         branchLike={branchLike}
+        component={component}
         filters={filters}
         hotspots={hotspots}
         hotspotsReviewedMeasure={hotspotsReviewedMeasure}
         hotspotsTotal={hotspotsTotal}
-        isProject={component.qualifier === ComponentQualifier.Project}
         isStaticListOfHotspots={Boolean(hotspotKeys && hotspotKeys.length > 0)}
         loading={loading}
         loadingMeasure={loadingMeasure}
index 51b2558f12ba10b7af803c84021801a2d287065c..c9d8b687f841a8fb5771b14c1e10effe0f560668 100644 (file)
@@ -35,11 +35,11 @@ import './styles.css';
 
 export interface SecurityHotspotsAppRendererProps {
   branchLike?: BranchLike;
+  component: T.Component;
   filters: HotspotFilters;
   hotspots: RawHotspot[];
   hotspotsReviewedMeasure?: string;
   hotspotsTotal?: number;
-  isProject: boolean;
   isStaticListOfHotspots: boolean;
   loading: boolean;
   loadingMeasure: boolean;
@@ -56,10 +56,10 @@ export interface SecurityHotspotsAppRendererProps {
 export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRendererProps) {
   const {
     branchLike,
+    component,
     hotspots,
     hotspotsReviewedMeasure,
     hotspotsTotal,
-    isProject,
     isStaticListOfHotspots,
     loading,
     loadingMeasure,
@@ -72,9 +72,9 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe
   return (
     <div id="security_hotspots">
       <FilterBar
+        component={component}
         filters={filters}
         hotspotsReviewedMeasure={hotspotsReviewedMeasure}
-        isProject={isProject}
         isStaticListOfHotspots={isStaticListOfHotspots}
         loadingMeasure={loadingMeasure}
         onBranch={isBranch(branchLike)}
@@ -120,6 +120,7 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe
                     <div className="main">
                       <HotspotViewer
                         branchLike={branchLike}
+                        component={component}
                         hotspotKey={selectedHotspot.key}
                         onUpdateHotspot={props.onUpdateHotspot}
                         securityCategories={securityCategories}
index ab89cfc8616ab27bc8849eccff7a917d43105744..5cdbc37d471a5eca69cae193e6d0ce75001e3578 100644 (file)
@@ -21,6 +21,7 @@ import { shallow } from 'enzyme';
 import * as React from 'react';
 import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
 import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots';
+import { mockComponent } from '../../../helpers/testMocks';
 import { HotspotStatusFilter } from '../../../types/security-hotspots';
 import FilterBar from '../components/FilterBar';
 import SecurityHotspotsAppRenderer, {
@@ -72,13 +73,13 @@ it('should properly propagate the "show all" call', () => {
 function shallowRender(props: Partial<SecurityHotspotsAppRendererProps> = {}) {
   return shallow(
     <SecurityHotspotsAppRenderer
+      component={mockComponent()}
       filters={{
         assignedToMe: false,
         sinceLeakPeriod: false,
         status: HotspotStatusFilter.TO_REVIEW
       }}
       hotspots={[]}
-      isProject={true}
       isStaticListOfHotspots={true}
       loading={false}
       loadingMeasure={false}
index 78f624205a3bdd7f3b1f5fd7df4517ddc942134c..16af456367b20f0e6882ef191c6d23fe6457fffe 100644 (file)
@@ -10,6 +10,29 @@ exports[`should render correctly 1`] = `
       "name": "branch-6.7",
     }
   }
+  component={
+    Object {
+      "breadcrumbs": Array [],
+      "key": "my-project",
+      "name": "MyProject",
+      "organization": "foo",
+      "qualifier": "TRK",
+      "qualityGate": Object {
+        "isDefault": true,
+        "key": "30",
+        "name": "Sonar way",
+      },
+      "qualityProfiles": Array [
+        Object {
+          "deleted": false,
+          "key": "my-qp",
+          "language": "ts",
+          "name": "Sonar way",
+        },
+      ],
+      "tags": Array [],
+    }
+  }
   filters={
     Object {
       "assignedToMe": false,
@@ -18,7 +41,6 @@ exports[`should render correctly 1`] = `
     }
   }
   hotspots={Array []}
-  isProject={true}
   isStaticListOfHotspots={false}
   loading={true}
   loadingMeasure={true}
index 0b68326d146fbc62750155c778da004c0eb20a7e..0aba55e85eb9643c446076e134af52dcbeac5b40 100644 (file)
@@ -5,6 +5,29 @@ exports[`should render correctly 1`] = `
   id="security_hotspots"
 >
   <Connect(withCurrentUser(FilterBar))
+    component={
+      Object {
+        "breadcrumbs": Array [],
+        "key": "my-project",
+        "name": "MyProject",
+        "organization": "foo",
+        "qualifier": "TRK",
+        "qualityGate": Object {
+          "isDefault": true,
+          "key": "30",
+          "name": "Sonar way",
+        },
+        "qualityProfiles": Array [
+          Object {
+            "deleted": false,
+            "key": "my-qp",
+            "language": "ts",
+            "name": "Sonar way",
+          },
+        ],
+        "tags": Array [],
+      }
+    }
     filters={
       Object {
         "assignedToMe": false,
@@ -12,7 +35,6 @@ exports[`should render correctly 1`] = `
         "status": "TO_REVIEW",
       }
     }
-    isProject={true}
     isStaticListOfHotspots={true}
     loadingMeasure={false}
     onBranch={false}
@@ -146,6 +168,29 @@ exports[`should render correctly with hotspots 2`] = `
         className="main"
       >
         <HotspotViewer
+          component={
+            Object {
+              "breadcrumbs": Array [],
+              "key": "my-project",
+              "name": "MyProject",
+              "organization": "foo",
+              "qualifier": "TRK",
+              "qualityGate": Object {
+                "isDefault": true,
+                "key": "30",
+                "name": "Sonar way",
+              },
+              "qualityProfiles": Array [
+                Object {
+                  "deleted": false,
+                  "key": "my-qp",
+                  "language": "ts",
+                  "name": "Sonar way",
+                },
+              ],
+              "tags": Array [],
+            }
+          }
           hotspotKey="h2"
           onUpdateHotspot={[MockFunction]}
           securityCategories={Object {}}
index 67151c4a16c36d3c04c8c5b473287c18f6389636..283c57fa5e8a5006da2b08101f4074bce6b9e93d 100644 (file)
@@ -27,13 +27,14 @@ import { withCurrentUser } from '../../../components/hoc/withCurrentUser';
 import Measure from '../../../components/measure/Measure';
 import CoverageRating from '../../../components/ui/CoverageRating';
 import { isLoggedIn } from '../../../helpers/users';
+import { ComponentQualifier } from '../../../types/component';
 import { HotspotFilters, HotspotStatusFilter } from '../../../types/security-hotspots';
 
 export interface FilterBarProps {
   currentUser: T.CurrentUser;
+  component: T.Component;
   filters: HotspotFilters;
   hotspotsReviewedMeasure?: string;
-  isProject: boolean;
   isStaticListOfHotspots: boolean;
   loadingMeasure: boolean;
   onBranch: boolean;
@@ -65,13 +66,14 @@ const assigneeFilterOptions = [
 export function FilterBar(props: FilterBarProps) {
   const {
     currentUser,
+    component,
     filters,
     hotspotsReviewedMeasure,
-    isProject,
     isStaticListOfHotspots,
     loadingMeasure,
     onBranch
   } = props;
+  const isProject = component.qualifier === ComponentQualifier.Project;
 
   return (
     <div className="filter-bar display-flex-center">
index 37ba943c2bcb2d5f865d2fd62cfdd3393d99f457..2a23816f1ba82dbc712c5b913c231ca00aff2658 100644 (file)
@@ -26,6 +26,7 @@ import HotspotViewerRenderer from './HotspotViewerRenderer';
 
 interface Props {
   branchLike?: BranchLike;
+  component: T.Component;
   hotspotKey: string;
   onUpdateHotspot: (hotspotKey: string) => Promise<void>;
   securityCategories: T.StandardSecurityCategories;
@@ -55,7 +56,7 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
     this.mounted = false;
   }
 
-  fetchHotspot() {
+  fetchHotspot = () => {
     this.setState({ loading: true });
     return getSecurityHotspotDetails(this.props.hotspotKey)
       .then(hotspot => {
@@ -65,7 +66,7 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
         return hotspot;
       })
       .catch(() => this.mounted && this.setState({ loading: false }));
-  }
+  };
 
   handleHotspotUpdate = () => {
     return this.fetchHotspot().then((hotspot?: Hotspot) => {
@@ -76,12 +77,13 @@ export default class HotspotViewer extends React.PureComponent<Props, State> {
   };
 
   render() {
-    const { branchLike, securityCategories } = this.props;
+    const { branchLike, component, securityCategories } = this.props;
     const { hotspot, loading } = this.state;
 
     return (
       <HotspotViewerRenderer
         branchLike={branchLike}
+        component={component}
         hotspot={hotspot}
         loading={loading}
         onUpdateHotspot={this.handleHotspotUpdate}
index ca3f618ec4168df404b8040ae9a54f0e50592875..9fa56f161e979514e9e96b4cd2c02e992de03778 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
+import { ClipboardButton } from 'sonar-ui-common/components/controls/clipboard';
+import LinkIcon from 'sonar-ui-common/components/icons/LinkIcon';
 import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
 import { translate } from 'sonar-ui-common/helpers/l10n';
+import { getPathUrlAsString } from 'sonar-ui-common/helpers/urls';
+import { getBranchLikeQuery } from '../../../helpers/branch-like';
+import { getComponentSecurityHotspotsUrl } from '../../../helpers/urls';
 import { BranchLike } from '../../../types/branch-like';
 import { Hotspot } from '../../../types/security-hotspots';
 import Assignee from './assignee/Assignee';
@@ -29,6 +34,7 @@ import Status from './status/Status';
 
 export interface HotspotViewerRendererProps {
   branchLike?: BranchLike;
+  component: T.Component;
   hotspot?: Hotspot;
   loading: boolean;
   onUpdateHotspot: () => Promise<void>;
@@ -36,7 +42,15 @@ export interface HotspotViewerRendererProps {
 }
 
 export default function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
-  const { branchLike, hotspot, loading, securityCategories } = props;
+  const { branchLike, component, hotspot, loading, securityCategories } = props;
+
+  const permalink = getPathUrlAsString(
+    getComponentSecurityHotspotsUrl(component.key, {
+      ...getBranchLikeQuery(branchLike),
+      hotspots: hotspot?.key
+    }),
+    false
+  );
 
   return (
     <DeferredSpinner loading={loading}>
@@ -44,7 +58,11 @@ export default function HotspotViewerRenderer(props: HotspotViewerRendererProps)
         <div className="big-padded">
           <div className="big-spacer-bottom">
             <div className="display-flex-space-between">
-              <h1>{hotspot.message}</h1>
+              <strong className="big">{hotspot.message}</strong>
+              <ClipboardButton copyValue={permalink}>
+                <LinkIcon className="spacer-right" />
+                <span>{translate('hotspots.get_permalink')}</span>
+              </ClipboardButton>
             </div>
             <div className="text-muted">
               <span>{translate('category')}:</span>
index 31dce745ea460dfc6ae2e1c2a89664fbab8d384b..da016ade85b13d49557da0d1f2c95a1e46159a27 100644 (file)
@@ -21,7 +21,8 @@ import { shallow } from 'enzyme';
 import * as React from 'react';
 import RadioToggle from 'sonar-ui-common/components/controls/RadioToggle';
 import Select from 'sonar-ui-common/components/controls/Select';
-import { mockCurrentUser, mockLoggedInUser } from '../../../../helpers/testMocks';
+import { mockComponent, mockCurrentUser, mockLoggedInUser } from '../../../../helpers/testMocks';
+import { ComponentQualifier } from '../../../../types/component';
 import { HotspotStatusFilter } from '../../../../types/security-hotspots';
 import { AssigneeFilterOption, FilterBar, FilterBarProps } from '../FilterBar';
 
@@ -32,9 +33,12 @@ it('should render correctly', () => {
   expect(shallowRender({ hotspotsReviewedMeasure: '23.30' })).toMatchSnapshot(
     'with hotspots reviewed measure'
   );
-  expect(shallowRender({ currentUser: mockLoggedInUser(), isProject: false })).toMatchSnapshot(
-    'non-project'
-  );
+  expect(
+    shallowRender({
+      currentUser: mockLoggedInUser(),
+      component: mockComponent({ qualifier: ComponentQualifier.Application })
+    })
+  ).toMatchSnapshot('non-project');
 });
 
 it('should render correctly when the list of hotspot is static', () => {
@@ -101,13 +105,13 @@ it('should trigger onChange for leak period', () => {
 function shallowRender(props: Partial<FilterBarProps> = {}) {
   return shallow(
     <FilterBar
+      component={mockComponent()}
       currentUser={mockCurrentUser()}
       filters={{
         assignedToMe: false,
         sinceLeakPeriod: false,
         status: HotspotStatusFilter.TO_REVIEW
       }}
-      isProject={true}
       isStaticListOfHotspots={false}
       loadingMeasure={false}
       onBranch={true}
index 01e8029eee08386132670ca2180cb1848ba0de4e..65efeea8fca65cb7576fad85149813df8055eaf9 100644 (file)
@@ -21,6 +21,7 @@ import { shallow } from 'enzyme';
 import * as React from 'react';
 import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
 import { getSecurityHotspotDetails } from '../../../../api/security-hotspots';
+import { mockComponent } from '../../../../helpers/testMocks';
 import HotspotViewer from '../HotspotViewer';
 
 const hotspotKey = 'hotspot-key';
@@ -48,6 +49,7 @@ it('should render correctly', async () => {
 function shallowRender(props?: Partial<HotspotViewer['props']>) {
   return shallow<HotspotViewer>(
     <HotspotViewer
+      component={mockComponent()}
       hotspotKey={hotspotKey}
       onUpdateHotspot={jest.fn()}
       securityCategories={{ cat1: { title: 'cat1' } }}
index de1a77e5c774148bdd42e2bc7def8187a3cc906c..5fb5a19fc4d5db68cffd81c4b9213456a026336c 100644 (file)
@@ -19,8 +19,9 @@
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
+import { mockBranch } from '../../../../helpers/mocks/branch-like';
 import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
-import { mockUser } from '../../../../helpers/testMocks';
+import { mockComponent, mockUser } from '../../../../helpers/testMocks';
 import HotspotViewerRenderer, { HotspotViewerRendererProps } from '../HotspotViewerRenderer';
 
 it('should render correctly', () => {
@@ -46,6 +47,8 @@ it('should render correctly', () => {
 function shallowRender(props?: Partial<HotspotViewerRendererProps>) {
   return shallow(
     <HotspotViewerRenderer
+      branchLike={mockBranch()}
+      component={mockComponent()}
       hotspot={mockHotspot()}
       loading={false}
       onUpdateHotspot={jest.fn()}
index c069dd4ca30daa3724b01d06cc18fcf0c3cd79b3..5168e89398f1e0edfc4a0749c187c239411d65b4 100644 (file)
@@ -2,6 +2,29 @@
 
 exports[`should render correctly 1`] = `
 <HotspotViewerRenderer
+  component={
+    Object {
+      "breadcrumbs": Array [],
+      "key": "my-project",
+      "name": "MyProject",
+      "organization": "foo",
+      "qualifier": "TRK",
+      "qualityGate": Object {
+        "isDefault": true,
+        "key": "30",
+        "name": "Sonar way",
+      },
+      "qualityProfiles": Array [
+        Object {
+          "deleted": false,
+          "key": "my-qp",
+          "language": "ts",
+          "name": "Sonar way",
+        },
+      ],
+      "tags": Array [],
+    }
+  }
   loading={true}
   onUpdateHotspot={[Function]}
   securityCategories={
@@ -16,6 +39,29 @@ exports[`should render correctly 1`] = `
 
 exports[`should render correctly 2`] = `
 <HotspotViewerRenderer
+  component={
+    Object {
+      "breadcrumbs": Array [],
+      "key": "my-project",
+      "name": "MyProject",
+      "organization": "foo",
+      "qualifier": "TRK",
+      "qualityGate": Object {
+        "isDefault": true,
+        "key": "30",
+        "name": "Sonar way",
+      },
+      "qualityProfiles": Array [
+        Object {
+          "deleted": false,
+          "key": "my-qp",
+          "language": "ts",
+          "name": "Sonar way",
+        },
+      ],
+      "tags": Array [],
+    }
+  }
   hotspot={
     Object {
       "id": "I am a detailled hotspot",
index 9f8802f8c9e2c097e02b43168a22d9d6e0d7adf3..5f829d2c31f0ec8bf805184311247647d8b3fca7 100644 (file)
@@ -14,9 +14,21 @@ exports[`should render correctly 1`] = `
       <div
         className="display-flex-space-between"
       >
-        <h1>
+        <strong
+          className="big"
+        >
           '3' is a magic number.
-        </h1>
+        </strong>
+        <ClipboardButton
+          copyValue="http://localhost/security_hotspots?id=my-project&branch=branch-6.7&hotspots=01fc972e-2a3c-433e-bcae-0bd7f88f5123"
+        >
+          <LinkIcon
+            className="spacer-right"
+          />
+          <span>
+            hotspots.get_permalink
+          </span>
+        </ClipboardButton>
       </div>
       <div
         className="text-muted"
@@ -241,6 +253,14 @@ exports[`should render correctly 1`] = `
       />
     </div>
     <HotspotSnippetContainer
+      branchLike={
+        Object {
+          "analysisDate": "2018-01-01",
+          "excludedFromPurge": true,
+          "isMain": false,
+          "name": "branch-6.7",
+        }
+      }
       hotspot={
         Object {
           "assignee": "assignee",
@@ -461,9 +481,21 @@ exports[`should render correctly: anonymous user 1`] = `
       <div
         className="display-flex-space-between"
       >
-        <h1>
+        <strong
+          className="big"
+        >
           '3' is a magic number.
-        </h1>
+        </strong>
+        <ClipboardButton
+          copyValue="http://localhost/security_hotspots?id=my-project&branch=branch-6.7&hotspots=01fc972e-2a3c-433e-bcae-0bd7f88f5123"
+        >
+          <LinkIcon
+            className="spacer-right"
+          />
+          <span>
+            hotspots.get_permalink
+          </span>
+        </ClipboardButton>
       </div>
       <div
         className="text-muted"
@@ -688,6 +720,14 @@ exports[`should render correctly: anonymous user 1`] = `
       />
     </div>
     <HotspotSnippetContainer
+      branchLike={
+        Object {
+          "analysisDate": "2018-01-01",
+          "excludedFromPurge": true,
+          "isMain": false,
+          "name": "branch-6.7",
+        }
+      }
       hotspot={
         Object {
           "assignee": "assignee",
@@ -908,9 +948,21 @@ exports[`should render correctly: assignee without name 1`] = `
       <div
         className="display-flex-space-between"
       >
-        <h1>
+        <strong
+          className="big"
+        >
           '3' is a magic number.
-        </h1>
+        </strong>
+        <ClipboardButton
+          copyValue="http://localhost/security_hotspots?id=my-project&branch=branch-6.7&hotspots=01fc972e-2a3c-433e-bcae-0bd7f88f5123"
+        >
+          <LinkIcon
+            className="spacer-right"
+          />
+          <span>
+            hotspots.get_permalink
+          </span>
+        </ClipboardButton>
       </div>
       <div
         className="text-muted"
@@ -1135,6 +1187,14 @@ exports[`should render correctly: assignee without name 1`] = `
       />
     </div>
     <HotspotSnippetContainer
+      branchLike={
+        Object {
+          "analysisDate": "2018-01-01",
+          "excludedFromPurge": true,
+          "isMain": false,
+          "name": "branch-6.7",
+        }
+      }
       hotspot={
         Object {
           "assignee": "assignee",
@@ -1355,9 +1415,21 @@ exports[`should render correctly: deleted assignee 1`] = `
       <div
         className="display-flex-space-between"
       >
-        <h1>
+        <strong
+          className="big"
+        >
           '3' is a magic number.
-        </h1>
+        </strong>
+        <ClipboardButton
+          copyValue="http://localhost/security_hotspots?id=my-project&branch=branch-6.7&hotspots=01fc972e-2a3c-433e-bcae-0bd7f88f5123"
+        >
+          <LinkIcon
+            className="spacer-right"
+          />
+          <span>
+            hotspots.get_permalink
+          </span>
+        </ClipboardButton>
       </div>
       <div
         className="text-muted"
@@ -1582,6 +1654,14 @@ exports[`should render correctly: deleted assignee 1`] = `
       />
     </div>
     <HotspotSnippetContainer
+      branchLike={
+        Object {
+          "analysisDate": "2018-01-01",
+          "excludedFromPurge": true,
+          "isMain": false,
+          "name": "branch-6.7",
+        }
+      }
       hotspot={
         Object {
           "assignee": "assignee",
@@ -1809,9 +1889,21 @@ exports[`should render correctly: unassigned 1`] = `
       <div
         className="display-flex-space-between"
       >
-        <h1>
+        <strong
+          className="big"
+        >
           '3' is a magic number.
-        </h1>
+        </strong>
+        <ClipboardButton
+          copyValue="http://localhost/security_hotspots?id=my-project&branch=branch-6.7&hotspots=01fc972e-2a3c-433e-bcae-0bd7f88f5123"
+        >
+          <LinkIcon
+            className="spacer-right"
+          />
+          <span>
+            hotspots.get_permalink
+          </span>
+        </ClipboardButton>
       </div>
       <div
         className="text-muted"
@@ -2036,6 +2128,14 @@ exports[`should render correctly: unassigned 1`] = `
       />
     </div>
     <HotspotSnippetContainer
+      branchLike={
+        Object {
+          "analysisDate": "2018-01-01",
+          "excludedFromPurge": true,
+          "isMain": false,
+          "name": "branch-6.7",
+        }
+      }
       hotspot={
         Object {
           "assignee": undefined,
index b9f034fff2ac7cc5a4f6fb9321fe2b3c7fe20966..1005c3dc63ec5942d1c1d67bc72be4ee607a6f6c 100644 (file)
@@ -23,6 +23,7 @@ import { InjectedRouter } from 'react-router';
 import { createStore, Store } from 'redux';
 import { DocumentationEntry } from '../apps/documentation/utils';
 import { Exporter, Profile } from '../apps/quality-profiles/types';
+import { ComponentQualifier } from '../types/component';
 
 export function mockAlmApplication(overrides: Partial<T.AlmApplication> = {}): T.AlmApplication {
   return {
@@ -277,7 +278,7 @@ export function mockComponent(overrides: Partial<T.Component> = {}): T.Component
     key: 'my-project',
     name: 'MyProject',
     organization: 'foo',
-    qualifier: 'TRK',
+    qualifier: ComponentQualifier.Project,
     qualityGate: { isDefault: true, key: '30', name: 'Sonar way' },
     qualityProfiles: [
       {
index f4c01c35339434e256e8dae858b93668d90e4a00..f19013aa7acc185e9e2d2335fdf1b7550635000c 100644 (file)
@@ -685,6 +685,7 @@ hotspots.status_option.FIXED=Fixed
 hotspots.status_option.FIXED.description=The code has been modified to follow recommended secure coding practices.
 hotspots.status_option.SAFE=Safe
 hotspots.status_option.SAFE.description=The code is not at risk and doesn't need to be modified.
+hotspots.get_permalink=Get Permalink
 
 hotspot.filters.title=Filters
 hotspot.filters.assignee.assigned_to_me=Assigned to me