]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-16303 The 'Why is this an issue' button is not available anymore on the issue...
authorMathieu Suen <mathieu.suen@sonarsource.com>
Fri, 6 May 2022 12:06:18 +0000 (14:06 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 24 May 2022 20:10:14 +0000 (20:10 +0000)
29 files changed:
server/sonar-web/src/main/js/api/issues.ts
server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesSourceViewer-test.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesSourceViewer-test.tsx.snap
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewerWrapper.tsx
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewerWrapper-test.tsx
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/ComponentSourceSnippetGroupViewer-test.tsx.snap
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewerWrapper-test.tsx.snap
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx
server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewerHeader-test.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssueList-test.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlay-test.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssueList-test.tsx.snap
server/sonar-web/src/main/js/components/issue/Issue.tsx
server/sonar-web/src/main/js/components/issue/IssueView.tsx
server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx
server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.tsx
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueMessage-test.tsx
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.tsx
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueMessage-test.tsx.snap
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.tsx.snap
server/sonar-web/src/main/js/helpers/request.ts
server/sonar-web/src/main/js/helpers/testMocks.ts
server/sonar-web/src/main/js/types/types.ts

index b4e6509c10cac6139a8bab02c2a99ccc8b781a78..6518a532e41970a4a0f18445b4301ad3696f8ed3 100644 (file)
  */
 import getCoverageStatus from '../components/SourceViewer/helpers/getCoverageStatus';
 import { throwGlobalError } from '../helpers/error';
-import { getJSON, post, postJSON, RequestData } from '../helpers/request';
+import {
+  get,
+  getJSON,
+  HttpStatus,
+  parseJSON,
+  post,
+  postJSON,
+  RequestData
+} from '../helpers/request';
 import { IssueResponse, RawIssuesResponse } from '../types/issues';
 import { Dict, FacetValue, IssueChangelog, SnippetsByComponent, SourceLine } from '../types/types';
 
@@ -145,19 +153,26 @@ export function searchIssueAuthors(data: {
 }
 
 export function getIssueFlowSnippets(issueKey: string): Promise<Dict<SnippetsByComponent>> {
-  return getJSON('/api/sources/issue_snippets', { issueKey }).then(result => {
-    Object.keys(result).forEach(k => {
-      if (result[k].sources) {
-        result[k].sources = result[k].sources.reduce(
-          (lineMap: Dict<SourceLine>, line: SourceLine) => {
-            line.coverageStatus = getCoverageStatus(line);
-            lineMap[line.line] = line;
-            return lineMap;
-          },
-          {}
-        );
+  return get('/api/sources/issue_snippets', { issueKey })
+    .then(r => {
+      if (r.status === HttpStatus.NoContent) {
+        return {} as any;
       }
+      return parseJSON(r);
+    })
+    .then(result => {
+      Object.keys(result).forEach(k => {
+        if (result[k].sources) {
+          result[k].sources = result[k].sources.reduce(
+            (lineMap: Dict<SourceLine>, line: SourceLine) => {
+              line.coverageStatus = getCoverageStatus(line);
+              lineMap[line.line] = line;
+              return lineMap;
+            },
+            {}
+          );
+        }
+      });
+      return result;
     });
-    return result;
-  });
 }
index e3c5384f700bcd501f313a1b1b8f3d01186102c0..34864825c2e187c7e08da1b84c11c40b1ca9e420 100644 (file)
@@ -581,52 +581,6 @@ export default class App extends React.PureComponent<Props, State> {
     );
   };
 
-  fetchIssuesForComponent = (_component: string, _from: number, to: number) => {
-    const { issues, openIssue, paging } = this.state;
-
-    if (!openIssue || !paging) {
-      return Promise.reject(undefined);
-    }
-
-    const isSameComponent = (issue: Issue) => issue.component === openIssue.component;
-
-    const done = (pageIssues: Issue[], p: Paging) => {
-      const lastIssue = pageIssues[pageIssues.length - 1];
-      if (p.total <= p.pageIndex * p.pageSize) {
-        return true;
-      }
-      if (lastIssue.component !== openIssue.component) {
-        return true;
-      }
-      return lastIssue.textRange !== undefined && lastIssue.textRange.endLine > to;
-    };
-
-    if (done(issues, paging)) {
-      return Promise.resolve(issues.filter(isSameComponent));
-    }
-
-    this.setState({ loading: true });
-    return this.fetchIssuesUntil(paging.pageIndex + 1, done).then(
-      response => {
-        const nextIssues = [...issues, ...response.issues];
-        if (this.mounted) {
-          this.setState({
-            issues: nextIssues,
-            loading: false,
-            paging: response.paging
-          });
-        }
-        return nextIssues.filter(isSameComponent);
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
-        return [];
-      }
-    );
-  };
-
   fetchFacet = (facet: string) => {
     return this.fetchIssues({ ps: 1, facets: facet }, false).then(
       ({ facets, ...other }) => {
@@ -1130,10 +1084,8 @@ export default class App extends React.PureComponent<Props, State> {
                   <IssuesSourceViewer
                     branchLike={fillBranchLike(openIssue.branch, openIssue.pullRequest)}
                     issues={issues}
-                    loadIssues={this.fetchIssuesForComponent}
                     locationsNavigator={this.state.locationsNavigator}
                     onIssueChange={this.handleIssueChange}
-                    onIssueSelect={this.openIssue}
                     onLocationSelect={this.selectLocation}
                     openIssue={openIssue}
                     selectedFlowIndex={this.state.selectedFlowIndex}
index c3ee8e8c94a42119b1de212f8a2dbe68714bfc40..649f1693ab0fbe9ab961ec639bd4f634d711621f 100644 (file)
@@ -18,7 +18,6 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import SourceViewer from '../../../components/SourceViewer/SourceViewer';
 import { scrollToElement } from '../../../helpers/scrolling';
 import { BranchLike } from '../../../types/branch-like';
 import { Issue } from '../../../types/types';
@@ -28,10 +27,8 @@ import { getLocations, getSelectedLocation } from '../utils';
 interface Props {
   branchLike: BranchLike | undefined;
   issues: Issue[];
-  loadIssues: (component: string, from: number, to: number) => Promise<Issue[]>;
   locationsNavigator: boolean;
   onIssueChange: (issue: Issue) => void;
-  onIssueSelect: (issueKey: string) => void;
   onLocationSelect: (index: number) => void;
   openIssue: Issue;
   selectedFlowIndex: number | undefined;
@@ -90,68 +87,21 @@ export default class IssuesSourceViewer extends React.PureComponent<Props> {
         ? selectedLocation && { index: selectedLocationIndex, text: selectedLocation.msg }
         : undefined;
 
-    const startLines = locations.map(l => l.textRange.startLine);
-    const showCrossComponentSourceViewer =
-      startLines.length > 0 ? Math.max(...startLines) !== Math.min(...startLines) : false;
-
-    if (showCrossComponentSourceViewer) {
-      return (
-        <div ref={node => (this.node = node)}>
-          <CrossComponentSourceViewer
-            branchLike={this.props.branchLike}
-            highlightedLocationMessage={highlightedLocationMessage}
-            issue={openIssue}
-            issues={this.props.issues}
-            locations={locations}
-            onIssueChange={this.props.onIssueChange}
-            onLoaded={this.handleLoaded}
-            onLocationSelect={this.props.onLocationSelect}
-            scroll={this.handleScroll}
-            selectedFlowIndex={selectedFlowIndex}
-          />
-        </div>
-      );
-    } else {
-      // if location is selected, show (and load) code around it
-      // otherwise show code around the open issue
-      const aroundLine = selectedLocation
-        ? selectedLocation.textRange.startLine
-        : openIssue.textRange && openIssue.textRange.endLine;
-
-      const component = selectedLocation ? selectedLocation.component : openIssue.component;
-      const allMessagesEmpty =
-        locations !== undefined && locations.every(location => !location.msg);
-
-      const highlightedLocations = locations.filter(location => location.component === component);
-
-      // do not load issues when open another file for a location
-      const loadIssues =
-        component === openIssue.component ? this.props.loadIssues : () => Promise.resolve([]);
-      const selectedIssue = component === openIssue.component ? openIssue.key : undefined;
-
-      return (
-        <div ref={node => (this.node = node)}>
-          <SourceViewer
-            aroundLine={aroundLine}
-            branchLike={this.props.branchLike}
-            component={component}
-            displayAllIssues={true}
-            displayIssueLocationsCount={true}
-            displayIssueLocationsLink={false}
-            displayLocationMarkers={!allMessagesEmpty}
-            highlightedLocationMessage={highlightedLocationMessage}
-            highlightedLocations={highlightedLocations}
-            loadIssues={loadIssues}
-            onIssueChange={this.props.onIssueChange}
-            onIssueSelect={this.props.onIssueSelect}
-            onLoaded={this.handleLoaded}
-            onLocationSelect={this.props.onLocationSelect}
-            scroll={this.handleScroll}
-            selectedIssue={selectedIssue}
-            slimHeader={true}
-          />
-        </div>
-      );
-    }
+    return (
+      <div ref={node => (this.node = node)}>
+        <CrossComponentSourceViewer
+          branchLike={this.props.branchLike}
+          highlightedLocationMessage={highlightedLocationMessage}
+          issue={openIssue}
+          issues={this.props.issues}
+          locations={locations}
+          onIssueChange={this.props.onIssueChange}
+          onLoaded={this.handleLoaded}
+          onLocationSelect={this.props.onLocationSelect}
+          scroll={this.handleScroll}
+          selectedFlowIndex={selectedFlowIndex}
+        />
+      </div>
+    );
   }
 }
index b1f3ebc1418b25cd8225a70ea67a37757be2a3f0..689eec932519324bec3c428f79be98f329579668 100644 (file)
@@ -350,21 +350,6 @@ it('should check max 500 issues', async () => {
   expect(wrapper.find('#issues-bulk-change')).toMatchSnapshot();
 });
 
-it('should fetch issues for component', async () => {
-  (searchIssues as jest.Mock).mockImplementation(mockSearchIssuesResponse());
-  const wrapper = shallowRender({
-    location: mockLocation({
-      query: { open: '0' }
-    })
-  });
-  const instance = wrapper.instance();
-  await waitAndUpdate(wrapper);
-  expect(wrapper.state('issues')).toHaveLength(2);
-
-  await instance.fetchIssuesForComponent('', 0, 30);
-  expect(wrapper.state('issues')).toHaveLength(6);
-});
-
 it('should display the right facets open', () => {
   expect(
     shallowRender({
index ec035492f45571c641128911f89ba2682ec970a4..e1e2a957372277a8a40a1d3e3a9bc7a9874e89c5 100644 (file)
@@ -69,10 +69,8 @@ function shallowRender(props: Partial<IssuesSourceViewer['props']> = {}) {
     <IssuesSourceViewer
       branchLike={mockMainBranch()}
       issues={[mockIssue()]}
-      loadIssues={jest.fn()}
       locationsNavigator={true}
       onIssueChange={jest.fn()}
-      onIssueSelect={jest.fn()}
       onLocationSelect={jest.fn()}
       openIssue={mockIssue()}
       selectedFlowIndex={undefined}
index 2604418e147c2e7836b9bcbe42112f91f7509fcc..b5cd58eeb0b0f581a6997114f1bfd3614910c123 100644 (file)
@@ -220,8 +220,7 @@ exports[`should render CrossComponentSourceViewer correctly 1`] = `
 
 exports[`should render SourceViewer correctly: all secondary locations on same line 1`] = `
 <div>
-  <SourceViewer
-    aroundLine={26}
+  <CrossComponentSourceViewer
     branchLike={
       Object {
         "analysisDate": "2018-01-01",
@@ -230,12 +229,192 @@ exports[`should render SourceViewer correctly: all secondary locations on same l
         "name": "master",
       }
     }
-    component="main.js"
-    displayAllIssues={true}
-    displayIssueLocationsCount={true}
-    displayIssueLocationsLink={false}
-    displayLocationMarkers={false}
-    highlightedLocations={
+    issue={
+      Object {
+        "actions": Array [],
+        "component": "main.js",
+        "componentLongName": "main.js",
+        "componentQualifier": "FIL",
+        "componentUuid": "foo1234",
+        "creationDate": "2017-03-01T09:36:01+0100",
+        "flows": Array [
+          Array [
+            Object {
+              "component": "main.js",
+              "index": 0,
+              "textRange": Object {
+                "endLine": 2,
+                "endOffset": 2,
+                "startLine": 1,
+                "startOffset": 1,
+              },
+            },
+            Object {
+              "component": "main.js",
+              "index": 1,
+              "textRange": Object {
+                "endLine": 2,
+                "endOffset": 2,
+                "startLine": 1,
+                "startOffset": 1,
+              },
+            },
+            Object {
+              "component": "main.js",
+              "index": 2,
+              "textRange": Object {
+                "endLine": 2,
+                "endOffset": 2,
+                "startLine": 1,
+                "startOffset": 1,
+              },
+            },
+          ],
+        ],
+        "fromHotspot": false,
+        "key": "AVsae-CQS-9G3txfbFN2",
+        "line": 25,
+        "message": "Reduce the number of conditional operators (4) used in the expression",
+        "project": "myproject",
+        "projectKey": "foo",
+        "projectName": "Foo",
+        "rule": "javascript:S1067",
+        "ruleName": "foo",
+        "secondaryLocations": Array [
+          Object {
+            "component": "main.js",
+            "textRange": Object {
+              "endLine": 2,
+              "endOffset": 2,
+              "startLine": 1,
+              "startOffset": 1,
+            },
+          },
+          Object {
+            "component": "main.js",
+            "textRange": Object {
+              "endLine": 2,
+              "endOffset": 2,
+              "startLine": 1,
+              "startOffset": 1,
+            },
+          },
+        ],
+        "severity": "MAJOR",
+        "status": "OPEN",
+        "textRange": Object {
+          "endLine": 26,
+          "endOffset": 15,
+          "startLine": 25,
+          "startOffset": 0,
+        },
+        "transitions": Array [],
+        "type": "BUG",
+      }
+    }
+    issues={
+      Array [
+        Object {
+          "actions": Array [],
+          "component": "main.js",
+          "componentLongName": "main.js",
+          "componentQualifier": "FIL",
+          "componentUuid": "foo1234",
+          "creationDate": "2017-03-01T09:36:01+0100",
+          "flows": Array [
+            Array [
+              Object {
+                "component": "main.js",
+                "textRange": Object {
+                  "endLine": 2,
+                  "endOffset": 2,
+                  "startLine": 1,
+                  "startOffset": 1,
+                },
+              },
+              Object {
+                "component": "main.js",
+                "textRange": Object {
+                  "endLine": 2,
+                  "endOffset": 2,
+                  "startLine": 1,
+                  "startOffset": 1,
+                },
+              },
+              Object {
+                "component": "main.js",
+                "textRange": Object {
+                  "endLine": 2,
+                  "endOffset": 2,
+                  "startLine": 1,
+                  "startOffset": 1,
+                },
+              },
+            ],
+            Array [
+              Object {
+                "component": "main.js",
+                "textRange": Object {
+                  "endLine": 2,
+                  "endOffset": 2,
+                  "startLine": 1,
+                  "startOffset": 1,
+                },
+              },
+              Object {
+                "component": "main.js",
+                "textRange": Object {
+                  "endLine": 2,
+                  "endOffset": 2,
+                  "startLine": 1,
+                  "startOffset": 1,
+                },
+              },
+            ],
+          ],
+          "fromHotspot": false,
+          "key": "AVsae-CQS-9G3txfbFN2",
+          "line": 25,
+          "message": "Reduce the number of conditional operators (4) used in the expression",
+          "project": "myproject",
+          "projectKey": "foo",
+          "projectName": "Foo",
+          "rule": "javascript:S1067",
+          "ruleName": "foo",
+          "secondaryLocations": Array [
+            Object {
+              "component": "main.js",
+              "textRange": Object {
+                "endLine": 2,
+                "endOffset": 2,
+                "startLine": 1,
+                "startOffset": 1,
+              },
+            },
+            Object {
+              "component": "main.js",
+              "textRange": Object {
+                "endLine": 2,
+                "endOffset": 2,
+                "startLine": 1,
+                "startOffset": 1,
+              },
+            },
+          ],
+          "severity": "MAJOR",
+          "status": "OPEN",
+          "textRange": Object {
+            "endLine": 26,
+            "endOffset": 15,
+            "startLine": 25,
+            "startOffset": 0,
+          },
+          "transitions": Array [],
+          "type": "BUG",
+        },
+      ]
+    }
+    locations={
       Array [
         Object {
           "component": "main.js",
@@ -269,22 +448,17 @@ exports[`should render SourceViewer correctly: all secondary locations on same l
         },
       ]
     }
-    loadIssues={[MockFunction]}
     onIssueChange={[MockFunction]}
-    onIssueSelect={[MockFunction]}
     onLoaded={[Function]}
     onLocationSelect={[MockFunction]}
     scroll={[Function]}
-    selectedIssue="AVsae-CQS-9G3txfbFN2"
-    slimHeader={true}
   />
 </div>
 `;
 
 exports[`should render SourceViewer correctly: default 1`] = `
 <div>
-  <SourceViewer
-    aroundLine={26}
+  <CrossComponentSourceViewer
     branchLike={
       Object {
         "analysisDate": "2018-01-01",
@@ -293,28 +467,82 @@ exports[`should render SourceViewer correctly: default 1`] = `
         "name": "master",
       }
     }
-    component="main.js"
-    displayAllIssues={true}
-    displayIssueLocationsCount={true}
-    displayIssueLocationsLink={false}
-    displayLocationMarkers={false}
-    highlightedLocations={Array []}
-    loadIssues={[MockFunction]}
+    issue={
+      Object {
+        "actions": Array [],
+        "component": "main.js",
+        "componentLongName": "main.js",
+        "componentQualifier": "FIL",
+        "componentUuid": "foo1234",
+        "creationDate": "2017-03-01T09:36:01+0100",
+        "flows": Array [],
+        "fromHotspot": false,
+        "key": "AVsae-CQS-9G3txfbFN2",
+        "line": 25,
+        "message": "Reduce the number of conditional operators (4) used in the expression",
+        "project": "myproject",
+        "projectKey": "foo",
+        "projectName": "Foo",
+        "rule": "javascript:S1067",
+        "ruleName": "foo",
+        "secondaryLocations": Array [],
+        "severity": "MAJOR",
+        "status": "OPEN",
+        "textRange": Object {
+          "endLine": 26,
+          "endOffset": 15,
+          "startLine": 25,
+          "startOffset": 0,
+        },
+        "transitions": Array [],
+        "type": "BUG",
+      }
+    }
+    issues={
+      Array [
+        Object {
+          "actions": Array [],
+          "component": "main.js",
+          "componentLongName": "main.js",
+          "componentQualifier": "FIL",
+          "componentUuid": "foo1234",
+          "creationDate": "2017-03-01T09:36:01+0100",
+          "flows": Array [],
+          "fromHotspot": false,
+          "key": "AVsae-CQS-9G3txfbFN2",
+          "line": 25,
+          "message": "Reduce the number of conditional operators (4) used in the expression",
+          "project": "myproject",
+          "projectKey": "foo",
+          "projectName": "Foo",
+          "rule": "javascript:S1067",
+          "ruleName": "foo",
+          "secondaryLocations": Array [],
+          "severity": "MAJOR",
+          "status": "OPEN",
+          "textRange": Object {
+            "endLine": 26,
+            "endOffset": 15,
+            "startLine": 25,
+            "startOffset": 0,
+          },
+          "transitions": Array [],
+          "type": "BUG",
+        },
+      ]
+    }
+    locations={Array []}
     onIssueChange={[MockFunction]}
-    onIssueSelect={[MockFunction]}
     onLoaded={[Function]}
     onLocationSelect={[MockFunction]}
     scroll={[Function]}
-    selectedIssue="AVsae-CQS-9G3txfbFN2"
-    slimHeader={true}
   />
 </div>
 `;
 
 exports[`should render SourceViewer correctly: single secondary location 1`] = `
 <div>
-  <SourceViewer
-    aroundLine={26}
+  <CrossComponentSourceViewer
     branchLike={
       Object {
         "analysisDate": "2018-01-01",
@@ -323,12 +551,172 @@ exports[`should render SourceViewer correctly: single secondary location 1`] = `
         "name": "master",
       }
     }
-    component="main.js"
-    displayAllIssues={true}
-    displayIssueLocationsCount={true}
-    displayIssueLocationsLink={false}
-    displayLocationMarkers={false}
-    highlightedLocations={
+    issue={
+      Object {
+        "actions": Array [],
+        "component": "main.js",
+        "componentLongName": "main.js",
+        "componentQualifier": "FIL",
+        "componentUuid": "foo1234",
+        "creationDate": "2017-03-01T09:36:01+0100",
+        "flows": Array [
+          Array [
+            Object {
+              "component": "main.js",
+              "index": 0,
+              "textRange": Object {
+                "endLine": 2,
+                "endOffset": 2,
+                "startLine": 1,
+                "startOffset": 1,
+              },
+            },
+          ],
+        ],
+        "fromHotspot": false,
+        "key": "AVsae-CQS-9G3txfbFN2",
+        "line": 25,
+        "message": "Reduce the number of conditional operators (4) used in the expression",
+        "project": "myproject",
+        "projectKey": "foo",
+        "projectName": "Foo",
+        "rule": "javascript:S1067",
+        "ruleName": "foo",
+        "secondaryLocations": Array [
+          Object {
+            "component": "main.js",
+            "textRange": Object {
+              "endLine": 2,
+              "endOffset": 2,
+              "startLine": 1,
+              "startOffset": 1,
+            },
+          },
+          Object {
+            "component": "main.js",
+            "textRange": Object {
+              "endLine": 2,
+              "endOffset": 2,
+              "startLine": 1,
+              "startOffset": 1,
+            },
+          },
+        ],
+        "severity": "MAJOR",
+        "status": "OPEN",
+        "textRange": Object {
+          "endLine": 26,
+          "endOffset": 15,
+          "startLine": 25,
+          "startOffset": 0,
+        },
+        "transitions": Array [],
+        "type": "BUG",
+      }
+    }
+    issues={
+      Array [
+        Object {
+          "actions": Array [],
+          "component": "main.js",
+          "componentLongName": "main.js",
+          "componentQualifier": "FIL",
+          "componentUuid": "foo1234",
+          "creationDate": "2017-03-01T09:36:01+0100",
+          "flows": Array [
+            Array [
+              Object {
+                "component": "main.js",
+                "textRange": Object {
+                  "endLine": 2,
+                  "endOffset": 2,
+                  "startLine": 1,
+                  "startOffset": 1,
+                },
+              },
+              Object {
+                "component": "main.js",
+                "textRange": Object {
+                  "endLine": 2,
+                  "endOffset": 2,
+                  "startLine": 1,
+                  "startOffset": 1,
+                },
+              },
+              Object {
+                "component": "main.js",
+                "textRange": Object {
+                  "endLine": 2,
+                  "endOffset": 2,
+                  "startLine": 1,
+                  "startOffset": 1,
+                },
+              },
+            ],
+            Array [
+              Object {
+                "component": "main.js",
+                "textRange": Object {
+                  "endLine": 2,
+                  "endOffset": 2,
+                  "startLine": 1,
+                  "startOffset": 1,
+                },
+              },
+              Object {
+                "component": "main.js",
+                "textRange": Object {
+                  "endLine": 2,
+                  "endOffset": 2,
+                  "startLine": 1,
+                  "startOffset": 1,
+                },
+              },
+            ],
+          ],
+          "fromHotspot": false,
+          "key": "AVsae-CQS-9G3txfbFN2",
+          "line": 25,
+          "message": "Reduce the number of conditional operators (4) used in the expression",
+          "project": "myproject",
+          "projectKey": "foo",
+          "projectName": "Foo",
+          "rule": "javascript:S1067",
+          "ruleName": "foo",
+          "secondaryLocations": Array [
+            Object {
+              "component": "main.js",
+              "textRange": Object {
+                "endLine": 2,
+                "endOffset": 2,
+                "startLine": 1,
+                "startOffset": 1,
+              },
+            },
+            Object {
+              "component": "main.js",
+              "textRange": Object {
+                "endLine": 2,
+                "endOffset": 2,
+                "startLine": 1,
+                "startOffset": 1,
+              },
+            },
+          ],
+          "severity": "MAJOR",
+          "status": "OPEN",
+          "textRange": Object {
+            "endLine": 26,
+            "endOffset": 15,
+            "startLine": 25,
+            "startOffset": 0,
+          },
+          "transitions": Array [],
+          "type": "BUG",
+        },
+      ]
+    }
+    locations={
       Array [
         Object {
           "component": "main.js",
@@ -342,14 +730,10 @@ exports[`should render SourceViewer correctly: single secondary location 1`] = `
         },
       ]
     }
-    loadIssues={[MockFunction]}
     onIssueChange={[MockFunction]}
-    onIssueSelect={[MockFunction]}
     onLoaded={[Function]}
     onLocationSelect={[MockFunction]}
     scroll={[Function]}
-    selectedIssue="AVsae-CQS-9G3txfbFN2"
-    slimHeader={true}
   />
 </div>
 `;
index aad18317be300686ad1b8c640a7fd9f6f2b05fbe..d54d1f0ad95cb020ecbff96767b5e5767d913238 100644 (file)
@@ -335,6 +335,7 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone
     const issueLocationsByLine = includeIssueLocation ? locations : {};
     return (
       <LineIssuesList
+        displayWhyIsThisAnIssue={false}
         issueLocationsByLine={issueLocationsByLine}
         issuesForLine={issuesForLine}
         line={line}
index 5117ef3b3fcb89416a629aeb228dd51f800aa922..676c7701ec6a63f001bc99ab0a5eb1b2f190372c 100644 (file)
@@ -17,9 +17,9 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { findLastIndex } from 'lodash';
+import { findLastIndex, keyBy } from 'lodash';
 import * as React from 'react';
-import { getDuplications } from '../../../api/components';
+import { getComponentForSourceViewer, getDuplications, getSources } from '../../../api/components';
 import { getIssueFlowSnippets } from '../../../api/issues';
 import DuplicationPopup from '../../../components/SourceViewer/components/DuplicationPopup';
 import {
@@ -39,6 +39,7 @@ import { getBranchLikeQuery } from '../../../helpers/branch-like';
 import { throwGlobalError } from '../../../helpers/error';
 import { translate } from '../../../helpers/l10n';
 import { BranchLike } from '../../../types/branch-like';
+import { isFile } from '../../../types/component';
 import {
   Dict,
   DuplicatedFile,
@@ -49,7 +50,7 @@ import {
   SourceViewerFile
 } from '../../../types/types';
 import ComponentSourceSnippetGroupViewer from './ComponentSourceSnippetGroupViewer';
-import { groupLocationsByComponent } from './utils';
+import { getPrimaryLocation, groupLocationsByComponent } from './utils';
 
 interface Props {
   branchLike: BranchLike | undefined;
@@ -85,12 +86,12 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone
 
   componentDidMount() {
     this.mounted = true;
-    this.fetchIssueFlowSnippets(this.props.issue.key);
+    this.fetchIssueFlowSnippets();
   }
 
   componentDidUpdate(prevProps: Props) {
     if (prevProps.issue.key !== this.props.issue.key) {
-      this.fetchIssueFlowSnippets(this.props.issue.key);
+      this.fetchIssueFlowSnippets();
     }
   }
 
@@ -116,30 +117,47 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone
     );
   };
 
-  fetchIssueFlowSnippets(issueKey: string) {
+  async fetchIssueFlowSnippets() {
+    const { issue, branchLike } = this.props;
     this.setState({ loading: true });
-    getIssueFlowSnippets(issueKey).then(
-      components => {
-        if (this.mounted) {
-          this.setState({
-            components,
-            issuePopup: undefined,
-            loading: false
-          });
-          if (this.props.onLoaded) {
-            this.props.onLoaded();
-          }
-        }
-      },
-      (response: Response) => {
-        if (response.status !== 403) {
-          throwGlobalError(response);
+
+    try {
+      const components = await getIssueFlowSnippets(issue.key);
+      if (components[issue.component] === undefined) {
+        const issueComponent = await getComponentForSourceViewer({
+          component: issue.component,
+          ...getBranchLikeQuery(branchLike)
+        });
+        components[issue.component] = { component: issueComponent, sources: [] };
+        if (isFile(issueComponent.q)) {
+          const sources = await getSources({
+            key: issueComponent.key,
+            ...getBranchLikeQuery(branchLike),
+            from: 1,
+            to: 10
+          }).then(lines => keyBy(lines, 'line'));
+          components[issue.component].sources = sources;
         }
-        if (this.mounted) {
-          this.setState({ loading: false, notAccessible: response.status === 403 });
+      }
+      if (this.mounted) {
+        this.setState({
+          components,
+          issuePopup: undefined,
+          loading: false
+        });
+        if (this.props.onLoaded) {
+          this.props.onLoaded();
         }
       }
-    );
+    } catch (response) {
+      const rsp = response as Response;
+      if (rsp.status !== 403) {
+        throwGlobalError(response);
+      }
+      if (this.mounted) {
+        this.setState({ loading: false, notAccessible: rsp.status === 403 });
+      }
+    }
   }
 
   handleIssuePopupToggle = (issue: string, popupName: string, open?: boolean) => {
@@ -209,6 +227,10 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone
       ({ component }) => component.key === issue.component
     );
 
+    if (components[issue.component] === undefined) {
+      return null;
+    }
+
     return (
       <div>
         {locationsByComponent.map((snippetGroup, i) => {
@@ -239,6 +261,31 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone
             </SourceViewerContext.Provider>
           );
         })}
+
+        {locationsByComponent.length === 0 && (
+          <ComponentSourceSnippetGroupViewer
+            branchLike={this.props.branchLike}
+            duplications={duplications}
+            duplicationsByLine={duplicationsByLine}
+            highlightedLocationMessage={this.props.highlightedLocationMessage}
+            issue={issue}
+            issuePopup={this.state.issuePopup}
+            issuesByLine={issuesByComponent[issue.component] || {}}
+            isLastOccurenceOfPrimaryComponent={true}
+            lastSnippetGroup={true}
+            loadDuplications={this.fetchDuplications}
+            locations={[]}
+            onIssueChange={this.props.onIssueChange}
+            onIssuePopupToggle={this.handleIssuePopupToggle}
+            onLocationSelect={this.props.onLocationSelect}
+            renderDuplicationPopup={this.renderDuplicationPopup}
+            scroll={this.props.scroll}
+            snippetGroup={{
+              locations: [getPrimaryLocation(issue)],
+              ...components[issue.component]
+            }}
+          />
+        )}
       </div>
     );
   }
index f22bc08b354b72a61d78c55ca75d19277529b7a5..bc2e28020796c45cc7706d79a8ce1d51eafe0e42 100644 (file)
@@ -39,7 +39,8 @@ jest.mock('../../../../api/issues', () => {
 });
 
 jest.mock('../../../../api/components', () => ({
-  getDuplications: jest.fn().mockResolvedValue({})
+  getDuplications: jest.fn().mockResolvedValue({}),
+  getComponentForSourceViewer: jest.fn().mockResolvedValue({})
 }));
 
 beforeEach(() => {
@@ -47,19 +48,26 @@ beforeEach(() => {
 });
 
 it('should render correctly', async () => {
-  const wrapper = shallowRender();
+  let wrapper = shallowRender();
   expect(wrapper).toMatchSnapshot();
 
   await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
+
+  wrapper = shallowRender({ issue: mockIssue(true, { component: 'test.js', key: 'unknown' }) });
+  await waitAndUpdate(wrapper);
+
+  expect(wrapper).toMatchSnapshot('no component found');
 });
 
 it('Should fetch data', async () => {
   const wrapper = shallowRender();
-  wrapper.instance().fetchIssueFlowSnippets('124');
+  wrapper.instance().fetchIssueFlowSnippets();
   await waitAndUpdate(wrapper);
   expect(getIssueFlowSnippets).toHaveBeenCalledWith('1');
-  expect(wrapper.state('components')).toEqual({ 'main.js': mockSnippetsByComponent() });
+  expect(wrapper.state('components')).toEqual(
+    expect.objectContaining({ 'main.js': mockSnippetsByComponent() })
+  );
 
   (getIssueFlowSnippets as jest.Mock).mockClear();
   wrapper.setProps({ issue: mockIssue(true, { key: 'foo' }) });
index 4d100ae4cf35af9ff7cb0b1ffcad4d0d59dd0302..c98ca92f56fa0d6a79807b59deed61a46acba9da 100644 (file)
@@ -49,6 +49,7 @@ exports[`should render correctly line with issue 1`] = `
         "name": "master",
       }
     }
+    displayWhyIsThisAnIssue={false}
     issue={
       Object {
         "actions": Array [],
index 06624417ee02b6bb70f7477d9e40085a6552457c..1c7210b016865fd489d72b1663913ebf0e9f4918 100644 (file)
@@ -217,3 +217,330 @@ exports[`should render correctly 2`] = `
   </ContextProvider>
 </div>
 `;
+
+exports[`should render correctly: no component found 1`] = `
+<div>
+  <ContextProvider
+    key="unknown-0-0"
+    value={
+      Object {
+        "branchLike": undefined,
+        "file": Object {},
+      }
+    }
+  >
+    <ComponentSourceSnippetGroupViewer
+      duplicationsByLine={Object {}}
+      isLastOccurenceOfPrimaryComponent={false}
+      issue={
+        Object {
+          "actions": Array [],
+          "component": "test.js",
+          "componentLongName": "main.js",
+          "componentQualifier": "FIL",
+          "componentUuid": "foo1234",
+          "creationDate": "2017-03-01T09:36:01+0100",
+          "flows": Array [
+            Array [
+              Object {
+                "component": "main.js",
+                "textRange": Object {
+                  "endLine": 2,
+                  "endOffset": 2,
+                  "startLine": 1,
+                  "startOffset": 1,
+                },
+              },
+              Object {
+                "component": "main.js",
+                "textRange": Object {
+                  "endLine": 2,
+                  "endOffset": 2,
+                  "startLine": 1,
+                  "startOffset": 1,
+                },
+              },
+              Object {
+                "component": "main.js",
+                "textRange": Object {
+                  "endLine": 2,
+                  "endOffset": 2,
+                  "startLine": 1,
+                  "startOffset": 1,
+                },
+              },
+            ],
+            Array [
+              Object {
+                "component": "main.js",
+                "textRange": Object {
+                  "endLine": 2,
+                  "endOffset": 2,
+                  "startLine": 1,
+                  "startOffset": 1,
+                },
+              },
+              Object {
+                "component": "main.js",
+                "textRange": Object {
+                  "endLine": 2,
+                  "endOffset": 2,
+                  "startLine": 1,
+                  "startOffset": 1,
+                },
+              },
+            ],
+          ],
+          "fromHotspot": false,
+          "key": "unknown",
+          "line": 25,
+          "message": "Reduce the number of conditional operators (4) used in the expression",
+          "project": "myproject",
+          "projectKey": "foo",
+          "projectName": "Foo",
+          "rule": "javascript:S1067",
+          "ruleName": "foo",
+          "secondaryLocations": Array [
+            Object {
+              "component": "main.js",
+              "textRange": Object {
+                "endLine": 2,
+                "endOffset": 2,
+                "startLine": 1,
+                "startOffset": 1,
+              },
+            },
+            Object {
+              "component": "main.js",
+              "textRange": Object {
+                "endLine": 2,
+                "endOffset": 2,
+                "startLine": 1,
+                "startOffset": 1,
+              },
+            },
+          ],
+          "severity": "MAJOR",
+          "status": "OPEN",
+          "textRange": Object {
+            "endLine": 26,
+            "endOffset": 15,
+            "startLine": 25,
+            "startOffset": 0,
+          },
+          "transitions": Array [],
+          "type": "BUG",
+        }
+      }
+      issuesByLine={Object {}}
+      lastSnippetGroup={false}
+      loadDuplications={[Function]}
+      locations={Array []}
+      onIssueChange={[MockFunction]}
+      onIssuePopupToggle={[Function]}
+      onLocationSelect={[MockFunction]}
+      renderDuplicationPopup={[Function]}
+      scroll={[MockFunction]}
+      snippetGroup={
+        Object {
+          "component": Object {},
+          "locations": Array [],
+          "sources": Array [],
+        }
+      }
+    />
+  </ContextProvider>
+  <ContextProvider
+    key="unknown-0-1"
+    value={
+      Object {
+        "branchLike": undefined,
+        "file": Object {
+          "key": "main.js",
+          "measures": Object {
+            "coverage": "85.2",
+            "duplicationDensity": "1.0",
+            "issues": "12",
+            "lines": "56",
+          },
+          "path": "main.js",
+          "project": "my-project",
+          "projectName": "MyProject",
+          "q": "FIL",
+          "uuid": "foo-bar",
+        },
+      }
+    }
+  >
+    <ComponentSourceSnippetGroupViewer
+      duplicationsByLine={Object {}}
+      isLastOccurenceOfPrimaryComponent={false}
+      issue={
+        Object {
+          "actions": Array [],
+          "component": "test.js",
+          "componentLongName": "main.js",
+          "componentQualifier": "FIL",
+          "componentUuid": "foo1234",
+          "creationDate": "2017-03-01T09:36:01+0100",
+          "flows": Array [
+            Array [
+              Object {
+                "component": "main.js",
+                "textRange": Object {
+                  "endLine": 2,
+                  "endOffset": 2,
+                  "startLine": 1,
+                  "startOffset": 1,
+                },
+              },
+              Object {
+                "component": "main.js",
+                "textRange": Object {
+                  "endLine": 2,
+                  "endOffset": 2,
+                  "startLine": 1,
+                  "startOffset": 1,
+                },
+              },
+              Object {
+                "component": "main.js",
+                "textRange": Object {
+                  "endLine": 2,
+                  "endOffset": 2,
+                  "startLine": 1,
+                  "startOffset": 1,
+                },
+              },
+            ],
+            Array [
+              Object {
+                "component": "main.js",
+                "textRange": Object {
+                  "endLine": 2,
+                  "endOffset": 2,
+                  "startLine": 1,
+                  "startOffset": 1,
+                },
+              },
+              Object {
+                "component": "main.js",
+                "textRange": Object {
+                  "endLine": 2,
+                  "endOffset": 2,
+                  "startLine": 1,
+                  "startOffset": 1,
+                },
+              },
+            ],
+          ],
+          "fromHotspot": false,
+          "key": "unknown",
+          "line": 25,
+          "message": "Reduce the number of conditional operators (4) used in the expression",
+          "project": "myproject",
+          "projectKey": "foo",
+          "projectName": "Foo",
+          "rule": "javascript:S1067",
+          "ruleName": "foo",
+          "secondaryLocations": Array [
+            Object {
+              "component": "main.js",
+              "textRange": Object {
+                "endLine": 2,
+                "endOffset": 2,
+                "startLine": 1,
+                "startOffset": 1,
+              },
+            },
+            Object {
+              "component": "main.js",
+              "textRange": Object {
+                "endLine": 2,
+                "endOffset": 2,
+                "startLine": 1,
+                "startOffset": 1,
+              },
+            },
+          ],
+          "severity": "MAJOR",
+          "status": "OPEN",
+          "textRange": Object {
+            "endLine": 26,
+            "endOffset": 15,
+            "startLine": 25,
+            "startOffset": 0,
+          },
+          "transitions": Array [],
+          "type": "BUG",
+        }
+      }
+      issuesByLine={Object {}}
+      lastSnippetGroup={true}
+      loadDuplications={[Function]}
+      locations={
+        Array [
+          Object {
+            "component": "main.js",
+            "index": 0,
+            "textRange": Object {
+              "endLine": 2,
+              "endOffset": 2,
+              "startLine": 1,
+              "startOffset": 1,
+            },
+          },
+        ]
+      }
+      onIssueChange={[MockFunction]}
+      onIssuePopupToggle={[Function]}
+      onLocationSelect={[MockFunction]}
+      renderDuplicationPopup={[Function]}
+      scroll={[MockFunction]}
+      snippetGroup={
+        Object {
+          "component": Object {
+            "key": "main.js",
+            "measures": Object {
+              "coverage": "85.2",
+              "duplicationDensity": "1.0",
+              "issues": "12",
+              "lines": "56",
+            },
+            "path": "main.js",
+            "project": "my-project",
+            "projectName": "MyProject",
+            "q": "FIL",
+            "uuid": "foo-bar",
+          },
+          "locations": Array [
+            Object {
+              "component": "main.js",
+              "index": 0,
+              "textRange": Object {
+                "endLine": 2,
+                "endOffset": 2,
+                "startLine": 1,
+                "startOffset": 1,
+              },
+            },
+          ],
+          "sources": Object {
+            "16": Object {
+              "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
+              "coverageStatus": "covered",
+              "coveredConditions": 2,
+              "duplicated": false,
+              "isNew": true,
+              "line": 16,
+              "scmAuthor": "simon.brandhof@sonarsource.com",
+              "scmDate": "2018-12-11T10:48:39+0100",
+              "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+            },
+          },
+        }
+      }
+    />
+  </ContextProvider>
+</div>
+`;
index 69409b43cdcc68b297496273e3b8d235ea5cdb8e..d41b61cbe0d7b94488a20a76cb0bd568a357e0fd 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { ComponentQualifier } from '../../../types/component';
 import {
   ExpandDirection,
   FlowLocation,
@@ -42,7 +43,7 @@ function unknownComponent(key: string): SnippetsByComponent {
       path: '',
       project: '',
       projectName: '',
-      q: 'FIL',
+      q: ComponentQualifier.File,
       uuid: ''
     },
     sources: []
index c591c404cdee7e314fc908df3b13f3c101661f5a..2d681daf14fd79cef24c455f66cfd6aecb1db684 100644 (file)
@@ -189,6 +189,7 @@ export default class SourceViewerCode extends React.PureComponent<Props> {
         scrollToUncoveredLine={scrollToUncoveredLine}
         secondaryIssueLocations={secondaryIssueLocations}>
         <LineIssuesList
+          displayWhyIsThisAnIssue={true}
           displayAllIssues={displayAllIssues}
           issueLocationsByLine={issueLocationsByLine}
           issuesForLine={issuesForLine}
index fa97e74aff4057543262d8ffcc73803d4c4fa3e8..2713a1e0ea4b7d2c6148527dda0d3640e7543c70 100644 (file)
@@ -21,6 +21,7 @@ import { shallow } from 'enzyme';
 import * as React from 'react';
 import { mockMainBranch } from '../../../helpers/mocks/branch-like';
 import { mockSourceViewerFile } from '../../../helpers/testMocks';
+import { ComponentQualifier } from '../../../types/component';
 import { MetricKey } from '../../../types/metrics';
 import { Measure } from '../../../types/types';
 import SourceViewerHeader from '../SourceViewerHeader';
@@ -33,7 +34,10 @@ it('should render correctly for a unit test', () => {
   expect(
     shallowRender({
       showMeasures: true,
-      sourceViewerFile: mockSourceViewerFile({ q: 'UTS', measures: { tests: '12' } })
+      sourceViewerFile: mockSourceViewerFile({
+        q: ComponentQualifier.TestFile,
+        measures: { tests: '12' }
+      })
     })
   ).toMatchSnapshot();
 });
index 5584d032e4774c7c2cbaaf19d1cd0faf818c1191..3bd7852163ba69b7655d6d3206469c69653f6e9a 100644 (file)
@@ -25,6 +25,7 @@ import Issue from '../../issue/Issue';
 export interface LineIssuesListProps {
   branchLike: BranchLike | undefined;
   displayAllIssues?: boolean;
+  displayWhyIsThisAnIssue: boolean;
   displayIssueLocationsCount?: boolean;
   displayIssueLocationsLink?: boolean;
   issuesForLine: TypeIssue[];
@@ -41,6 +42,7 @@ export interface LineIssuesListProps {
 export default function LineIssuesList(props: LineIssuesListProps) {
   const {
     line,
+    displayWhyIsThisAnIssue,
     displayAllIssues,
     openIssuesByLine,
     selectedIssue,
@@ -66,6 +68,7 @@ export default function LineIssuesList(props: LineIssuesListProps) {
       {displayedIssue.map(issue => (
         <Issue
           branchLike={props.branchLike}
+          displayWhyIsThisAnIssue={displayWhyIsThisAnIssue}
           displayLocationsCount={props.displayIssueLocationsCount}
           displayLocationsLink={props.displayIssueLocationsLink}
           issue={issue}
index 70ec54f61d289d63dadc9339c920107965696cc5..8d803bbb1728b1ca57999ad68b2f4a6448b100fc 100644 (file)
@@ -23,7 +23,7 @@ import { mockBranch } from '../../../../helpers/mocks/branch-like';
 import { mockIssue, mockSourceLine } from '../../../../helpers/testMocks';
 import LineIssuesList, { LineIssuesListProps } from '../LineIssuesList';
 
-it('shoule render issues', () => {
+it('should render issues', () => {
   const wrapper = shallowRender({
     selectedIssue: 'issue',
     issueLocationsByLine: { '1': [{ from: 1, to: 1, line: 1 }] },
@@ -37,6 +37,7 @@ function shallowRender(props: Partial<LineIssuesListProps> = {}) {
   return shallow(
     <LineIssuesList
       selectedIssue=""
+      displayWhyIsThisAnIssue={true}
       onIssueChange={jest.fn()}
       onIssueClick={jest.fn()}
       onIssuePopupToggle={jest.fn()}
index 11e5d2a72a918c91cd09a314273215d09d9255e4..f506a276f0bc1ffbae7c40da6a783f0f2029d7bc 100644 (file)
@@ -21,6 +21,7 @@ import { shallow } from 'enzyme';
 import * as React from 'react';
 import { mockBranch } from '../../../../helpers/mocks/branch-like';
 import { click, waitAndUpdate } from '../../../../helpers/testUtils';
+import { ComponentQualifier } from '../../../../types/component';
 import { MetricKey } from '../../../../types/metrics';
 import { SourceViewerFile } from '../../../../types/types';
 import MeasuresOverlay from '../MeasuresOverlay';
@@ -146,7 +147,7 @@ const sourceViewerFile: SourceViewerFile = {
   path: 'src/file.js',
   project: 'project-key',
   projectName: 'Project Name',
-  q: 'FIL',
+  q: ComponentQualifier.File,
   subProject: 'sub-project-key',
   subProjectName: 'Sub-Project Name',
   uuid: 'abcd123'
@@ -164,7 +165,9 @@ it('should render source file', async () => {
 });
 
 it('should render test file', async () => {
-  const wrapper = shallowRender({ sourceViewerFile: { ...sourceViewerFile, q: 'UTS' } });
+  const wrapper = shallowRender({
+    sourceViewerFile: { ...sourceViewerFile, q: ComponentQualifier.TestFile }
+  });
   await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
 });
index eef65511cda31bb32f8f3d8c2c7028dd1cd26df2..4a7b0a9eff59458e6a6e7e10e28bbae15a790c80 100644 (file)
@@ -1,6 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`shoule render issues 1`] = `
+exports[`should render issues 1`] = `
 <div
   className="issue-list"
 >
@@ -13,6 +13,7 @@ exports[`shoule render issues 1`] = `
         "name": "branch-6.7",
       }
     }
+    displayWhyIsThisAnIssue={true}
     issue={
       Object {
         "actions": Array [],
index c06a7d5d694488828d03c0df769a2ca71c4df52a..76de0d88fa3f7d21938c854334e58074bf7c7ae1 100644 (file)
@@ -29,6 +29,7 @@ import IssueView from './IssueView';
 interface Props {
   branchLike?: BranchLike;
   checked?: boolean;
+  displayWhyIsThisAnIssue?: boolean;
   displayLocationsCount?: boolean;
   displayLocationsLink?: boolean;
   issue: TypeIssue;
@@ -116,6 +117,7 @@ export default class Issue extends React.PureComponent<Props> {
         branchLike={this.props.branchLike}
         checked={this.props.checked}
         currentPopup={this.props.openPopup}
+        displayWhyIsThisAnIssue={this.props.displayWhyIsThisAnIssue}
         displayLocationsCount={this.props.displayLocationsCount}
         displayLocationsLink={this.props.displayLocationsLink}
         issue={this.props.issue}
index e42903a8c15d10aac4f4af1be2173feefb285ae6..919237c89c8a3bc1c432c18260a75e5671e82289 100644 (file)
@@ -33,6 +33,7 @@ interface Props {
   branchLike?: BranchLike;
   checked?: boolean;
   currentPopup?: string;
+  displayWhyIsThisAnIssue?: boolean;
   displayLocationsCount?: boolean;
   displayLocationsLink?: boolean;
   issue: Issue;
@@ -68,7 +69,15 @@ export default class IssueView extends React.PureComponent<Props> {
   };
 
   render() {
-    const { issue } = this.props;
+    const {
+      issue,
+      branchLike,
+      checked,
+      currentPopup,
+      displayWhyIsThisAnIssue,
+      displayLocationsLink,
+      displayLocationsCount
+    } = this.props;
 
     const hasCheckbox = this.props.onCheck != null;
 
@@ -86,16 +95,17 @@ export default class IssueView extends React.PureComponent<Props> {
         role="region"
         aria-label={issue.message}>
         <IssueTitleBar
-          branchLike={this.props.branchLike}
-          currentPopup={this.props.currentPopup}
-          displayLocationsCount={this.props.displayLocationsCount}
-          displayLocationsLink={this.props.displayLocationsLink}
+          branchLike={branchLike}
+          currentPopup={currentPopup}
+          displayLocationsCount={displayLocationsCount}
+          displayLocationsLink={displayLocationsLink}
+          displayWhyIsThisAnIssue={displayWhyIsThisAnIssue}
           issue={issue}
           onFilter={this.props.onFilter}
           togglePopup={this.props.togglePopup}
         />
         <IssueActionsBar
-          currentPopup={this.props.currentPopup}
+          currentPopup={currentPopup}
           issue={issue}
           onAssign={this.props.onAssign}
           onChange={this.props.onChange}
@@ -115,7 +125,7 @@ export default class IssueView extends React.PureComponent<Props> {
         )}
         {hasCheckbox && (
           <Checkbox
-            checked={this.props.checked || false}
+            checked={checked || false}
             className="issue-checkbox-container"
             onCheck={this.handleCheck}
             title={translate('issues.action_select')}
index 33d4e8316b7e4e3f5bb30b963805c1e59f3e32dc..2778241253b171cacae4974d686c220e1954e6e9 100644 (file)
@@ -25,15 +25,14 @@ import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { RuleStatus } from '../../../types/rules';
 import DocumentationTooltip from '../../common/DocumentationTooltip';
 import SonarLintIcon from '../../icons/SonarLintIcon';
-import { WorkspaceContextShape } from '../../workspace/context';
+import { WorkspaceContext } from '../../workspace/context';
 
 export interface IssueMessageProps {
   engine?: string;
-  engineName?: string;
   quickFixAvailable?: boolean;
+  displayWhyIsThisAnIssue?: boolean;
   manualVulnerability: boolean;
   message: string;
-  onOpenRule: WorkspaceContextShape['openRule'];
   ruleKey: string;
   ruleStatus?: RuleStatus;
 }
@@ -41,14 +40,16 @@ export interface IssueMessageProps {
 export default function IssueMessage(props: IssueMessageProps) {
   const {
     engine,
-    engineName,
     quickFixAvailable,
     manualVulnerability,
     message,
     ruleKey,
-    ruleStatus
+    ruleStatus,
+    displayWhyIsThisAnIssue
   } = props;
-  const ruleEngine = engineName ? engineName : engine;
+
+  const { externalRulesRepoNames, openRule } = React.useContext(WorkspaceContext);
+  const ruleEngine = (engine && externalRulesRepoNames && externalRulesRepoNames[engine]) || engine;
 
   return (
     <>
@@ -106,16 +107,18 @@ export default function IssueMessage(props: IssueMessageProps) {
           </Tooltip>
         )}
       </div>
-      <ButtonLink
-        aria-label={translate('issue.why_this_issue.long')}
-        className="issue-see-rule spacer-right text-baseline"
-        onClick={() =>
-          props.onOpenRule({
-            key: ruleKey
-          })
-        }>
-        {translate('issue.why_this_issue')}
-      </ButtonLink>
+      {displayWhyIsThisAnIssue && (
+        <ButtonLink
+          aria-label={translate('issue.why_this_issue.long')}
+          className="issue-see-rule spacer-right text-baseline"
+          onClick={() =>
+            openRule({
+              key: ruleKey
+            })
+          }>
+          {translate('issue.why_this_issue')}
+        </ButtonLink>
+      )}
     </>
   );
 }
index 82fd5adc024c3efc03326637fe4607340cba93dc..b010d31c68893e84d3a5e3ff4ebb3ff37f77bfe4 100644 (file)
@@ -29,7 +29,6 @@ import { BranchLike } from '../../../types/branch-like';
 import { RuleStatus } from '../../../types/rules';
 import { Issue } from '../../../types/types';
 import LocationIndex from '../../common/LocationIndex';
-import { WorkspaceContext } from '../../workspace/context';
 import IssueChangelog from './IssueChangelog';
 import IssueMessage from './IssueMessage';
 import SimilarIssuesFilter from './SimilarIssuesFilter';
@@ -37,6 +36,7 @@ import SimilarIssuesFilter from './SimilarIssuesFilter';
 export interface IssueTitleBarProps {
   branchLike?: BranchLike;
   currentPopup?: string;
+  displayWhyIsThisAnIssue?: boolean;
   displayLocationsCount?: boolean;
   displayLocationsLink?: boolean;
   issue: Issue;
@@ -45,7 +45,7 @@ export interface IssueTitleBarProps {
 }
 
 export default function IssueTitleBar(props: IssueTitleBarProps) {
-  const { issue } = props;
+  const { issue, displayWhyIsThisAnIssue } = props;
   const hasSimilarIssuesFilter = props.onFilter != null;
 
   const locationsCount =
@@ -73,25 +73,15 @@ export default function IssueTitleBar(props: IssueTitleBarProps) {
 
   return (
     <div className="issue-row">
-      <WorkspaceContext.Consumer>
-        {({ externalRulesRepoNames, openRule }) => (
-          <IssueMessage
-            engine={issue.externalRuleEngine}
-            engineName={
-              issue.externalRuleEngine &&
-              externalRulesRepoNames &&
-              externalRulesRepoNames[issue.externalRuleEngine]
-            }
-            quickFixAvailable={issue.quickFixAvailable}
-            manualVulnerability={issue.fromHotspot && issue.type === 'VULNERABILITY'}
-            message={issue.message}
-            onOpenRule={openRule}
-            ruleKey={issue.rule}
-            ruleStatus={issue.ruleStatus as RuleStatus | undefined}
-          />
-        )}
-      </WorkspaceContext.Consumer>
-
+      <IssueMessage
+        engine={issue.externalRuleEngine}
+        quickFixAvailable={issue.quickFixAvailable}
+        displayWhyIsThisAnIssue={displayWhyIsThisAnIssue}
+        manualVulnerability={issue.fromHotspot && issue.type === 'VULNERABILITY'}
+        message={issue.message}
+        ruleKey={issue.rule}
+        ruleStatus={issue.ruleStatus as RuleStatus | undefined}
+      />
       <div className="issue-row-meta">
         <div className="issue-meta-list">
           <div className="issue-meta">
index 13b5bf2b67713e47a6f2e5a7adac74962e98f9c1..c11c314aaa18dde056bc441ae03f16a9711a1698 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import { ButtonLink } from '../../../../components/controls/buttons';
-import { click } from '../../../../helpers/testUtils';
 import { RuleStatus } from '../../../../types/rules';
+import { ButtonLink } from '../../../controls/buttons';
 import IssueMessage, { IssueMessageProps } from '../IssueMessage';
 
+jest.mock('react', () => {
+  return {
+    ...jest.requireActual('react'),
+    useContext: jest
+      .fn()
+      .mockImplementation(() => ({ externalRulesRepoNames: {}, openRule: jest.fn() }))
+  };
+});
+
 it('should render correctly', () => {
   expect(shallowRender()).toMatchSnapshot('default');
   expect(shallowRender({ engine: 'js' })).toMatchSnapshot('with engine info');
   expect(shallowRender({ quickFixAvailable: true })).toMatchSnapshot('with quick fix');
-  expect(shallowRender({ engineName: 'JS' })).toMatchSnapshot('with engine name');
   expect(shallowRender({ manualVulnerability: true })).toMatchSnapshot('is manual vulnerability');
   expect(shallowRender({ ruleStatus: RuleStatus.Deprecated })).toMatchSnapshot(
     'is deprecated rule'
   );
   expect(shallowRender({ ruleStatus: RuleStatus.Removed })).toMatchSnapshot('is removed rule');
+  expect(shallowRender({ displayWhyIsThisAnIssue: false })).toMatchSnapshot(
+    'hide why is it an issue'
+  );
 });
 
-it('should handle click correctly', () => {
-  const onOpenRule = jest.fn();
-  const wrapper = shallowRender({ onOpenRule });
-  click(wrapper.find(ButtonLink));
-  expect(onOpenRule).toBeCalledWith({
-    key: 'javascript:S1067'
-  });
+it('should open why is this an issue workspace', () => {
+  const openRule = jest.fn();
+  (React.useContext as jest.Mock).mockImplementationOnce(() => ({
+    externalRulesRepoNames: {},
+    openRule
+  }));
+  const wrapper = shallowRender();
+  wrapper.find(ButtonLink).simulate('click');
+
+  expect(openRule).toBeCalled();
 });
 
 function shallowRender(props: Partial<IssueMessageProps> = {}) {
@@ -50,7 +63,7 @@ function shallowRender(props: Partial<IssueMessageProps> = {}) {
     <IssueMessage
       manualVulnerability={false}
       message="Reduce the number of conditional operators (4) used in the expression"
-      onOpenRule={jest.fn()}
+      displayWhyIsThisAnIssue={true}
       ruleKey="javascript:S1067"
       {...props}
     />
index 29d1882b8f5cf3cd2418498eb6627dc88f4bf55c..7178fa1a17a24afc6f83cd37f8b9837b3031c6a1 100644 (file)
@@ -21,7 +21,6 @@ import { shallow } from 'enzyme';
 import * as React from 'react';
 import { mockBranch } from '../../../../helpers/mocks/branch-like';
 import { mockIssue } from '../../../../helpers/testMocks';
-import { WorkspaceContext } from '../../../workspace/context';
 import IssueTitleBar, { IssueTitleBarProps } from '../IssueTitleBar';
 
 it('should render correctly', () => {
@@ -38,11 +37,6 @@ it('should render correctly', () => {
       issue: mockIssue(true)
     })
   ).toMatchSnapshot('with multi locations and link');
-  expect(
-    shallowRender()
-      .find(WorkspaceContext.Consumer)
-      .dive()
-  ).toMatchSnapshot('issue message');
 });
 
 function shallowRender(props: Partial<IssueTitleBarProps> = {}) {
index ad368c1d951cceb4861f171c6594d061d047ae48..efae668c86c691e7a152f629bfb8ae5a06b20bc1 100644 (file)
@@ -21,6 +21,20 @@ exports[`should render correctly: default 1`] = `
 </Fragment>
 `;
 
+exports[`should render correctly: hide why is it an issue 1`] = `
+<Fragment>
+  <div
+    className="display-inline-flex-center issue-message break-word"
+  >
+    <span
+      className="spacer-right"
+    >
+      Reduce the number of conditional operators (4) used in the expression
+    </span>
+  </div>
+</Fragment>
+`;
+
 exports[`should render correctly: is deprecated rule 1`] = `
 <Fragment>
   <div
@@ -159,36 +173,6 @@ exports[`should render correctly: with engine info 1`] = `
 </Fragment>
 `;
 
-exports[`should render correctly: with engine name 1`] = `
-<Fragment>
-  <div
-    className="display-inline-flex-center issue-message break-word"
-  >
-    <span
-      className="spacer-right"
-    >
-      Reduce the number of conditional operators (4) used in the expression
-    </span>
-    <Tooltip
-      overlay="issue.from_external_rule_engine.JS"
-    >
-      <div
-        className="badge spacer-right text-baseline"
-      >
-        JS
-      </div>
-    </Tooltip>
-  </div>
-  <ButtonLink
-    aria-label="issue.why_this_issue.long"
-    className="issue-see-rule spacer-right text-baseline"
-    onClick={[Function]}
-  >
-    issue.why_this_issue
-  </ButtonLink>
-</Fragment>
-`;
-
 exports[`should render correctly: with quick fix 1`] = `
 <Fragment>
   <div
index 3301b5c37636a2afdaf8dfa7c91f120f70291bd2..8b2455b7c457a0d1bb1bc23cc004926b49b49f05 100644 (file)
@@ -4,9 +4,12 @@ exports[`should render correctly: default 1`] = `
 <div
   className="issue-row"
 >
-  <ContextConsumer>
-    <Component />
-  </ContextConsumer>
+  <IssueMessage
+    engine="foo"
+    manualVulnerability={false}
+    message="Reduce the number of conditional operators (4) used in the expression"
+    ruleKey="javascript:S1067"
+  />
   <div
     className="issue-row-meta"
   >
@@ -94,23 +97,16 @@ exports[`should render correctly: default 1`] = `
 </div>
 `;
 
-exports[`should render correctly: issue message 1`] = `
-<IssueMessage
-  engine="foo"
-  manualVulnerability={false}
-  message="Reduce the number of conditional operators (4) used in the expression"
-  onOpenRule={[Function]}
-  ruleKey="javascript:S1067"
-/>
-`;
-
 exports[`should render correctly: with filter 1`] = `
 <div
   className="issue-row"
 >
-  <ContextConsumer>
-    <Component />
-  </ContextConsumer>
+  <IssueMessage
+    engine="foo"
+    manualVulnerability={false}
+    message="Reduce the number of conditional operators (4) used in the expression"
+    ruleKey="javascript:S1067"
+  />
   <div
     className="issue-row-meta"
   >
@@ -243,9 +239,11 @@ exports[`should render correctly: with multi locations 1`] = `
 <div
   className="issue-row"
 >
-  <ContextConsumer>
-    <Component />
-  </ContextConsumer>
+  <IssueMessage
+    manualVulnerability={false}
+    message="Reduce the number of conditional operators (4) used in the expression"
+    ruleKey="javascript:S1067"
+  />
   <div
     className="issue-row-meta"
   >
@@ -416,9 +414,11 @@ exports[`should render correctly: with multi locations and link 1`] = `
 <div
   className="issue-row"
 >
-  <ContextConsumer>
-    <Component />
-  </ContextConsumer>
+  <IssueMessage
+    manualVulnerability={false}
+    message="Reduce the number of conditional operators (4) used in the expression"
+    ruleKey="javascript:S1067"
+  />
   <div
     className="issue-row-meta"
   >
index ac1644791f7356a8bd2d7aa7d56cd8c062ac18eb..569ea04e6108872c25bdfae4beae42bf196d8792 100644 (file)
@@ -323,6 +323,7 @@ export function isSuccessStatus(status: number) {
 export enum HttpStatus {
   Ok = 200,
   Created = 201,
+  NoContent = 204,
   MultipleChoices = 300,
   MovedPermanently = 301,
   Found = 302,
index 4cc1426456a0e2c1acf5b1d34e2ebfdce65df3dc..dd4270c4c43b38bf03c319facbabbf457aa430d1 100644 (file)
@@ -23,6 +23,7 @@ import { DocumentationEntry } from '../apps/documentation/utils';
 import { Exporter, Profile } from '../apps/quality-profiles/types';
 import { AppState } from '../types/appstate';
 import { RuleRepository } from '../types/coding-rules';
+import { ComponentQualifier } from '../types/component';
 import { EditionKey } from '../types/editions';
 import { RawIssue } from '../types/issues';
 import { Language } from '../types/languages';
@@ -659,7 +660,7 @@ export function mockSourceViewerFile(overrides: Partial<SourceViewerFile> = {}):
     path: 'foo/bar.ts',
     project: 'my-project',
     projectName: 'MyProject',
-    q: 'FIL',
+    q: ComponentQualifier.File,
     uuid: 'foo-bar',
     ...overrides
   };
index 890237e2040b069180f0ba0e842a7fc8be5583aa..6d85754086496ec747db94b24e9670ec7bc17feb 100644 (file)
@@ -18,6 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
+import { ComponentQualifier } from './component';
 import { UserActive, UserBase } from './users';
 
 export type Dict<T> = { [key: string]: T };
@@ -676,7 +677,7 @@ export interface SourceViewerFile {
   path: string;
   project: string;
   projectName: string;
-  q: string;
+  q: ComponentQualifier;
   subProject?: string;
   subProjectName?: string;
   uuid: string;