aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps
diff options
context:
space:
mode:
authorJeremy Davis <jeremy.davis@sonarsource.com>2021-06-28 11:50:02 +0200
committersonartech <sonartech@sonarsource.com>2021-06-29 20:03:18 +0000
commitc7b7d416b63af4c861cd117cf45bc590d4f790a4 (patch)
treefc89e29048a66532152b0be5620070077b65f687 /server/sonar-web/src/main/js/apps
parent7d1010bbf9b85fd1b8dc08b1873781c2c67fdafd (diff)
downloadsonarqube-c7b7d416b63af4c861cd117cf45bc590d4f790a4.tar.gz
sonarqube-c7b7d416b63af4c861cd117cf45bc590d4f790a4.zip
SONAR-13184 filter hotspots by file
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx46
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/ComponentCell-test.tsx66
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/ComponentCell-test.tsx.snap139
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/utils.ts14
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/IssueLabel-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx21
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx13
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap3
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/EmptyHotspotsPage.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSimpleList.tsx26
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/EmptyHotspotsPage-test.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSimpleList-test.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/EmptyHotspotsPage-test.tsx.snap36
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSimpleList-test.tsx.snap102
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>
+`;