diff options
author | Jeremy Davis <jeremy.davis@sonarsource.com> | 2021-06-28 11:50:02 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2021-06-29 20:03:18 +0000 |
commit | c7b7d416b63af4c861cd117cf45bc590d4f790a4 (patch) | |
tree | fc89e29048a66532152b0be5620070077b65f687 /server/sonar-web/src/main/js/apps | |
parent | 7d1010bbf9b85fd1b8dc08b1873781c2c67fdafd (diff) | |
download | sonarqube-c7b7d416b63af4c861cd117cf45bc590d4f790a4.tar.gz sonarqube-c7b7d416b63af4c861cd117cf45bc590d4f790a4.zip |
SONAR-13184 filter hotspots by file
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
15 files changed, 465 insertions, 33 deletions
diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx index 8d8d08b4ae6..b3cc4f33e22 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx @@ -23,15 +23,16 @@ import BranchIcon from 'sonar-ui-common/components/icons/BranchIcon'; import LinkIcon from 'sonar-ui-common/components/icons/LinkIcon'; import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon'; import { translate } from 'sonar-ui-common/helpers/l10n'; +import { isDiffMetric } from 'sonar-ui-common/helpers/measures'; import { splitPath } from 'sonar-ui-common/helpers/path'; -import { getPathUrlAsString } from 'sonar-ui-common/helpers/urls'; import { getBranchLikeUrl, getComponentDrilldownUrlWithSelection, + getComponentSecurityHotspotsUrl, getProjectUrl } from '../../../helpers/urls'; import { BranchLike } from '../../../types/branch-like'; -import { View } from '../utils'; +import { isFileType, isSecurityReviewMetric, View } from '../utils'; interface Props { branchLike?: BranchLike; @@ -73,42 +74,49 @@ export default class ComponentCell extends React.PureComponent<Props> { <QualifierIcon className="little-spacer-right" qualifier={component.qualifier} /> {head.length > 0 && <span className="note">{head}/</span>} <span>{tail}</span> - {isApp && ( - <> - {component.branch ? ( - <> - <BranchIcon className="spacer-left little-spacer-right" /> - <span className="note">{component.branch}</span> - </> - ) : ( - <span className="spacer-left badge">{translate('branches.main_branch')}</span> - )} - </> - )} + {isApp && + (component.branch ? ( + <> + <BranchIcon className="spacer-left little-spacer-right" /> + <span className="note">{component.branch}</span> + </> + ) : ( + <span className="spacer-left badge">{translate('branches.main_branch')}</span> + ))} </span> ); } render() { const { branchLike, component, metric, rootComponent } = this.props; + + let hotspotsUrl; + if (isFileType(component) && isSecurityReviewMetric(metric.key)) { + hotspotsUrl = getComponentSecurityHotspotsUrl(this.props.rootComponent.key, { + file: component.path, + sinceLeakPeriod: isDiffMetric(metric.key) ? 'true' : undefined + }); + } + return ( <td className="measure-details-component-cell"> <div className="text-ellipsis"> {!component.refKey ? ( - <a + <Link className="link-no-underline" - href={getPathUrlAsString( + to={ + hotspotsUrl || getComponentDrilldownUrlWithSelection( rootComponent.key, component.key, metric.key, branchLike ) - )} + } id={'component-measures-component-link-' + component.key} - onClick={this.handleClick}> + onClick={hotspotsUrl ? undefined : this.handleClick}> {this.renderInner(component.key)} - </a> + </Link> ) : ( <Link className="link-no-underline" diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/ComponentCell-test.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/ComponentCell-test.tsx new file mode 100644 index 00000000000..981ef5a8bbe --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/ComponentCell-test.tsx @@ -0,0 +1,66 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 { shallow } from 'enzyme'; +import * as React from 'react'; +import { mockComponentMeasure, mockMetric } from '../../../../helpers/testMocks'; +import { MetricKey } from '../../../../types/metrics'; +import { enhanceComponent } from '../../utils'; +import ComponentCell from '../ComponentCell'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({}, MetricKey.security_hotspots)).toMatchSnapshot('security review domain'); + + const metric = mockMetric({ key: MetricKey.bugs }); + expect( + shallowRender({ + component: enhanceComponent( + mockComponentMeasure(false, { refKey: 'project-key' }), + { key: metric.key }, + { [metric.key]: metric } + ) + }) + ).toMatchSnapshot('ref component'); +}); + +function shallowRender( + overrides: Partial<ComponentCell['props']> = {}, + metricKey = MetricKey.bugs +) { + const metric = mockMetric({ key: metricKey }); + const component = enhanceComponent( + mockComponentMeasure(true, { + measures: [{ metric: metric.key, value: '1', bestValue: false }] + }), + metric, + { [metric.key]: metric } + ); + + return shallow<ComponentCell>( + <ComponentCell + component={component} + metric={metric} + onClick={jest.fn()} + rootComponent={mockComponentMeasure(false)} + view="list" + {...overrides} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/ComponentCell-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/ComponentCell-test.tsx.snap new file mode 100644 index 00000000000..21d4106dec4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/ComponentCell-test.tsx.snap @@ -0,0 +1,139 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly: default 1`] = ` +<td + className="measure-details-component-cell" +> + <div + className="text-ellipsis" + > + <Link + className="link-no-underline" + id="component-measures-component-link-foo:src/index.tsx" + onClick={[Function]} + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/component_measures", + "query": Object { + "id": "foo", + "metric": "bugs", + "selected": "foo:src/index.tsx", + }, + } + } + > + <span + title="foo:src/index.tsx" + > + <QualifierIcon + className="little-spacer-right" + qualifier="FIL" + /> + <span + className="note" + > + src + / + </span> + <span> + index.tsx + </span> + </span> + </Link> + </div> +</td> +`; + +exports[`should render correctly: ref component 1`] = ` +<td + className="measure-details-component-cell" +> + <div + className="text-ellipsis" + > + <Link + className="link-no-underline" + id="component-measures-component-link-project-key" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/dashboard", + "query": Object { + "branch": undefined, + "id": "project-key", + }, + } + } + > + <span + className="big-spacer-right" + > + <LinkIcon /> + </span> + <span + title="project-key" + > + <QualifierIcon + className="little-spacer-right" + qualifier="TRK" + /> + <span> + Foo + </span> + </span> + </Link> + </div> +</td> +`; + +exports[`should render correctly: security review domain 1`] = ` +<td + className="measure-details-component-cell" +> + <div + className="text-ellipsis" + > + <Link + className="link-no-underline" + id="component-measures-component-link-foo:src/index.tsx" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/security_hotspots", + "query": Object { + "assignedToMe": undefined, + "branch": undefined, + "file": "src/index.tsx", + "hotspots": undefined, + "id": "foo", + "pullRequest": undefined, + "sinceLeakPeriod": undefined, + }, + } + } + > + <span + title="foo:src/index.tsx" + > + <QualifierIcon + className="little-spacer-right" + qualifier="FIL" + /> + <span + className="note" + > + src + / + </span> + <span> + index.tsx + </span> + </span> + </Link> + </div> +</td> +`; diff --git a/server/sonar-web/src/main/js/apps/component-measures/utils.ts b/server/sonar-web/src/main/js/apps/component-measures/utils.ts index e2e869e1ea6..956bb6f655d 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/utils.ts +++ b/server/sonar-web/src/main/js/apps/component-measures/utils.ts @@ -25,6 +25,7 @@ import { isBranch, isPullRequest } from '../../helpers/branch-like'; import { getDisplayMetrics, isDiffMetric } from '../../helpers/measures'; import { BranchLike } from '../../types/branch-like'; import { ComponentQualifier } from '../../types/component'; +import { MetricKey } from '../../types/metrics'; import { bubbles } from './config/bubbles'; import { domains } from './config/domains'; @@ -104,7 +105,7 @@ export function enhanceComponent( return { ...component, value, leak, measures: enhancedMeasures }; } -export function isFileType(component: T.ComponentMeasure): boolean { +export function isFileType(component: { qualifier: string | ComponentQualifier }): boolean { return [ComponentQualifier.File, ComponentQualifier.TestFile].includes( component.qualifier as ComponentQualifier ); @@ -118,6 +119,17 @@ export function isViewType(component: T.ComponentMeasure): boolean { ].includes(component.qualifier as ComponentQualifier); } +export function isSecurityReviewMetric(metricKey: MetricKey | string): boolean { + return [ + MetricKey.security_hotspots, + MetricKey.security_hotspots_reviewed, + MetricKey.security_review_rating, + MetricKey.new_security_hotspots, + MetricKey.new_security_hotspots_reviewed, + MetricKey.new_security_review_rating + ].includes(metricKey as MetricKey); +} + export function banQualityGateMeasure({ measures = [], qualifier diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/IssueLabel-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/IssueLabel-test.tsx.snap index aa95bf48b7d..a172ab650a8 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/IssueLabel-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/IssueLabel-test.tsx.snap @@ -124,6 +124,7 @@ exports[`should render correctly for hotspots 1`] = ` "query": Object { "assignedToMe": undefined, "branch": undefined, + "file": undefined, "hotspots": undefined, "id": "my-project", "pullRequest": "1001", @@ -157,6 +158,7 @@ exports[`should render correctly for hotspots 2`] = ` "query": Object { "assignedToMe": undefined, "branch": undefined, + "file": undefined, "hotspots": undefined, "id": "my-project", "pullRequest": "1001", 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 3ac430ec4de..931e98fc8e9 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 @@ -63,6 +63,7 @@ type Props = DispatchProps & OwnProps; interface State { filterByCategory?: { standard: SecurityStandard; category: string }; filterByCWE?: string; + filterByFile?: string; filters: HotspotFilters; hotspotKeys?: string[]; hotspots: RawHotspot[]; @@ -114,7 +115,8 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { if ( this.props.component.key !== previous.component.key || this.props.location.query.hotspots !== previous.location.query.hotspots || - SECURITY_STANDARDS.some(s => this.props.location.query[s] !== previous.location.query[s]) + SECURITY_STANDARDS.some(s => this.props.location.query[s] !== previous.location.query[s]) || + this.props.location.query.file !== previous.location.query.file ) { this.fetchInitialData(); } @@ -256,7 +258,9 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { const filterByCWE: string | undefined = location.query.cwe; - this.setState({ filterByCategory, filterByCWE, hotspotKeys }); + const filterByFile: string | undefined = location.query.file; + + this.setState({ filterByCategory, filterByCWE, filterByFile, hotspotKeys }); if (hotspotKeys && hotspotKeys.length > 0) { return getSecurityHotspotList(hotspotKeys, { @@ -265,7 +269,7 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { }); } - if (filterByCategory || filterByCWE) { + if (filterByCategory || filterByCWE || filterByFile) { const hotspotFilters: T.Dict<string> = {}; if (filterByCategory) { @@ -274,6 +278,9 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { if (filterByCWE) { hotspotFilters[SecurityStandard.CWE] = filterByCWE; } + if (filterByFile) { + hotspotFilters.files = filterByFile; + } return getSecurityHotspots({ ...hotspotFilters, @@ -281,6 +288,7 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { p: page, ps: PAGE_SIZE, status: HotspotStatus.TO_REVIEW, // we're only interested in unresolved hotspots + sinceLeakPeriod: filters.sinceLeakPeriod && Boolean(filterByFile), // only add leak period when filtering by file ...getBranchLikeQuery(branchLike) }); } @@ -379,7 +387,8 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { [SecurityStandard.CWE]: undefined, [SecurityStandard.OWASP_TOP10]: undefined, [SecurityStandard.SANS_TOP25]: undefined, - [SecurityStandard.SONARSOURCE]: undefined + [SecurityStandard.SONARSOURCE]: undefined, + file: undefined } }); }; @@ -409,6 +418,7 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { const { filterByCategory, filterByCWE, + filterByFile, filters, hotspotKeys, hotspots, @@ -428,11 +438,12 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { filters={filters} filterByCategory={filterByCategory} filterByCWE={filterByCWE} + filterByFile={filterByFile} hotspots={hotspots} hotspotsReviewedMeasure={hotspotsReviewedMeasure} hotspotsTotal={hotspotsTotal} isStaticListOfHotspots={Boolean( - (hotspotKeys && hotspotKeys.length > 0) || filterByCategory || filterByCWE + (hotspotKeys && hotspotKeys.length > 0) || filterByCategory || filterByCWE || filterByFile )} loading={loading} loadingMeasure={loadingMeasure} 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 4a8b9ab78f2..d18e906686e 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 @@ -44,6 +44,7 @@ export interface SecurityHotspotsAppRendererProps { category: string; }; filterByCWE?: string; + filterByFile?: string; filters: HotspotFilters; hotspots: RawHotspot[]; hotspotsReviewedMeasure?: string; @@ -68,6 +69,7 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe component, filterByCategory, filterByCWE, + filterByFile, filters, hotspots, hotspotsReviewedMeasure, @@ -125,6 +127,7 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe (isBranch(branchLike) && filters.sinceLeakPeriod) || filters.status !== HotspotStatusFilter.TO_REVIEW } + filterByFile={Boolean(filterByFile)} isStaticListOfHotspots={isStaticListOfHotspots} /> ) : ( @@ -133,10 +136,11 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe {({ top }) => ( <div className="layout-page-side" ref={scrollableRef} style={{ top }}> <div className="layout-page-side-inner"> - {filterByCategory || filterByCWE ? ( + {filterByCategory || filterByCWE || filterByFile ? ( <HotspotSimpleList filterByCategory={filterByCategory} filterByCWE={filterByCWE} + filterByFile={filterByFile} hotspots={hotspots} hotspotsTotal={hotspotsTotal} loadingMore={loadingMore} diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx index fd0b36779f7..0124c3a841a 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx @@ -129,6 +129,19 @@ it('should handle cwe request', () => { ); }); +it('should handle file request', () => { + (getStandards as jest.Mock).mockResolvedValue(mockStandards()); + (getMeasures as jest.Mock).mockResolvedValue([{ value: '86.6' }]); + + const filepath = 'src/path/to/file.java'; + + shallowRender({ + location: mockLocation({ query: { file: filepath } }) + }); + + expect(getSecurityHotspots).toBeCalledWith(expect.objectContaining({ files: filepath })); +}); + it('should load data correctly when hotspot key list is forced', async () => { const hotspots = [ mockRawHotspot({ key: 'test1' }), diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap index a5e4b851798..97577c3aa1f 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap @@ -52,6 +52,7 @@ exports[`should render correctly 1`] = ` onShowAllHotspots={[MockFunction]} /> <EmptyHotspotsPage + filterByFile={false} filtered={false} isStaticListOfHotspots={true} /> @@ -353,6 +354,7 @@ exports[`should render correctly with hotspots 1`] = ` onShowAllHotspots={[MockFunction]} /> <EmptyHotspotsPage + filterByFile={false} filtered={false} isStaticListOfHotspots={true} /> @@ -552,6 +554,7 @@ exports[`should render correctly: no hotspots with filters 1`] = ` onShowAllHotspots={[MockFunction]} /> <EmptyHotspotsPage + filterByFile={false} filtered={true} isStaticListOfHotspots={true} /> diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/EmptyHotspotsPage.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/EmptyHotspotsPage.tsx index 0b766cc3dd8..475335b4f97 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/EmptyHotspotsPage.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/EmptyHotspotsPage.tsx @@ -24,14 +24,17 @@ import { getBaseUrl } from 'sonar-ui-common/helpers/urls'; export interface EmptyHotspotsPageProps { filtered: boolean; + filterByFile: boolean; isStaticListOfHotspots: boolean; } export default function EmptyHotspotsPage(props: EmptyHotspotsPageProps) { - const { filtered, isStaticListOfHotspots } = props; + const { filtered, filterByFile, isStaticListOfHotspots } = props; let translationRoot; - if (isStaticListOfHotspots) { + if (filterByFile) { + translationRoot = 'no_hotspots_for_file'; + } else if (isStaticListOfHotspots) { translationRoot = 'no_hotspots_for_keys'; } else if (filtered) { translationRoot = 'no_hotspots_for_filters'; @@ -45,7 +48,9 @@ export default function EmptyHotspotsPage(props: EmptyHotspotsPageProps) { alt={translate('hotspots.page')} className="huge-spacer-top" height={100} - src={`${getBaseUrl()}/images/${filtered ? 'filter-large' : 'hotspot-large'}.svg`} + src={`${getBaseUrl()}/images/${ + filtered && !filterByFile ? 'filter-large' : 'hotspot-large' + }.svg`} /> <h1 className="huge-spacer-top">{translate(`hotspots.${translationRoot}.title`)}</h1> <div className="abs-width-400 text-center big-spacer-top"> diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSimpleList.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSimpleList.tsx index 97951b7ffb2..58d0db29f12 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSimpleList.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSimpleList.tsx @@ -19,9 +19,13 @@ */ import * as React from 'react'; import ListFooter from 'sonar-ui-common/components/controls/ListFooter'; +import Tooltip from 'sonar-ui-common/components/controls/Tooltip'; +import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon'; import SecurityHotspotIcon from 'sonar-ui-common/components/icons/SecurityHotspotIcon'; import { translateWithParameters } from 'sonar-ui-common/helpers/l10n'; import { addSideBarClass, removeSideBarClass } from 'sonar-ui-common/helpers/pages'; +import { fileFromPath } from 'sonar-ui-common/helpers/path'; +import { ComponentQualifier } from '../../../types/component'; import { SecurityStandard, Standards } from '../../../types/security'; import { RawHotspot } from '../../../types/security-hotspots'; import { SECURITY_STANDARD_RENDERER } from '../utils'; @@ -33,6 +37,7 @@ export interface HotspotSimpleListProps { category: string; }; filterByCWE?: string; + filterByFile?: string; hotspots: RawHotspot[]; hotspotsTotal: number; loadingMore: boolean; @@ -55,6 +60,7 @@ export default class HotspotSimpleList extends React.Component<HotspotSimpleList const { filterByCategory, filterByCWE, + filterByFile, hotspots, hotspotsTotal, loadingMore, @@ -79,9 +85,23 @@ export default class HotspotSimpleList extends React.Component<HotspotSimpleList <div className="hotspot-category"> <div className="hotspot-category-header"> <strong className="flex-1 spacer-right break-word"> - {categoryLabel} - {categoryLabel && cweLabel && <hr />} - {cweLabel} + {filterByFile ? ( + <Tooltip overlay={filterByFile}> + <span> + <QualifierIcon + className="little-spacer-right" + qualifier={ComponentQualifier.File} + /> + {fileFromPath(filterByFile)} + </span> + </Tooltip> + ) : ( + <> + {categoryLabel} + {categoryLabel && cweLabel && <hr />} + {cweLabel} + </> + )} </strong> </div> <ul> diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/EmptyHotspotsPage-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/EmptyHotspotsPage-test.tsx index 3864db17a54..5deec43a541 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/EmptyHotspotsPage-test.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/EmptyHotspotsPage-test.tsx @@ -25,8 +25,16 @@ it('should render correctly', () => { expect(shallowRender()).toMatchSnapshot(); expect(shallowRender({ filtered: true })).toMatchSnapshot('filtered'); expect(shallowRender({ isStaticListOfHotspots: true })).toMatchSnapshot('keys'); + expect(shallowRender({ filterByFile: true })).toMatchSnapshot('file'); }); function shallowRender(props: Partial<EmptyHotspotsPageProps> = {}) { - return shallow(<EmptyHotspotsPage filtered={false} isStaticListOfHotspots={false} {...props} />); + return shallow( + <EmptyHotspotsPage + filtered={false} + filterByFile={false} + isStaticListOfHotspots={false} + {...props} + /> + ); } diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSimpleList-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSimpleList-test.tsx index 930f45edbcf..ee28eb15ef2 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSimpleList-test.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSimpleList-test.tsx @@ -36,6 +36,9 @@ it('should render correctly', () => { 'filter by cwe' ); expect(shallowRender({ filterByCWE: '327' })).toMatchSnapshot('filter by both'); + expect(shallowRender({ filterByFile: 'src/apps/something/main.ts' })).toMatchSnapshot( + 'filter by file' + ); }); it('should add/remove sidebar classes', async () => { diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/EmptyHotspotsPage-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/EmptyHotspotsPage-test.tsx.snap index b90efd1eb50..9ea274c206a 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/EmptyHotspotsPage-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/EmptyHotspotsPage-test.tsx.snap @@ -36,6 +36,42 @@ exports[`should render correctly 1`] = ` </div> `; +exports[`should render correctly: file 1`] = ` +<div + className="display-flex-column display-flex-center huge-spacer-top" +> + <img + alt="hotspots.page" + className="huge-spacer-top" + height={100} + src="/images/hotspot-large.svg" + /> + <h1 + className="huge-spacer-top" + > + hotspots.no_hotspots_for_file.title + </h1> + <div + className="abs-width-400 text-center big-spacer-top" + > + hotspots.no_hotspots_for_file.description + </div> + <Link + className="big-spacer-top" + onlyActiveOnIndex={false} + style={Object {}} + target="_blank" + to={ + Object { + "pathname": "/documentation/user-guide/security-hotspots/", + } + } + > + hotspots.learn_more + </Link> +</div> +`; + exports[`should render correctly: filtered 1`] = ` <div className="display-flex-column display-flex-center huge-spacer-top" diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSimpleList-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSimpleList-test.tsx.snap index ca7d9427533..950f99e0f46 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSimpleList-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSimpleList-test.tsx.snap @@ -277,3 +277,105 @@ exports[`should render correctly: filter by cwe 1`] = ` /> </div> `; + +exports[`should render correctly: filter by file 1`] = ` +<div + className="hotspots-list-single-category huge-spacer-bottom" +> + <h1 + className="hotspot-list-header bordered-bottom" + > + <SecurityHotspotIcon + className="spacer-right" + /> + hotspots.list_title.2 + </h1> + <div + className="big-spacer-bottom" + > + <div + className="hotspot-category" + > + <div + className="hotspot-category-header" + > + <strong + className="flex-1 spacer-right break-word" + > + <Tooltip + overlay="src/apps/something/main.ts" + > + <span> + <QualifierIcon + className="little-spacer-right" + qualifier="FIL" + /> + main.ts + </span> + </Tooltip> + </strong> + </div> + <ul> + <li + data-hotspot-key="h1" + key="h1" + > + <HotspotListItem + hotspot={ + Object { + "author": "Developer 1", + "component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest", + "creationDate": "2013-05-13T17:55:39+0200", + "key": "h1", + "line": 81, + "message": "'3' is a magic number.", + "project": "com.github.kevinsawicki:http-request", + "resolution": undefined, + "rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck", + "securityCategory": "command-injection", + "status": "TO_REVIEW", + "updateDate": "2013-05-13T17:55:39+0200", + "vulnerabilityProbability": "HIGH", + } + } + onClick={[MockFunction]} + selected={true} + /> + </li> + <li + data-hotspot-key="h2" + key="h2" + > + <HotspotListItem + hotspot={ + Object { + "author": "Developer 1", + "component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest", + "creationDate": "2013-05-13T17:55:39+0200", + "key": "h2", + "line": 81, + "message": "'3' is a magic number.", + "project": "com.github.kevinsawicki:http-request", + "resolution": undefined, + "rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck", + "securityCategory": "command-injection", + "status": "TO_REVIEW", + "updateDate": "2013-05-13T17:55:39+0200", + "vulnerabilityProbability": "HIGH", + } + } + onClick={[MockFunction]} + selected={false} + /> + </li> + </ul> + </div> + </div> + <ListFooter + count={2} + loadMore={[MockFunction]} + loading={false} + total={2} + /> +</div> +`; |