]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19174 Code viewer changes for hotspots page
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>
Thu, 25 May 2023 13:40:11 +0000 (15:40 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 1 Jun 2023 20:02:59 +0000 (20:02 +0000)
server/sonar-web/design-system/src/components/IssueLocationMarker.tsx [deleted file]
server/sonar-web/design-system/src/components/__tests__/LineFinding-test.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/__tests__/__snapshots__/LineFinding-test.tsx.snap [new file with mode: 0644]
server/sonar-web/design-system/src/components/code-line/LineFinding.tsx
server/sonar-web/design-system/src/components/code-line/LineMarker.tsx
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotPrimaryLocationBox.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx

diff --git a/server/sonar-web/design-system/src/components/IssueLocationMarker.tsx b/server/sonar-web/design-system/src/components/IssueLocationMarker.tsx
deleted file mode 100644 (file)
index f9886fd..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import styled from '@emotion/styled';
-import classNames from 'classnames';
-import { forwardRef, LegacyRef } from 'react';
-import tw from 'twin.macro';
-import { themeColor, themeContrast } from '../helpers/theme';
-import { isDefined } from '../helpers/types';
-import { IssueLocationIcon } from './icons/IssueLocationIcon';
-
-interface Props {
-  className?: string;
-  onClick?: () => void;
-  selected: boolean;
-  text?: number | string;
-}
-
-function IssueLocationMarkerFunc(
-  { className, onClick, text, selected }: Props,
-  ref: LegacyRef<HTMLElement>
-) {
-  return (
-    <Marker
-      className={classNames(className, {
-        selected,
-        concealed: !isDefined(text),
-        'sw-cursor-pointer': isDefined(onClick),
-      })}
-      onClick={onClick}
-      ref={ref}
-    >
-      {isDefined(text) ? text : <IssueLocationIcon />}
-    </Marker>
-  );
-}
-
-export const IssueLocationMarker = forwardRef<HTMLElement, Props>(IssueLocationMarkerFunc);
-
-export const Marker = styled.span`
-  ${tw`sw-flex sw-grow-0 sw-items-center sw-justify-center`}
-  ${tw`sw-body-sm-highlight`}
-  ${tw`sw-rounded-1/2`}
-
-  height: 1.125rem;
-  color: ${themeContrast('codeLineLocationMarker')};
-  background-color: ${themeColor('codeLineLocationMarker')};
-
-  &.selected,
-  &:hover {
-    background-color: ${themeColor('codeLineLocationMarkerSelected')};
-  }
-
-  &:not(.concealed) {
-    ${tw`sw-px-1`}
-    ${tw`sw-self-start`}
-  }
-`;
diff --git a/server/sonar-web/design-system/src/components/__tests__/LineFinding-test.tsx b/server/sonar-web/design-system/src/components/__tests__/LineFinding-test.tsx
new file mode 100644 (file)
index 0000000..30d470d
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { render } from '../../helpers/testUtils';
+import { FCProps } from '../../types/misc';
+import { LineFinding } from '../code-line/LineFinding';
+
+it('should render correctly', async () => {
+  const user = userEvent.setup();
+  const { container } = setupWithProps();
+  await user.click(screen.getByRole('button'));
+  expect(container).toMatchSnapshot();
+});
+
+it('should render correctly when issueType is provided', () => {
+  const { container } = setupWithProps({ issueType: 'bugs' });
+  expect(container).toMatchSnapshot();
+});
+
+it('should be clickable when onIssueSelect is provided', async () => {
+  const mockClick = jest.fn();
+  const user = userEvent.setup();
+
+  setupWithProps({ issueType: 'bugs', onIssueSelect: mockClick });
+  await user.click(screen.getByRole('button'));
+  expect(mockClick).toHaveBeenCalled();
+});
+
+function setupWithProps(props?: Partial<FCProps<typeof LineFinding>>) {
+  return render(
+    <LineFinding issueKey="key" message="message" onIssueSelect={jest.fn()} {...props} />
+  );
+}
diff --git a/server/sonar-web/design-system/src/components/__tests__/__snapshots__/LineFinding-test.tsx.snap b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/LineFinding-test.tsx.snap
new file mode 100644 (file)
index 0000000..b72c648
--- /dev/null
@@ -0,0 +1,146 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+.emotion-0 {
+  all: unset;
+  cursor: pointer;
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-align-items: center;
+  -webkit-box-align: center;
+  -ms-flex-align: center;
+  align-items: center;
+  gap: 0.5rem;
+  margin-left: 0.25rem;
+  margin-right: 0.25rem;
+  margin-top: 0.75rem;
+  margin-bottom: 0.75rem;
+  padding: 0.75rem;
+  font-family: Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
+  font-size: 1rem;
+  line-height: 1.5rem;
+  font-weight: 600;
+  border-radius: 0.25rem;
+  width: 100%;
+  box-sizing: border-box;
+  border: 1px solid rgb(253,162,155);
+  color: rgb(62,67,87);
+  word-break: break-word;
+  background-color: rgb(255,255,255);
+}
+
+.emotion-0:focus-visible {
+  background-color: rgb(239,242,249);
+}
+
+.emotion-0:hover {
+  box-shadow: 0px 1px 3px 0px rgba(29,33,47,0.05),0px 1px 25px 0px rgba(29,33,47,0.05);
+}
+
+<div>
+  <button
+    class="emotion-0 emotion-1"
+    data-issue="key"
+  >
+    message
+  </button>
+</div>
+`;
+
+exports[`should render correctly when issueType is provided 1`] = `
+.emotion-0 {
+  all: unset;
+  cursor: pointer;
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-align-items: center;
+  -webkit-box-align: center;
+  -ms-flex-align: center;
+  align-items: center;
+  gap: 0.5rem;
+  margin-left: 0.25rem;
+  margin-right: 0.25rem;
+  margin-top: 0.75rem;
+  margin-bottom: 0.75rem;
+  padding: 0.75rem;
+  font-family: Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
+  font-size: 1rem;
+  line-height: 1.5rem;
+  font-weight: 600;
+  border-radius: 0.25rem;
+  width: 100%;
+  box-sizing: border-box;
+  border: 1px solid rgb(253,162,155);
+  color: rgb(62,67,87);
+  word-break: break-word;
+  background-color: rgb(255,255,255);
+}
+
+.emotion-0:focus-visible {
+  background-color: rgb(239,242,249);
+}
+
+.emotion-0:hover {
+  box-shadow: 0px 1px 3px 0px rgba(29,33,47,0.05),0px 1px 25px 0px rgba(29,33,47,0.05);
+}
+
+.emotion-2 {
+  height: 1.5rem;
+  width: 1.5rem;
+  display: -webkit-inline-box;
+  display: -webkit-inline-flex;
+  display: -ms-inline-flexbox;
+  display: inline-flex;
+  -webkit-flex-shrink: 0;
+  -ms-flex-negative: 0;
+  flex-shrink: 0;
+  -webkit-align-items: center;
+  -webkit-box-align: center;
+  -ms-flex-align: center;
+  align-items: center;
+  -webkit-box-pack: center;
+  -ms-flex-pack: center;
+  -webkit-justify-content: center;
+  justify-content: center;
+  background: rgb(254,205,202);
+  border-radius: 100%;
+}
+
+<div>
+  <button
+    class="emotion-0 emotion-1"
+    data-issue="key"
+  >
+    <div
+      class="sw-ml-1/2 emotion-2 emotion-3"
+    >
+      <svg
+        aria-hidden="true"
+        fill="none"
+        height="1rem"
+        role="img"
+        style="clip-rule: evenodd; display: inline-block; fill-rule: evenodd; user-select: none; vertical-align: middle; stroke-linejoin: round; stroke-miterlimit: 1.414;"
+        version="1.1"
+        viewBox="0 0 16 16"
+        width="1rem"
+        xml:space="preserve"
+        xmlns:xlink="http://www.w3.org/1999/xlink"
+      >
+        <path
+          d="M10.09,1.88A2.86,2.86,0,0,0,8,1a2.87,2.87,0,0,0-2.11.87A2.93,2.93,0,0,0,5,4h6A2.93,2.93,0,0,0,10.09,1.88Z"
+          fill="rgb(93,29,19)"
+        />
+        <path
+          d="M14.54,9H13V5.6L14.3,4.42a.5.5,0,0,0,0-.71.49.49,0,0,0-.7,0L12.17,5H3.82L2.34,3.66a.5.5,0,0,0-.67.74L2.94,5.55V9H1.46a.5.5,0,0,0,0,1H3a5.2,5.2,0,0,0,1.05,2.32l-2,1.81a.5.5,0,1,0,.67.74l2-1.82A4.62,4.62,0,0,0,7,14.1V8A1,1,0,0,1,8,7a.94.94,0,0,1,1,.9v6.17A4.55,4.55,0,0,0,11.18,13l2,1.83a.51.51,0,0,0,.33.13.48.48,0,0,0,.37-.17.49.49,0,0,0,0-.7l-2-1.8a5.34,5.34,0,0,0,1-2.29h1.64a.5.5,0,0,0,0-1Z"
+          fill="rgb(93,29,19)"
+        />
+      </svg>
+    </div>
+    message
+  </button>
+</div>
+`;
index 9d2cc794208ae154e7e766fd003f25a7c311089b..052ea396271ef1e4b9e3b84ab2b60ea56d0525fb 100644 (file)
@@ -27,9 +27,9 @@ import { IssueTypeCircleIcon } from '../icons/IssueTypeIcon';
 interface Props {
   className?: string;
   issueKey: string;
-  issueType: string;
-  message: string;
-  onIssueSelect: (issueKey: string) => void;
+  issueType?: string;
+  message: React.ReactNode;
+  onIssueSelect?: (issueKey: string) => void;
   selected?: boolean;
 }
 
@@ -42,12 +42,14 @@ function LineFindingFunc(
       className={className}
       data-issue={issueKey}
       onClick={() => {
-        onIssueSelect(issueKey);
+        if (onIssueSelect) {
+          onIssueSelect(issueKey);
+        }
       }}
       ref={ref}
       selected={selected}
     >
-      <IssueTypeCircleIcon className="sw-ml-1/2" type={issueType} />
+      {issueType && <IssueTypeCircleIcon className="sw-ml-1/2" type={issueType} />}
       {message}
     </LineFindingStyled>
   );
index cf62c597baa9b04e651e32d7051c2800b34d6caf..092cfc0696c06da9f2e225bdf32e93909c582816 100644 (file)
@@ -22,7 +22,7 @@ import classNames from 'classnames';
 import { forwardRef, Ref, useCallback, useRef } from 'react';
 import tw from 'twin.macro';
 import { themeColor, themeContrast } from '../../helpers/theme';
-import { IssueLocationMarker } from '../IssueLocationMarker';
+import { LocationMarker } from '../LocationMarker';
 
 interface Props {
   hideLocationIndex?: boolean;
@@ -46,9 +46,9 @@ function LineMarkerFunc(
 
   return (
     <Wrapper className={classNames({ leading })} ref={element}>
-      <IssueLocationMarker
+      <LocationMarker
         onClick={handleClick}
-        ref={ref}
+        ref={ref as React.RefObject<HTMLDivElement>}
         selected={selected}
         text={hideLocationIndex ? undefined : index + 1}
       />
index fe0935791f501eba2adbdb436714348504515136..337e813a485b82d7778296eda9490be8c041e527 100644 (file)
  */
 import styled from '@emotion/styled';
 import classNames from 'classnames';
-import { FlagMessage, LineFinding, ThemeProp, themeColor, withTheme } from 'design-system';
+import { FlagMessage, LineFinding, themeColor } from 'design-system';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
 import { getSources } from '../../../api/components';
 import getCoverageStatus from '../../../components/SourceViewer/helpers/getCoverageStatus';
 import { locationsByLine } from '../../../components/SourceViewer/helpers/indexing';
+import { IssueMessageHighlighting } from '../../../components/issue/IssueMessageHighlighting';
 import { getBranchLikeQuery } from '../../../helpers/branch-like';
 import { translate } from '../../../helpers/l10n';
 import { BranchLike } from '../../../types/branch-like';
@@ -81,10 +82,10 @@ interface State {
   snippets: Snippet[];
 }
 
-class ComponentSourceSnippetGroupViewer extends React.PureComponent<Props & ThemeProp, State> {
+export default class ComponentSourceSnippetGroupViewer extends React.PureComponent<Props, State> {
   mounted = false;
 
-  constructor(props: Props & ThemeProp) {
+  constructor(props: Props) {
     super(props);
     this.state = {
       additionalLines: {},
@@ -242,7 +243,12 @@ class ComponentSourceSnippetGroupViewer extends React.PureComponent<Props & Them
                   <LineFinding
                     issueType={issueToDisplay.type}
                     issueKey={issueToDisplay.key}
-                    message={issueToDisplay.message}
+                    message={
+                      <IssueMessageHighlighting
+                        message={issueToDisplay.message}
+                        messageFormattings={issueToDisplay.messageFormattings}
+                      />
+                    }
                     selected={isSelectedIssue}
                     ref={isSelectedIssue ? ctx?.registerPrimaryLocationRef : undefined}
                     onIssueSelect={this.props.onIssueSelect}
@@ -257,8 +263,7 @@ class ComponentSourceSnippetGroupViewer extends React.PureComponent<Props & Them
   };
 
   render() {
-    const { branchLike, isLastOccurenceOfPrimaryComponent, issue, snippetGroup, theme } =
-      this.props;
+    const { branchLike, isLastOccurenceOfPrimaryComponent, issue, snippetGroup } = this.props;
     const { additionalLines, loading, snippets } = this.state;
 
     const snippetLines = linesForSnippets(snippets, {
@@ -272,12 +277,6 @@ class ComponentSourceSnippetGroupViewer extends React.PureComponent<Props & Them
       ? 'issue.closed.file_level'
       : 'issue.closed.project_level';
 
-    const borderColor = themeColor('codeLineBorder')({ theme });
-
-    const FileLevelIssueStyle = styled.div`
-      border: 1px solid ${borderColor};
-    `;
-
     const hideLocationIndex = issue.secondaryLocations.length !== 0;
 
     return (
@@ -323,7 +322,12 @@ class ComponentSourceSnippetGroupViewer extends React.PureComponent<Props & Them
                   <LineFinding
                     issueType={issue.type}
                     issueKey={issue.key}
-                    message={issue.message}
+                    message={
+                      <IssueMessageHighlighting
+                        message={issue.message}
+                        messageFormattings={issue.messageFormattings}
+                      />
+                    }
                     selected={true}
                     ref={ctx?.registerPrimaryLocationRef}
                     onIssueSelect={this.props.onIssueSelect}
@@ -391,4 +395,6 @@ function isExpandable(snippets: Snippet[], snippetGroup: SnippetGroup) {
   return !fullyShown && isFile(snippetGroup.component.q);
 }
 
-export default withTheme(ComponentSourceSnippetGroupViewer);
+const FileLevelIssueStyle = styled.div`
+  border: 1px solid ${themeColor('codeLineBorder')};
+`;
index 6506a0bc9f7d958972ec1c6804a97297ea3a44a4..ceba4a4006a4b110e8215fb1a07cce628f138348 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import classNames from 'classnames';
+import { LineFinding } from 'design-system';
 import * as React from 'react';
 import { IssueMessageHighlighting } from '../../../components/issue/IssueMessageHighlighting';
 import { Hotspot } from '../../../types/security-hotspots';
-import './HotspotPrimaryLocationBox.css';
 
 const SCROLL_DELAY = 100;
 const SCROLL_TOP_OFFSET = 100; // 5 lines above
@@ -51,23 +50,23 @@ export default function HotspotPrimaryLocationBox(props: HotspotPrimaryLocationB
 
   return (
     <div
-      className={classNames(
-        'hotspot-primary-location',
-        'display-flex-space-between display-flex-center padded-top padded-bottom big-padded-left big-padded-right',
-        `hotspot-risk-exposure-${hotspot.rule.vulnerabilityProbability}`
-      )}
       style={{
         scrollMarginTop: `${SCROLL_TOP_OFFSET}px`,
         scrollMarginBottom: `${SCROLL_BOTTOM_OFFSET}px`,
       }}
       ref={locationRef}
     >
-      <div className="text-bold">
-        <IssueMessageHighlighting
-          message={hotspot.message}
-          messageFormattings={hotspot.messageFormattings}
-        />
-      </div>
+      <LineFinding
+        issueKey={hotspot.key}
+        message={
+          <IssueMessageHighlighting
+            message={hotspot.message}
+            messageFormattings={hotspot.messageFormattings}
+          />
+        }
+        selected={true}
+        className="sw-cursor-default"
+      />
     </div>
   );
 }
index 3434874b0d7ad3b9c6c7ceb3ca82802de78ac818..48433425bde6e6920ed03b83269cd6779fb725a5 100644 (file)
@@ -62,7 +62,7 @@ export async function animateExpansion(
   expandBlock: (direction: ExpandDirection) => Promise<void>,
   direction: ExpandDirection
 ) {
-  const wrapper = scrollableRef.current?.querySelector<HTMLElement>('.snippet');
+  const wrapper = scrollableRef.current?.querySelector<HTMLElement>('.it__source-viewer-code');
   const table = wrapper?.firstChild as HTMLElement;
 
   if (!wrapper || !table) {
@@ -175,6 +175,7 @@ export default function HotspotSnippetContainerRenderer(
             renderAdditionalChildInLine={renderHotspotBoxInLine}
             renderDuplicationPopup={noop}
             snippet={sourceLines}
+            hideLocationIndex={secondaryLocations.length !== 0}
           />
         )}
       </SourceFileWrapper>
index 90c85e342c989c34949f75c297a5474a2611184d..370073cf80b9842a6c90876a7816c5103304608e 100644 (file)
@@ -31,7 +31,7 @@ import {
   UncoveredUnderlineLabel,
   UnderlineLabels,
 } from 'design-system';
-import React, { Fragment, PureComponent, ReactNode, RefObject, createRef } from 'react';
+import React, { PureComponent, ReactNode, RefObject, createRef } from 'react';
 import { IssueSourceViewerScrollContext } from '../../../apps/issues/components/IssueSourceViewerScrollContext';
 import { translate } from '../../../helpers/l10n';
 import { LinearIssueLocation, SourceLine } from '../../../types/types';
@@ -104,21 +104,19 @@ export class LineCode extends PureComponent<React.PropsWithChildren<Props>> {
     const message = loc?.text;
     const isLeading = leadingMarker && markerIndex === 0;
     return (
-      <Fragment key={`${marker}-${index}`}>
-        <IssueSourceViewerScrollContext.Consumer>
-          {(ctx) => (
-            <LineMarker
-              hideLocationIndex={hideLocationIndex}
-              index={marker}
-              leading={isLeading}
-              message={message}
-              onLocationSelect={this.props.onLocationSelect}
-              ref={selected ? ctx?.registerSelectedSecondaryLocationRef : undefined}
-              selected={selected}
-            />
-          )}
-        </IssueSourceViewerScrollContext.Consumer>
-      </Fragment>
+      <IssueSourceViewerScrollContext.Consumer key={`${marker}-${index}`}>
+        {(ctx) => (
+          <LineMarker
+            hideLocationIndex={hideLocationIndex}
+            index={marker}
+            leading={isLeading}
+            message={message}
+            onLocationSelect={this.props.onLocationSelect}
+            ref={selected ? ctx?.registerSelectedSecondaryLocationRef : undefined}
+            selected={selected}
+          />
+        )}
+      </IssueSourceViewerScrollContext.Consumer>
     );
   };
 
@@ -189,10 +187,7 @@ export class LineCode extends PureComponent<React.PropsWithChildren<Props>> {
       (previousLine?.coverageStatus && previousLine.coverageBlock === line.coverageBlock);
 
     return (
-      <LineCodeLayers
-        className="js-source-line-code it__source-line-code"
-        data-line-number={line.line}
-      >
+      <LineCodeLayers className="it__source-line-code" data-line-number={line.line}>
         {(displayCoverageUnderlineLabel || displayNewCodeUnderlineLabel) && (
           <UnderlineLabels aria-hidden={true} transparentBackground={previousLineHasUnderline}>
             {displayCoverageUnderlineLabel && line.coverageStatus === 'covered' && (