diff options
author | 7PH <benjamin.raymond@sonarsource.com> | 2023-05-15 11:10:22 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-05-24 20:03:13 +0000 |
commit | 1948544ed0d2441d4aae7f95411cea4430c3e51c (patch) | |
tree | c86a7d554c84e28ad047d32bf5b43b837e7e3f7c /server/sonar-web | |
parent | faf2c7ae1f4a3f537e0f8493ad82a816c21d4568 (diff) | |
download | sonarqube-1948544ed0d2441d4aae7f95411cea4430c3e51c.tar.gz sonarqube-1948544ed0d2441d4aae7f95411cea4430c3e51c.zip |
SONAR-19236 Implement sidebar hotspot list using the new design system
Diffstat (limited to 'server/sonar-web')
12 files changed, 189 insertions, 329 deletions
diff --git a/server/sonar-web/src/main/js/app/components/GlobalMessage.tsx b/server/sonar-web/src/main/js/app/components/GlobalMessage.tsx index af2208f3f2c..46e83ae2ddf 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalMessage.tsx +++ b/server/sonar-web/src/main/js/app/components/GlobalMessage.tsx @@ -34,7 +34,7 @@ export default function GlobalMessage(props: GlobalMessageProps) { const { message } = props; return ( <MessageBox - data-test={`global-message__${message.level}`} + data-testid={`global-message__${message.level}`} level={message.level} role={message.level === 'SUCCESS' ? 'status' : 'alert'} > diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx index 9d6818d14c6..b39c2c0b297 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx @@ -43,7 +43,7 @@ import { Component, Dict } from '../../types/types'; import { CurrentUser, isLoggedIn } from '../../types/users'; import SecurityHotspotsAppRenderer from './SecurityHotspotsAppRenderer'; import './styles.css'; -import { getLocations, SECURITY_STANDARDS } from './utils'; +import { SECURITY_STANDARDS, getLocations } from './utils'; const PAGE_SIZE = 500; interface DispatchProps { @@ -526,7 +526,6 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { onLocationClick={this.handleLocationClick} securityCategories={standards[SecurityStandard.SONARSOURCE]} selectedHotspot={selectedHotspot} - selectedHotspotLocation={selectedHotspotLocationIndex} standards={standards} /> ); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx index 3f084399815..6ef57e100d6 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx @@ -17,7 +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 { useTheme } from '@emotion/react'; +import { withTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { LargeCenteredLayout, @@ -94,21 +94,6 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe standards, } = props; - const theme = useTheme(); - - React.useEffect(() => { - if (!selectedHotspot) { - return; - } - // Wait for next tick, in case newly selected hotspot is not yet expanded - setTimeout(() => { - document.querySelector(`[data-hotspot-key="${selectedHotspot.key}"]`)?.scrollIntoView({ - block: 'center', - behavior: 'smooth', - }); - }); - }, [selectedHotspot]); - return ( <> <Suggestions suggestions="security_hotspots" /> @@ -142,10 +127,7 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe /> ) : ( <> - <FilterbarStyled - theme={theme} - className="sw-col-span-4 sw-rounded-t-1 sw-mt-0 sw-z-filterbar" - > + <FilterbarStyled className="sw-col-span-4 sw-rounded-t-1 sw-mt-0 sw-z-filterbar sw-p-4 it__hotspot-list"> {filterByCategory || filterByCWE || filterByFile ? ( <HotspotSimpleList filterByCategory={filterByCategory} @@ -198,18 +180,15 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe ); } -const FilterbarStyled = styled.div( - (props) => ` -position: sticky; -box-sizing: border-box; -overflow-x: hidden; -overflow-y: auto; -background-color: ${themeColor('filterbar')(props)}; -border-right: ${themeBorder('default', 'filterbarBorder')(props)}; -// ToDo set proper hegiht -height: calc(100vh - ${'100px'}); - -&.border-left { - border-left: ${themeBorder('default', 'filterbarBorder')(props)}; -}` +const FilterbarStyled = withTheme( + styled.div` + position: sticky; + box-sizing: border-box; + overflow-x: hidden; + overflow-y: auto; + background-color: ${themeColor('filterbar')}; + border-right: ${themeBorder('default', 'filterbarBorder')}; + // ToDo set proper height + height: calc(100vh - ${'100px'}); + ` ); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts index 1f0040b8bbb..73eeed4ec54 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts +++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts @@ -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 { HotspotRatingEnum } from 'design-system'; import { mockHotspot, mockRawHotspot } from '../../../helpers/mocks/security-hotspots'; import { mockUser } from '../../../helpers/testMocks'; import { @@ -26,7 +27,6 @@ import { HotspotStatusOption, RawHotspot, ReviewHistoryType, - RiskExposure, } from '../../../types/security-hotspots'; import { FlowLocation, IssueChangelog } from '../../../types/types'; import { @@ -43,55 +43,55 @@ import { const hotspots = [ mockRawHotspot({ key: '3', - vulnerabilityProbability: RiskExposure.HIGH, + vulnerabilityProbability: HotspotRatingEnum.HIGH, securityCategory: 'object-injection', message: 'tfdh', }), mockRawHotspot({ key: '5', - vulnerabilityProbability: RiskExposure.MEDIUM, + vulnerabilityProbability: HotspotRatingEnum.MEDIUM, securityCategory: 'xpath-injection', message: 'asdf', }), mockRawHotspot({ key: '1', - vulnerabilityProbability: RiskExposure.HIGH, + vulnerabilityProbability: HotspotRatingEnum.HIGH, securityCategory: 'dos', message: 'a', }), mockRawHotspot({ key: '7', - vulnerabilityProbability: RiskExposure.LOW, + vulnerabilityProbability: HotspotRatingEnum.LOW, securityCategory: 'ssrf', message: 'rrrr', }), mockRawHotspot({ key: '2', - vulnerabilityProbability: RiskExposure.HIGH, + vulnerabilityProbability: HotspotRatingEnum.HIGH, securityCategory: 'dos', message: 'b', }), mockRawHotspot({ key: '8', - vulnerabilityProbability: RiskExposure.LOW, + vulnerabilityProbability: HotspotRatingEnum.LOW, securityCategory: 'ssrf', message: 'sssss', }), mockRawHotspot({ key: '4', - vulnerabilityProbability: RiskExposure.MEDIUM, + vulnerabilityProbability: HotspotRatingEnum.MEDIUM, securityCategory: 'log-injection', message: 'asdf', }), mockRawHotspot({ key: '9', - vulnerabilityProbability: RiskExposure.LOW, + vulnerabilityProbability: HotspotRatingEnum.LOW, securityCategory: 'xxe', message: 'aaa', }), mockRawHotspot({ key: '6', - vulnerabilityProbability: RiskExposure.LOW, + vulnerabilityProbability: HotspotRatingEnum.LOW, securityCategory: 'xss', message: 'zzz', }), diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx index 4a736d9e1bb..e34ad991cf0 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx @@ -17,32 +17,33 @@ * 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 * as React from 'react'; -import { ButtonPlain } from '../../../components/controls/buttons'; -import ChevronDownIcon from '../../../components/icons/ChevronDownIcon'; -import ChevronUpIcon from '../../../components/icons/ChevronUpIcon'; +import styled from '@emotion/styled'; +import { Badge, HotspotRating, HotspotRatingEnum, SubnavigationAccordion } from 'design-system'; +import React, { memo } from 'react'; import { RawHotspot } from '../../../types/security-hotspots'; import HotspotListItem from './HotspotListItem'; -export interface HotspotCategoryProps { - categoryKey: string; +interface HotspotCategoryProps { expanded: boolean; + onSetExpanded: (expanded: boolean) => void; hotspots: RawHotspot[]; + isLastAndIncomplete: boolean; onHotspotClick: (hotspot: RawHotspot) => void; - onToggleExpand?: (categoryKey: string, value: boolean) => void; onLocationClick: (index: number) => void; + rating: HotspotRatingEnum; selectedHotspot: RawHotspot; selectedHotspotLocation?: number; title: string; - isLastAndIncomplete: boolean; } export default function HotspotCategory(props: HotspotCategoryProps) { const { - categoryKey, expanded, + onSetExpanded, hotspots, + onLocationClick, + onHotspotClick, + rating, selectedHotspot, title, isLastAndIncomplete, @@ -56,49 +57,55 @@ export default function HotspotCategory(props: HotspotCategoryProps) { const risk = hotspots[0].vulnerabilityProbability; return ( - <div className={classNames('hotspot-category', risk)}> - {props.onToggleExpand ? ( - <ButtonPlain - className={classNames( - 'hotspot-category-header display-flex-space-between display-flex-center', - { 'contains-selected-hotspot': selectedHotspot.securityCategory === categoryKey } - )} - onClick={() => props.onToggleExpand && props.onToggleExpand(categoryKey, !expanded)} - aria-expanded={expanded} - > - <strong className="flex-1 spacer-right break-word">{title}</strong> - <span> - <span className="counter-badge"> - {hotspots.length} - {isLastAndIncomplete && '+'} - </span> - {expanded ? ( - <ChevronUpIcon className="big-spacer-left" /> - ) : ( - <ChevronDownIcon className="big-spacer-left" /> - )} - </span> - </ButtonPlain> - ) : ( - <div className="hotspot-category-header"> - <strong className="flex-1 spacer-right break-word">{title}</strong> - </div> - )} - {expanded && ( - <ul> - {hotspots.map((h) => ( - <li data-hotspot-key={h.key} key={h.key}> - <HotspotListItem - hotspot={h} - onClick={props.onHotspotClick} - onLocationClick={props.onLocationClick} - selectedHotspotLocation={selectedHotspotLocation} - selected={h.key === selectedHotspot.key} - /> - </li> - ))} - </ul> - )} - </div> + <SubnavigationAccordion + header={ + <MemoizedHeader + hotspots={hotspots} + isLastAndIncomplete={isLastAndIncomplete} + rating={rating} + title={title} + /> + } + id={`hotspot-category-${risk}`} + expanded={expanded} + onSetExpanded={onSetExpanded} + > + {hotspots.map((hotspot) => ( + <HotspotListItem + hotspot={hotspot} + key={hotspot.key} + onClick={onHotspotClick} + selected={hotspot.key === selectedHotspot.key} + onLocationClick={onLocationClick} + selectedHotspotLocation={selectedHotspotLocation} + /> + ))} + </SubnavigationAccordion> + ); +} + +type NavigationHeaderProps = Pick< + HotspotCategoryProps, + 'hotspots' | 'isLastAndIncomplete' | 'rating' | 'title' +>; + +function NavigationHeader(props: NavigationHeaderProps) { + const { hotspots, isLastAndIncomplete, rating, title } = props; + const counter = hotspots.length + (isLastAndIncomplete ? '+' : ''); + + return ( + <SubNavigationContainer className="sw-flex sw-justify-between"> + <div className="sw-flex sw-items-center"> + <HotspotRating className="sw-mr-2" rating={rating} /> + {title} + </div> + <Badge variant="counter">{counter}</Badge> + </SubNavigationContainer> ); } + +const MemoizedHeader = memo(NavigationHeader); + +const SubNavigationContainer = styled.div` + width: calc(100% - 1.5rem); +`; diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.css b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.css deleted file mode 100644 index 1f65c3e9640..00000000000 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.css +++ /dev/null @@ -1,127 +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. - */ -.hotspot-list-header { - padding: calc(2 * var(--gridSize)) var(--gridSize); -} - -.hotspot-risk-header { - padding: var(--gridSize); -} - -.hotspot-category { - background-color: white; - border: 1px solid var(--barBorderColor); -} - -.hotspot-category .hotspot-category-header { - width: 100%; - padding: calc(2 * var(--gridSize)) var(--gridSize); - color: var(--baseFontColor); - border-bottom: none; - border-left: 4px solid; - box-sizing: border-box; -} - -.hotspot-category strong { - text-align: left; -} - -.hotspot-category .hotspot-category-header:hover, -.hotspot-category .hotspot-category-header.contains-selected-hotspot { - color: var(--blue); -} - -.hotspot-category.HIGH .hotspot-category-header { - border-left-color: var(--error400); -} - -.hotspot-category.MEDIUM .hotspot-category-header { - border-left-color: var(--warningAccent); -} - -.hotspot-category.LOW .hotspot-category-header { - border-left-color: var(--warningVariant); -} - -.hotspot-category .hotspot-item { - color: var(--baseFontColor); - display: block; - padding: var(--gridSize) calc(2 * var(--gridSize)); - border: 2px solid transparent; - border-top-color: var(--barBorderColor); - transition: padding 0s, border 0s; - width: 100%; - text-align: left; -} - -.hotspot-category button.hotspot-item:focus { - color: var(--baseFontColor); -} - -.hotspot-category .hotspot-item:hover { - background-color: var(--veryLightBlue); - border: 2px dashed var(--blue); - color: var(--baseFontColor); -} - -.hotspot-category .hotspot-item.highlight:hover { - background-color: transparent; -} - -.hotspot-category .hotspot-item.highlight { - color: var(--baseFontColor); - border: 2px solid var(--blue); - cursor: unset; -} - -.hotspot-risk-badge { - text-transform: uppercase; - display: inline-block; - text-align: center; - padding: 0 calc(var(--gridSize) / 2); - font-weight: bold; - border-radius: 3px; -} - -.hotspot-risk-badge.HIGH { - color: var(--blacka87); - background-color: var(--error400); -} -.hotspot-risk-badge.MEDIUM { - color: var(--blacka87); - background-color: var(--warningAccent); -} -.hotspot-risk-badge.LOW { - color: var(--blacka87); - background-color: var(--warningVariant); -} - -.hotspot-box-filename { - direction: rtl; -} - -.hotspot-header .issue-message-highlight-CODE { - background-color: var(--blacka06); - border-radius: 5px; -} - -.hotspot-item .issue-message-highlight-CODE { - background-color: var(--blacka06); -} diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx index 2f72ebdd5dc..acb87ce24e9 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx @@ -17,18 +17,19 @@ * 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 { withTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { HotspotRating, HotspotRatingEnum, SubnavigationHeading, themeColor } from 'design-system'; import { groupBy } from 'lodash'; import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; import ListFooter from '../../../components/controls/ListFooter'; -import SecurityHotspotIcon from '../../../components/icons/SecurityHotspotIcon'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { addSideBarClass, removeSideBarClass } from '../../../helpers/pages'; -import { HotspotStatusFilter, RawHotspot, RiskExposure } from '../../../types/security-hotspots'; +import { translate } from '../../../helpers/l10n'; +import { removeSideBarClass } from '../../../helpers/pages'; +import { HotspotStatusFilter, RawHotspot } from '../../../types/security-hotspots'; import { Dict, StandardSecurityCategories } from '../../../types/types'; -import { groupByCategory, RISK_EXPOSURE_LEVELS } from '../utils'; +import { RISK_EXPOSURE_LEVELS, groupByCategory } from '../utils'; import HotspotCategory from './HotspotCategory'; -import './HotspotList.css'; interface Props { hotspots: RawHotspot[]; @@ -47,7 +48,7 @@ interface Props { interface State { expandedCategories: Dict<boolean>; groupedHotspots: Array<{ - risk: RiskExposure; + risk: HotspotRatingEnum; categories: Array<{ key: string; hotspots: RawHotspot[]; title: string }>; }>; } @@ -62,10 +63,6 @@ export default class HotspotList extends React.Component<Props, State> { }; } - componentDidMount() { - addSideBarClass(); - } - componentDidUpdate(prevProps: Props) { // Force open the category of selected hotspot if ( @@ -109,9 +106,14 @@ export default class HotspotList extends React.Component<Props, State> { }; handleToggleCategory = (categoryKey: string, value: boolean) => { - this.setState(({ expandedCategories }) => ({ - expandedCategories: { ...expandedCategories, [categoryKey]: value }, - })); + this.setState(({ expandedCategories }) => { + return { + expandedCategories: { + ...expandedCategories, + [categoryKey]: value, + }, + }; + }); }; render() { @@ -128,61 +130,74 @@ export default class HotspotList extends React.Component<Props, State> { const { expandedCategories, groupedHotspots } = this.state; return ( - <div className="huge-spacer-bottom"> - <h1 className="hotspot-list-header bordered-bottom"> - <SecurityHotspotIcon className="spacer-right" /> - {translateWithParameters( - isStaticListOfHotspots ? 'hotspots.list_title' : `hotspots.list_title.${statusFilter}`, - hotspotsTotal - )} - </h1> - <ul className="big-spacer-bottom big-spacer-top"> + <StyledContainer> + <span className="sw-body-sm"> + <FormattedMessage + id="hotspots.list_title" + defaultMessage={ + isStaticListOfHotspots + ? translate('hotspots.list_title') + : translate(`hotspots.list_title.${statusFilter}`) + } + values={{ + 0: <strong className="sw-body-sm-highlight">{hotspotsTotal}</strong>, + }} + /> + </span> + <div className="sw-mt-8 sw-mb-4"> {groupedHotspots.map((riskGroup, riskGroupIndex) => { const isLastRiskGroup = riskGroupIndex === groupedHotspots.length - 1; return ( - <li className="big-spacer-bottom" key={riskGroup.risk}> - <div className="hotspot-risk-header little-spacer-left spacer-top spacer-bottom"> - <span>{translate('hotspots.risk_exposure')}:</span> - <div className={classNames('hotspot-risk-badge', 'spacer-left', riskGroup.risk)}> + <div className="sw-mb-4" key={riskGroup.risk}> + <SubnavigationHeading className="sw-px-0"> + <div className="sw-flex sw-items-center"> + <span className="sw-body-sm-highlight"> + {translate('hotspots.risk_exposure')}: + </span> + <HotspotRating className="sw-ml-2 sw-mr-1" rating={riskGroup.risk} /> {translate('risk_exposure', riskGroup.risk)} </div> - </div> - <ul> - {riskGroup.categories.map((cat, categoryIndex) => { + </SubnavigationHeading> + <div> + {riskGroup.categories.map((category, categoryIndex) => { const isLastCategory = categoryIndex === riskGroup.categories.length - 1; return ( - <li className="spacer-bottom" key={cat.key}> + <div className="sw-mb-2" key={category.key}> <HotspotCategory - categoryKey={cat.key} - expanded={expandedCategories[cat.key]} - hotspots={cat.hotspots} - onHotspotClick={this.props.onHotspotClick} - onToggleExpand={this.handleToggleCategory} - onLocationClick={this.props.onLocationClick} - selectedHotspot={selectedHotspot} - selectedHotspotLocation={selectedHotspotLocation} - title={cat.title} + expanded={Boolean(expandedCategories[category.key])} + onSetExpanded={this.handleToggleCategory.bind(this, category.key)} + hotspots={category.hotspots} isLastAndIncomplete={ isLastRiskGroup && isLastCategory && hotspots.length < hotspotsTotal } + onHotspotClick={this.props.onHotspotClick} + rating={riskGroup.risk} + selectedHotspot={selectedHotspot} + selectedHotspotLocation={selectedHotspotLocation} + onLocationClick={this.props.onLocationClick} + title={category.title} /> - </li> + </div> ); })} - </ul> - </li> + </div> + </div> ); })} - </ul> + </div> <ListFooter count={hotspots.length} loadMore={!loadingMore ? this.props.onLoadMore : undefined} loading={loadingMore} total={hotspotsTotal} /> - </div> + </StyledContainer> ); } } + +const StyledContainer = withTheme(styled.div` + background-color: ${themeColor('subnavigation')}; +`); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx index a04129e4853..2a4dca91052 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx @@ -17,17 +17,13 @@ * 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 * as React from 'react'; -import { ButtonPlain } from '../../../components/controls/buttons'; -import QualifierIcon from '../../../components/icons/QualifierIcon'; -import { IssueMessageHighlighting } from '../../../components/issue/IssueMessageHighlighting'; +import { SubnavigationItem } from 'design-system'; +import React, { useCallback } from 'react'; import LocationsList from '../../../components/locations/LocationsList'; -import { ComponentQualifier } from '../../../types/component'; import { RawHotspot } from '../../../types/security-hotspots'; -import { getFilePath, getLocations } from '../utils'; +import { getLocations } from '../utils'; -export interface HotspotListItemProps { +interface HotspotListItemProps { hotspot: RawHotspot; onClick: (hotspot: RawHotspot) => void; onLocationClick: (index?: number) => void; @@ -38,33 +34,34 @@ export interface HotspotListItemProps { export default function HotspotListItem(props: HotspotListItemProps) { const { hotspot, selected, selectedHotspotLocation } = props; const locations = getLocations(hotspot.flows, undefined); - const path = getFilePath(hotspot.component, hotspot.project); + + // Use useCallback instead of useEffect/useRef combination to be notified of the ref changes + const itemRef = useCallback( + (node) => { + if (selected && node) { + node.scrollIntoView({ + block: 'center', + behavior: 'smooth', + }); + } + }, + [selected] + ); + + const handleClick = () => { + if (!selected) { + props.onClick(hotspot); + } + }; return ( - <ButtonPlain - aria-current={selected} - className={classNames('hotspot-item', { highlight: selected })} - onClick={() => !selected && props.onClick(hotspot)} + <SubnavigationItem + active={selected} + innerRef={itemRef} + onClick={handleClick} + className="sw-flex-col sw-items-start" > - {/* This is not a real interaction it is only for scrolling */ - /* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} - <div - className={classNames('little-spacer-left text-bold', { 'cursor-pointer': selected })} - onClick={selected ? () => props.onLocationClick() : undefined} - > - <IssueMessageHighlighting - message={hotspot.message} - messageFormattings={hotspot.messageFormattings} - /> - </div> - <div className="display-flex-center big-spacer-top"> - <QualifierIcon qualifier={ComponentQualifier.File} /> - <div className="little-spacer-left hotspot-box-filename text-ellipsis" title={path}> - {/* <bdi> is used to avoid some cases where the path is wrongly displayed */} - {/* because of the parent's direction=rtl */} - <bdi>{path}</bdi> - </div> - </div> + <div>{hotspot.message}</div> {selected && ( <LocationsList locations={locations} @@ -74,6 +71,6 @@ export default function HotspotListItem(props: HotspotListItemProps) { selectedLocationIndex={selectedHotspotLocation} /> )} - </ButtonPlain> + </SubnavigationItem> ); } diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/styles.css b/server/sonar-web/src/main/js/apps/security-hotspots/styles.css index cb388941c31..b336939bb10 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/styles.css +++ b/server/sonar-web/src/main/js/apps/security-hotspots/styles.css @@ -66,12 +66,3 @@ background: white; box-sizing: border-box; } - -#security_hotspots .invisible { - height: 0; - overflow: hidden; -} - -#security_hotspots .hotspots-list-single-category .hotspot-category .hotspot-category-header { - color: var(--blue); -} diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts b/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts index e7a79986fb1..6e12fe7d629 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts +++ b/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts @@ -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 { HotspotRatingEnum } from 'design-system'; import { flatten, groupBy, sortBy } from 'lodash'; import { renderCWECategory, @@ -37,7 +38,6 @@ import { RawHotspot, ReviewHistoryElement, ReviewHistoryType, - RiskExposure, } from '../../types/security-hotspots'; import { Dict, @@ -48,7 +48,11 @@ import { const OTHERS_SECURITY_CATEGORY = 'others'; -export const RISK_EXPOSURE_LEVELS = [RiskExposure.HIGH, RiskExposure.MEDIUM, RiskExposure.LOW]; +export const RISK_EXPOSURE_LEVELS = [ + HotspotRatingEnum.HIGH, + HotspotRatingEnum.MEDIUM, + HotspotRatingEnum.LOW, +]; export const SECURITY_STANDARDS = [ SecurityStandard.SONARSOURCE, SecurityStandard.OWASP_TOP10, diff --git a/server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts b/server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts index 946b1f79a24..8e20ba3d8c0 100644 --- a/server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts +++ b/server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts @@ -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 { HotspotRatingEnum } from 'design-system'; import { ComponentQualifier } from '../../types/component'; import { Standards } from '../../types/security'; import { @@ -29,7 +30,6 @@ import { RawHotspot, ReviewHistoryElement, ReviewHistoryType, - RiskExposure, } from '../../types/security-hotspots'; import { mockFlowLocation, mockUser } from '../testMocks'; @@ -42,7 +42,7 @@ export function mockRawHotspot(overrides: Partial<RawHotspot> = {}): RawHotspot status: HotspotStatus.TO_REVIEW, resolution: undefined, securityCategory: 'command-injection', - vulnerabilityProbability: RiskExposure.HIGH, + vulnerabilityProbability: HotspotRatingEnum.HIGH, message: "'3' is a magic number.", line: 81, author: 'Developer 1', @@ -113,7 +113,7 @@ export function mockHotspotRule(overrides?: Partial<HotspotRule>): HotspotRule { return { key: 'squid:S2077', name: 'That rule', - vulnerabilityProbability: RiskExposure.HIGH, + vulnerabilityProbability: HotspotRatingEnum.HIGH, securityCategory: 'sql-injection', ...overrides, }; diff --git a/server/sonar-web/src/main/js/types/security-hotspots.ts b/server/sonar-web/src/main/js/types/security-hotspots.ts index f759c59ac21..9fd212b3d70 100644 --- a/server/sonar-web/src/main/js/types/security-hotspots.ts +++ b/server/sonar-web/src/main/js/types/security-hotspots.ts @@ -17,17 +17,12 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { HotspotRatingEnum } from 'design-system'; import { ComponentQualifier } from './component'; import { MessageFormatting } from './issues'; import { FlowLocation, IssueChangelog, IssueChangelogDiff, Paging, TextRange } from './types'; import { UserBase } from './users'; -export enum RiskExposure { - LOW = 'LOW', - MEDIUM = 'MEDIUM', - HIGH = 'HIGH', -} - export enum HotspotStatus { TO_REVIEW = 'TO_REVIEW', REVIEWED = 'REVIEWED', @@ -74,7 +69,7 @@ export interface RawHotspot { securityCategory: string; status: HotspotStatus; updateDate: string; - vulnerabilityProbability: RiskExposure; + vulnerabilityProbability: HotspotRatingEnum; flows?: Array<{ locations?: Array<Omit<FlowLocation, 'componentName'>>; }>; @@ -128,7 +123,7 @@ export interface HotspotRule { key: string; name: string; securityCategory: string; - vulnerabilityProbability: RiskExposure; + vulnerabilityProbability: HotspotRatingEnum; } export interface HotspotComment { |