]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19236 Implement sidebar hotspot list using the new design system
author7PH <benjamin.raymond@sonarsource.com>
Mon, 15 May 2023 09:10:22 +0000 (11:10 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 24 May 2023 20:03:13 +0000 (20:03 +0000)
12 files changed:
server/sonar-web/src/main/js/app/components/GlobalMessage.tsx
server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.css [deleted file]
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx
server/sonar-web/src/main/js/apps/security-hotspots/styles.css
server/sonar-web/src/main/js/apps/security-hotspots/utils.ts
server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts
server/sonar-web/src/main/js/types/security-hotspots.ts

index af2208f3f2cdb8880ddf7ef36bc25d1bb2b595dd..46e83ae2ddfab81671c233916c58673bad8e75b4 100644 (file)
@@ -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'}
     >
index 9d6818d14c65e9c2c964ca3cfc4e18c273f663ba..b39c2c0b297d058bdf5bdf64dd6ef0cd8839aaad 100644 (file)
@@ -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}
       />
     );
index 3f084399815ad9a7581447a887c89f878a9ffecc..6ef57e100d6b90a0cad1f43d1cf6eabf2d1bcce1 100644 (file)
@@ -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'});
+  `
 );
index 1f0040b8bbb05ea01b9104e1302cee6a00be9e09..73eeed4ec548def501a3e23e80d411caf2b32037 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 { 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',
   }),
index 4a736d9e1bbbd21480d1c0e7cfdcb2e1c030defe..e34ad991cf0020c9b3cb963ac830c39d52a42799 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 * 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 (file)
index 1f65c3e..0000000
+++ /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);
-}
index 2f72ebdd5dc645d0f030aecb5a5c18a7a4f8a6c9..acb87ce24e94411d9350953750feaed1673c6c67 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 { 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')};
+`);
index a04129e4853f5375a9cac16ce92344912f6fe6c4..2a4dca9105296fc7638821a772f281e27812fe17 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 * 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>
   );
 }
index cb388941c31d3b30cd5f77f6b6fc01d43c0f9b30..b336939bb1049355b4cea9e016129a7590b0771a 100644 (file)
   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);
-}
index e7a79986fb1371559a3418cce46165618b962b76..6e12fe7d629bf81f1324766d3622717f6b6e2e5b 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 { 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,
index 946b1f79a24496d23d2813936de9704d645cfe74..8e20ba3d8c0f81048728147e885fa67fcc2f9409 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 { 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,
   };
index f759c59ac21278ea0b2b53a764f03befdecb723f..9fd212b3d70ad8c46d4c5ce2ebce1f0a01d30d83 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 { 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 {