diff options
author | Revanshu Paliwal <revanshu.paliwal@sonarsource.com> | 2022-02-16 10:41:39 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-02-25 20:02:54 +0000 |
commit | e5474111c8f3e985cfa751d8cd86f1950aaf8e4d (patch) | |
tree | 5164d6200ea226175def9007343bd1948507c40a /server/sonar-web/src/main/js/apps/security-hotspots | |
parent | 9620694f92f2525837ce822d69f1296bf003ae69 (diff) | |
download | sonarqube-e5474111c8f3e985cfa751d8cd86f1950aaf8e4d.tar.gz sonarqube-e5474111c8f3e985cfa751d8cd86f1950aaf8e4d.zip |
SONAR-16007 Showing secondary locations in hotspot list box
Diffstat (limited to 'server/sonar-web/src/main/js/apps/security-hotspots')
9 files changed, 168 insertions, 18 deletions
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 969217f5617..f90fc16bf4b 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 @@ -74,7 +74,8 @@ interface State { loading: boolean; loadingMeasure: boolean; loadingMore: boolean; - selectedHotspot: RawHotspot | undefined; + selectedHotspot?: RawHotspot; + selectedHotspotLocation?: number; 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 715ce19cbff..43f521eb9e4 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 @@ -60,7 +60,8 @@ export interface SecurityHotspotsAppRendererProps { onShowAllHotspots: () => void; onSwitchStatusFilter: (option: HotspotStatusFilter) => void; onUpdateHotspot: (hotspotKey: string) => Promise<void>; - selectedHotspot: RawHotspot | undefined; + selectedHotspot?: RawHotspot; + selectedHotspotLocation?: number; securityCategories: StandardSecurityCategories; standards: Standards; } 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 d8155826feb..12856d774dd 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 @@ -24,12 +24,14 @@ import { HotspotStatus, HotspotStatusFilter, HotspotStatusOption, + RawHotspot, ReviewHistoryType, RiskExposure } from '../../../types/security-hotspots'; -import { IssueChangelog } from '../../../types/types'; +import { FlowLocation, IssueChangelog } from '../../../types/types'; import { getHotspotReviewHistory, + getLocations, getStatusAndResolutionFromStatusOption, getStatusFilterFromStatusOption, getStatusOptionFromStatusAndResolution, @@ -277,3 +279,43 @@ describe('getStatusFilterFromStatusOption', () => { ); }); }); + +describe('getLocations', () => { + it('should return the correct value', () => { + const location1: FlowLocation = { + component: 'foo', + msg: 'Do not use foo', + textRange: { startLine: 7, endLine: 7, startOffset: 5, endOffset: 8 } + }; + + const location2: FlowLocation = { + component: 'foo2', + msg: 'Do not use foo2', + textRange: { startLine: 7, endLine: 7, startOffset: 5, endOffset: 8 } + }; + + let rawFlows: RawHotspot['flows'] = [ + { + locations: [location1] + } + ]; + expect(getLocations(rawFlows, undefined)).toEqual([location1]); + + rawFlows = [ + { + locations: [location1, location2] + } + ]; + expect(getLocations(rawFlows, undefined)).toEqual([location2, location1]); + + rawFlows = [ + { + locations: [location1, location2] + }, + { + locations: [] + } + ]; + expect(getLocations(rawFlows, 0)).toEqual([location2, location1]); + }); +}); 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 4d2f505c4f2..fa176838b89 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 @@ -31,6 +31,7 @@ export interface HotspotCategoryProps { onHotspotClick: (hotspot: RawHotspot) => void; onToggleExpand?: (categoryKey: string, value: boolean) => void; selectedHotspot: RawHotspot; + selectedHotspotLocation?: number; title: string; isLastAndIncomplete: boolean; } 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 index 482112477b2..8fa66008174 100644 --- 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 @@ -96,3 +96,7 @@ .hotspot-risk-badge.LOW { background-color: var(--yellow); } + +.hotspot-box-filename { + direction: rtl; +} 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 8c78d98ab5e..afa5194f180 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 @@ -39,6 +39,7 @@ interface Props { onLoadMore: () => void; securityCategories: StandardSecurityCategories; selectedHotspot: RawHotspot; + selectedHotspotLocation?: number; statusFilter: HotspotStatusFilter; } 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 ed241c168dc..7865080337c 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 @@ -19,9 +19,11 @@ */ import classNames from 'classnames'; import * as React from 'react'; -import { translate } from '../../../helpers/l10n'; +import QualifierIcon from '../../../components/icons/QualifierIcon'; +import { ComponentQualifier } from '../../../types/component'; +import { getFilePath, getLocations } from '../utils'; +import LocationsList from '../../../components/locations/LocationsList'; import { RawHotspot } from '../../../types/security-hotspots'; -import { getStatusOptionFromStatusAndResolution } from '../utils'; export interface HotspotListItemProps { hotspot: RawHotspot; @@ -31,16 +33,36 @@ export interface HotspotListItemProps { export default function HotspotListItem(props: HotspotListItemProps) { const { hotspot, selected } = props; + const locations = getLocations(hotspot.flows, undefined); + const path = getFilePath(hotspot.component, hotspot.project); + return ( <a className={classNames('hotspot-item', { highlight: selected })} href="#" onClick={() => !selected && props.onClick(hotspot)}> - <div className="little-spacer-left">{hotspot.message}</div> - <div className="badge spacer-top"> - {translate( - 'hotspots.status_option', - getStatusOptionFromStatusAndResolution(hotspot.status, hotspot.resolution) + <div className="little-spacer-left text-bold">{hotspot.message}</div> + <div className="display-flex-center"> + <QualifierIcon qualifier={ComponentQualifier.File} /> + <div + className="little-spacer-left hotspot-box-filename text-ellipsis big-spacer-top big-spacer-bottom" + title={path}> + {path} + </div> + </div> + <div className="spacer-top"> + {selected && ( + <LocationsList + locations={locations} + isCrossFile={false} // Currently we are not supporting cross file for security hotspot + uniqueKey={hotspot.key} + onLocationSelect={() => { + /* noop */ + }} + scroll={() => { + /* noop */ + }} + /> )} </div> </a> diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap index b959dae2d97..0d0f163550e 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap @@ -7,15 +7,26 @@ exports[`should render correctly 1`] = ` onClick={[Function]} > <div - className="little-spacer-left" + className="little-spacer-left text-bold" > '3' is a magic number. </div> <div - className="badge spacer-top" + className="display-flex-center" > - hotspots.status_option.TO_REVIEW + <QualifierIcon + qualifier="FIL" + /> + <div + className="little-spacer-left hotspot-box-filename text-ellipsis big-spacer-top big-spacer-bottom" + title="com.github.kevinsawicki.http.HttpRequest" + > + com.github.kevinsawicki.http.HttpRequest + </div> </div> + <div + className="spacer-top" + /> </a> `; @@ -26,14 +37,33 @@ exports[`should render correctly 2`] = ` onClick={[Function]} > <div - className="little-spacer-left" + className="little-spacer-left text-bold" > '3' is a magic number. </div> <div - className="badge spacer-top" + className="display-flex-center" + > + <QualifierIcon + qualifier="FIL" + /> + <div + className="little-spacer-left hotspot-box-filename text-ellipsis big-spacer-top big-spacer-bottom" + title="com.github.kevinsawicki.http.HttpRequest" + > + com.github.kevinsawicki.http.HttpRequest + </div> + </div> + <div + className="spacer-top" > - hotspots.status_option.TO_REVIEW + <LocationsList + isCrossFile={false} + locations={Array []} + onLocationSelect={[Function]} + scroll={[Function]} + uniqueKey="01fc972e-2a3c-433e-bcae-0bd7f88f5123" + /> </div> </a> `; 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 b50a669065c..cdcdd8cae45 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,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 { groupBy, sortBy } from 'lodash'; +import { flatten, groupBy, sortBy } from 'lodash'; import { renderCWECategory, renderOwaspTop10Category, @@ -36,7 +36,12 @@ import { ReviewHistoryType, RiskExposure } from '../../types/security-hotspots'; -import { Dict, SourceViewerFile, StandardSecurityCategories } from '../../types/types'; +import { + Dict, + FlowLocation, + SourceViewerFile, + StandardSecurityCategories +} from '../../types/types'; export const RISK_EXPOSURE_LEVELS = [RiskExposure.HIGH, RiskExposure.MEDIUM, RiskExposure.LOW]; export const SECURITY_STANDARDS = [ @@ -190,3 +195,46 @@ const STATUS_OPTION_TO_STATUS_FILTER = { export function getStatusFilterFromStatusOption(statusOption: HotspotStatusOption) { return STATUS_OPTION_TO_STATUS_FILTER[statusOption]; } + +function getSecondaryLocations(flows: RawHotspot['flows']) { + const parsedFlows: FlowLocation[][] = (flows || []) + .filter(flow => flow.locations !== undefined) + .map(flow => flow.locations!.filter(location => location.textRange != null)) + .map(flow => + flow.map(location => { + return { ...location }; + }) + ); + + const onlySecondaryLocations = parsedFlows.every(flow => flow.length === 1); + + return onlySecondaryLocations + ? { secondaryLocations: orderLocations(flatten(parsedFlows)), flows: [] } + : { secondaryLocations: [], flows: parsedFlows.map(reverseLocations) }; +} + +export function getLocations(rawFlows: RawHotspot['flows'], selectedFlowIndex: number | undefined) { + const { flows, secondaryLocations } = getSecondaryLocations(rawFlows); + if (selectedFlowIndex !== undefined) { + return flows[selectedFlowIndex] || []; + } + return flows.length > 0 ? flows[0] : secondaryLocations; +} + +function orderLocations(locations: FlowLocation[]) { + return sortBy( + locations, + location => location.textRange && location.textRange.startLine, + location => location.textRange && location.textRange.startOffset + ); +} + +function reverseLocations(locations: FlowLocation[]): FlowLocation[] { + const x = [...locations]; + x.reverse(); + return x; +} + +export function getFilePath(component: string, project: string) { + return component.replace(project, '').replace(':', ''); +} |