diff options
author | Revanshu Paliwal <revanshu.paliwal@sonarsource.com> | 2023-05-25 15:40:11 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-06-01 20:02:59 +0000 |
commit | cd2200061453646ec5a1563dc68ee5799c18bef3 (patch) | |
tree | 246dba0ce4c8f480446e9c5e17da8da11bf82004 /server/sonar-web | |
parent | 8592cedbc5b667e62acc701986e8cc14fe3402ca (diff) | |
download | sonarqube-cd2200061453646ec5a1563dc68ee5799c18bef3.tar.gz sonarqube-cd2200061453646ec5a1563dc68ee5799c18bef3.zip |
SONAR-19174 Code viewer changes for hotspots page
Diffstat (limited to 'server/sonar-web')
9 files changed, 256 insertions, 130 deletions
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 index f9886fd80b9..00000000000 --- a/server/sonar-web/design-system/src/components/IssueLocationMarker.tsx +++ /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 index 00000000000..30d470d809b --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/LineFinding-test.tsx @@ -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 index 00000000000..b72c648fe60 --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/LineFinding-test.tsx.snap @@ -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> +`; diff --git a/server/sonar-web/design-system/src/components/code-line/LineFinding.tsx b/server/sonar-web/design-system/src/components/code-line/LineFinding.tsx index 9d2cc794208..052ea396271 100644 --- a/server/sonar-web/design-system/src/components/code-line/LineFinding.tsx +++ b/server/sonar-web/design-system/src/components/code-line/LineFinding.tsx @@ -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> ); diff --git a/server/sonar-web/design-system/src/components/code-line/LineMarker.tsx b/server/sonar-web/design-system/src/components/code-line/LineMarker.tsx index cf62c597baa..092cfc0696c 100644 --- a/server/sonar-web/design-system/src/components/code-line/LineMarker.tsx +++ b/server/sonar-web/design-system/src/components/code-line/LineMarker.tsx @@ -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} /> diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx index fe0935791f5..337e813a485 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx @@ -19,12 +19,13 @@ */ 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')}; +`; diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotPrimaryLocationBox.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotPrimaryLocationBox.tsx index 6506a0bc9f7..ceba4a4006a 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotPrimaryLocationBox.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotPrimaryLocationBox.tsx @@ -17,11 +17,10 @@ * 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> ); } diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx index 3434874b0d7..48433425bde 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx index 90c85e342c9..370073cf80b 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx @@ -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' && ( |