Browse Source

SONAR-12964 Add hotspots reviewed percent to hotspots page

tags/8.2.0.32929
Jeremy Davis 4 years ago
parent
commit
f8e5e5f691

+ 56
- 3
server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsApp.tsx View File

@@ -20,13 +20,16 @@
import { Location } from 'history';
import * as React from 'react';
import { addNoFooterPageClass, removeNoFooterPageClass } from 'sonar-ui-common/helpers/pages';
import { getMeasures } from '../../api/measures';
import { getSecurityHotspotList, getSecurityHotspots } from '../../api/security-hotspots';
import { withCurrentUser } from '../../components/hoc/withCurrentUser';
import { Router } from '../../components/hoc/withRouter';
import { getLeakValue } from '../../components/measure/utils';
import { getBranchLikeQuery, isPullRequest, isSameBranchLike } from '../../helpers/branch-like';
import { getStandards } from '../../helpers/security-standard';
import { isLoggedIn } from '../../helpers/users';
import { BranchLike } from '../../types/branch-like';
import { ComponentQualifier } from '../../types/component';
import {
HotspotFilters,
HotspotResolution,
@@ -52,8 +55,10 @@ interface State {
hotspotKeys?: string[];
hotspots: RawHotspot[];
hotspotsPageIndex: number;
hotspotsReviewedMeasure?: string;
hotspotsTotal?: number;
loading: boolean;
loadingMeasure: boolean;
loadingMore: boolean;
securityCategories: T.StandardSecurityCategories;
selectedHotspotKey: string | undefined;
@@ -69,6 +74,7 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {

this.state = {
loading: true,
loadingMeasure: false,
loadingMore: false,
hotspots: [],
hotspotsPageIndex: 1,
@@ -129,7 +135,11 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
};

fetchInitialData() {
return Promise.all([getStandards(), this.fetchSecurityHotspots()])
return Promise.all([
getStandards(),
this.fetchSecurityHotspots(),
this.fetchSecurityHotspotsReviewed()
])
.then(([{ sonarsourceSecurity }, { hotspots, paging }]) => {
if (!this.mounted) {
return;
@@ -146,6 +156,38 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
.catch(this.handleCallFailure);
}

fetchSecurityHotspotsReviewed() {
const { branchLike, component } = this.props;
const { filters } = this.state;

const reviewedHotspotsMetricKey = filters.sinceLeakPeriod
? 'new_security_hotspots_reviewed'
: 'security_hotspots_reviewed';

this.setState({ loadingMeasure: true });
return getMeasures({
component: component.key,
metricKeys: reviewedHotspotsMetricKey,
...getBranchLikeQuery(branchLike)
})
.then(measures => {
if (!this.mounted) {
return;
}
const measure = measures && measures.length > 0 ? measures[0] : undefined;
const hotspotsReviewedMeasure = filters.sinceLeakPeriod
? getLeakValue(measure)
: measure?.value;

this.setState({ hotspotsReviewedMeasure, loadingMeasure: false });
})
.catch(() => {
if (this.mounted) {
this.setState({ loadingMeasure: false });
}
});
}

fetchSecurityHotspots(page = 1) {
const { branchLike, component, location } = this.props;
const { filters } = this.state;
@@ -208,7 +250,12 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
handleChangeFilters = (changes: Partial<HotspotFilters>) => {
this.setState(
({ filters }) => ({ filters: { ...filters, ...changes } }),
this.reloadSecurityHotspotList
() => {
this.reloadSecurityHotspotList();
if (changes.sinceLeakPeriod !== undefined) {
this.fetchSecurityHotspotsReviewed();
}
}
);
};

@@ -229,6 +276,7 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
}
return null;
});
return this.fetchSecurityHotspotsReviewed();
};

handleShowAllHotspots = () => {
@@ -259,12 +307,14 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
};

render() {
const { branchLike } = this.props;
const { branchLike, component } = this.props;
const {
hotspotKeys,
hotspots,
hotspotsReviewedMeasure,
hotspotsTotal,
loading,
loadingMeasure,
loadingMore,
securityCategories,
selectedHotspotKey,
@@ -276,9 +326,12 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
branchLike={branchLike}
filters={filters}
hotspots={hotspots}
hotspotsReviewedMeasure={hotspotsReviewedMeasure}
hotspotsTotal={hotspotsTotal}
isProject={component.qualifier === ComponentQualifier.Project}
isStaticListOfHotspots={Boolean(hotspotKeys && hotspotKeys.length > 0)}
loading={loading}
loadingMeasure={loadingMeasure}
loadingMore={loadingMore}
onChangeFilters={this.handleChangeFilters}
onHotspotClick={this.handleHotspotClick}

+ 9
- 0
server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx View File

@@ -42,9 +42,12 @@ export interface SecurityHotspotsAppRendererProps {
branchLike?: BranchLike;
filters: HotspotFilters;
hotspots: RawHotspot[];
hotspotsReviewedMeasure?: string;
hotspotsTotal?: number;
isProject: boolean;
isStaticListOfHotspots: boolean;
loading: boolean;
loadingMeasure: boolean;
loadingMore: boolean;
onChangeFilters: (filters: Partial<HotspotFilters>) => void;
onHotspotClick: (key: string) => void;
@@ -59,9 +62,12 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe
const {
branchLike,
hotspots,
hotspotsReviewedMeasure,
hotspotsTotal,
isProject,
isStaticListOfHotspots,
loading,
loadingMeasure,
loadingMore,
securityCategories,
selectedHotspotKey,
@@ -72,7 +78,10 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe
<div id="security_hotspots">
<FilterBar
filters={filters}
hotspotsReviewedMeasure={hotspotsReviewedMeasure}
isProject={isProject}
isStaticListOfHotspots={isStaticListOfHotspots}
loadingMeasure={loadingMeasure}
onBranch={isBranch(branchLike)}
onChangeFilters={props.onChangeFilters}
onShowAllHotspots={props.onShowAllHotspots}

+ 49
- 7
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsApp-test.tsx View File

@@ -21,6 +21,7 @@ import { shallow } from 'enzyme';
import * as React from 'react';
import { addNoFooterPageClass } from 'sonar-ui-common/helpers/pages';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { getMeasures } from '../../../api/measures';
import { getSecurityHotspotList, getSecurityHotspots } from '../../../api/security-hotspots';
import { mockBranch, mockPullRequest } from '../../../helpers/mocks/branch-like';
import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots';
@@ -47,6 +48,10 @@ jest.mock('sonar-ui-common/helpers/pages', () => ({
removeNoFooterPageClass: jest.fn()
}));

jest.mock('../../../api/measures', () => ({
getMeasures: jest.fn().mockResolvedValue([])
}));

jest.mock('../../../api/security-hotspots', () => ({
getSecurityHotspots: jest.fn().mockResolvedValue({ hotspots: [], paging: { total: 0 } }),
getSecurityHotspotList: jest.fn().mockResolvedValue({ hotspots: [], rules: [] })
@@ -70,10 +75,12 @@ it('should load data correctly', async () => {
total: 1
}
});
(getMeasures as jest.Mock).mockResolvedValue([{ value: '86.6' }]);

const wrapper = shallowRender();

expect(wrapper.state().loading).toBe(true);
expect(wrapper.state().loadingMeasure).toBe(true);

expect(addNoFooterPageClass).toBeCalled();
expect(getStandards).toBeCalled();
@@ -82,6 +89,11 @@ it('should load data correctly', async () => {
branch: branch.name
})
);
expect(getMeasures).toBeCalledWith(
expect.objectContaining({
branch: branch.name
})
);

await waitAndUpdate(wrapper);

@@ -91,8 +103,8 @@ it('should load data correctly', async () => {
expect(wrapper.state().securityCategories).toEqual({
cat1: { title: 'cat 1' }
});
expect(wrapper.state());
expect(wrapper.state().loadingMeasure).toBe(false);
expect(wrapper.state().hotspotsReviewedMeasure).toBe('86.6');
});

it('should load data correctly when hotspot key list is forced', async () => {
@@ -155,11 +167,12 @@ it('should set "leakperiod" filter according to context (branchlike & location q
});

it('should set "assigned to me" filter according to context (logged in & explicit location query)', () => {
expect(shallowRender().state().filters.assignedToMe).toBe(false);
expect(
shallowRender({ location: mockLocation({ query: { assignedToMe: 'true' } }) }).state().filters
.assignedToMe
).toBe(false);
const wrapper = shallowRender();
expect(wrapper.state().filters.assignedToMe).toBe(false);

wrapper.setProps({ location: mockLocation({ query: { assignedToMe: 'true' } }) });
expect(wrapper.state().filters.assignedToMe).toBe(false);

expect(shallowRender({ currentUser: mockLoggedInUser() }).state().filters.assignedToMe).toBe(
false
);
@@ -224,7 +237,9 @@ it('should handle hotspot update', async () => {
status: HotspotStatus.REVIEWED,
resolution: HotspotResolution.SAFE
});
expect(getMeasures).toBeCalled();

await waitAndUpdate(wrapper);
const previousState = wrapper.state();
wrapper.instance().handleHotspotUpdate({
key: 'unknown',
@@ -251,8 +266,11 @@ it('should handle status filter change', async () => {

await waitAndUpdate(wrapper);

expect(getMeasures).toBeCalledTimes(1);

// Set filter to SAFE:
wrapper.instance().handleChangeFilters({ status: HotspotStatusFilter.SAFE });
expect(getMeasures).toBeCalledTimes(1);

expect(getSecurityHotspots).toBeCalledWith(
expect.objectContaining({ status: HotspotStatus.REVIEWED, resolution: HotspotResolution.SAFE })
@@ -274,6 +292,30 @@ it('should handle status filter change', async () => {
expect(wrapper.state().hotspots).toHaveLength(0);
});

it('should handle leakPeriod filter change', async () => {
const hotspots = [mockRawHotspot({ key: 'key1' })];
const hotspots2 = [mockRawHotspot({ key: 'key2' })];
(getSecurityHotspots as jest.Mock)
.mockResolvedValueOnce({ hotspots, paging: { total: 1 } })
.mockResolvedValueOnce({ hotspots: hotspots2, paging: { total: 1 } })
.mockResolvedValueOnce({ hotspots: [], paging: { total: 0 } });

const wrapper = shallowRender();

expect(getSecurityHotspots).toBeCalledWith(
expect.objectContaining({ status: HotspotStatus.TO_REVIEW, resolution: undefined })
);

await waitAndUpdate(wrapper);

expect(getMeasures).toBeCalledTimes(1);

wrapper.instance().handleChangeFilters({ sinceLeakPeriod: true });

expect(getMeasures).toBeCalledTimes(2);
expect(getSecurityHotspots).toBeCalledWith(expect.objectContaining({ sinceLeakPeriod: true }));
});

function shallowRender(props: Partial<SecurityHotspotsApp['props']> = {}) {
return shallow<SecurityHotspotsApp>(
<SecurityHotspotsApp

+ 2
- 0
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx View File

@@ -78,8 +78,10 @@ function shallowRender(props: Partial<SecurityHotspotsAppRendererProps> = {}) {
status: HotspotStatusFilter.TO_REVIEW
}}
hotspots={[]}
isProject={true}
isStaticListOfHotspots={true}
loading={false}
loadingMeasure={false}
loadingMore={false}
onChangeFilters={jest.fn()}
onHotspotClick={jest.fn()}

+ 2
- 0
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap View File

@@ -18,8 +18,10 @@ exports[`should render correctly 1`] = `
}
}
hotspots={Array []}
isProject={true}
isStaticListOfHotspots={false}
loading={true}
loadingMeasure={true}
loadingMore={false}
onChangeFilters={[Function]}
onHotspotClick={[Function]}

+ 2
- 0
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap View File

@@ -12,7 +12,9 @@ exports[`should render correctly 1`] = `
"status": "TO_REVIEW",
}
}
isProject={true}
isStaticListOfHotspots={true}
loadingMeasure={false}
onBranch={false}
onChangeFilters={[MockFunction]}
onShowAllHotspots={[MockFunction]}

+ 74
- 32
server/sonar-web/src/main/js/apps/securityHotspots/components/FilterBar.tsx View File

@@ -18,17 +18,24 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip';
import RadioToggle from 'sonar-ui-common/components/controls/RadioToggle';
import Select from 'sonar-ui-common/components/controls/Select';
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { withCurrentUser } from '../../../components/hoc/withCurrentUser';
import Measure from '../../../components/measure/Measure';
import CoverageRating from '../../../components/ui/CoverageRating';
import { isLoggedIn } from '../../../helpers/users';
import { HotspotFilters, HotspotStatusFilter } from '../../../types/security-hotspots';

export interface FilterBarProps {
currentUser: T.CurrentUser;
filters: HotspotFilters;
hotspotsReviewedMeasure?: string;
isProject: boolean;
isStaticListOfHotspots: boolean;
loadingMeasure: boolean;
onBranch: boolean;
onChangeFilters: (filters: Partial<HotspotFilters>) => void;
onShowAllHotspots: () => void;
@@ -56,7 +63,15 @@ const assigneeFilterOptions = [
];

export function FilterBar(props: FilterBarProps) {
const { currentUser, filters, isStaticListOfHotspots, onBranch } = props;
const {
currentUser,
filters,
hotspotsReviewedMeasure,
isProject,
isStaticListOfHotspots,
loadingMeasure,
onBranch
} = props;

return (
<div className="filter-bar display-flex-center">
@@ -65,46 +80,73 @@ export function FilterBar(props: FilterBarProps) {
{translate('hotspot.filters.show_all')}
</a>
) : (
<>
<h3 className="huge-spacer-right">{translate('hotspot.filters.title')}</h3>
<div className="display-flex-space-between width-100">
<div className="display-flex-center">
<h3 className="huge-spacer-right">{translate('hotspot.filters.title')}</h3>

{isLoggedIn(currentUser) && (
<RadioToggle
className="huge-spacer-right"
name="assignee-filter"
onCheck={(value: AssigneeFilterOption) =>
props.onChangeFilters({ assignedToMe: value === AssigneeFilterOption.ME })
}
options={assigneeFilterOptions}
value={filters.assignedToMe ? AssigneeFilterOption.ME : AssigneeFilterOption.ALL}
/>
)}
{isLoggedIn(currentUser) && (
<RadioToggle
className="huge-spacer-right"
name="assignee-filter"
onCheck={(value: AssigneeFilterOption) =>
props.onChangeFilters({ assignedToMe: value === AssigneeFilterOption.ME })
}
options={assigneeFilterOptions}
value={filters.assignedToMe ? AssigneeFilterOption.ME : AssigneeFilterOption.ALL}
/>
)}

<span className="spacer-right">{translate('status')}</span>
<Select
className="input-medium big-spacer-right"
clearable={false}
onChange={(option: { value: HotspotStatusFilter }) =>
props.onChangeFilters({ status: option.value })
}
options={statusOptions}
searchable={false}
value={filters.status}
/>

{onBranch && (
<span className="spacer-right">{translate('status')}</span>
<Select
className="input-medium big-spacer-right"
clearable={false}
onChange={(option: { value: boolean }) =>
props.onChangeFilters({ sinceLeakPeriod: option.value })
onChange={(option: { value: HotspotStatusFilter }) =>
props.onChangeFilters({ status: option.value })
}
options={periodOptions}
options={statusOptions}
searchable={false}
value={filters.sinceLeakPeriod}
value={filters.status}
/>

{onBranch && (
<Select
className="input-medium big-spacer-right"
clearable={false}
onChange={(option: { value: boolean }) =>
props.onChangeFilters({ sinceLeakPeriod: option.value })
}
options={periodOptions}
searchable={false}
value={filters.sinceLeakPeriod}
/>
)}
</div>

{isProject && (
<div className="display-flex-center">
<span className="little-spacer-right">
{translate('metric.security_hotspots_reviewed.name')}
</span>
<HelpTooltip
className="big-spacer-right"
overlay={translate('hotspots.reviewed.tooltip')}
/>
<DeferredSpinner loading={loadingMeasure}>
{hotspotsReviewedMeasure && <CoverageRating value={hotspotsReviewedMeasure} />}
<Measure
className="spacer-left huge"
metricKey={
onBranch && !filters.sinceLeakPeriod
? 'security_hotspots_reviewed'
: 'new_security_hotspots_reviewed'
}
metricType="PERCENT"
value={hotspotsReviewedMeasure}
/>
</DeferredSpinner>
</div>
)}
</>
</div>
)}
</div>
);

+ 8
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/FilterBar-test.tsx View File

@@ -29,6 +29,12 @@ it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('anonymous');
expect(shallowRender({ currentUser: mockLoggedInUser() })).toMatchSnapshot('logged-in');
expect(shallowRender({ onBranch: false })).toMatchSnapshot('on Pull request');
expect(shallowRender({ hotspotsReviewedMeasure: '23.30' })).toMatchSnapshot(
'with hotspots reviewed measure'
);
expect(shallowRender({ currentUser: mockLoggedInUser(), isProject: false })).toMatchSnapshot(
'non-project'
);
});

it('should render correctly when the list of hotspot is static', () => {
@@ -101,7 +107,9 @@ function shallowRender(props: Partial<FilterBarProps> = {}) {
sinceLeakPeriod: false,
status: HotspotStatusFilter.TO_REVIEW
}}
isProject={true}
isStaticListOfHotspots={false}
loadingMeasure={false}
onBranch={true}
onChangeFilters={jest.fn()}
onShowAllHotspots={jest.fn()}

+ 424
- 151
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/FilterBar-test.tsx.snap View File

@@ -19,58 +19,89 @@ exports[`should render correctly: anonymous 1`] = `
<div
className="filter-bar display-flex-center"
>
<h3
className="huge-spacer-right"
<div
className="display-flex-space-between width-100"
>
hotspot.filters.title
</h3>
<span
className="spacer-right"
>
status
</span>
<Select
className="input-medium big-spacer-right"
clearable={false}
onChange={[Function]}
options={
Array [
Object {
"label": "hotspot.filters.status.to_review",
"value": "TO_REVIEW",
},
Object {
"label": "hotspot.filters.status.fixed",
"value": "FIXED",
},
Object {
"label": "hotspot.filters.status.safe",
"value": "SAFE",
},
]
}
searchable={false}
value="TO_REVIEW"
/>
<Select
className="input-medium big-spacer-right"
clearable={false}
onChange={[Function]}
options={
Array [
Object {
"label": "hotspot.filters.period.since_leak_period",
"value": true,
},
Object {
"label": "hotspot.filters.period.overall",
"value": false,
},
]
}
searchable={false}
value={false}
/>
<div
className="display-flex-center"
>
<h3
className="huge-spacer-right"
>
hotspot.filters.title
</h3>
<span
className="spacer-right"
>
status
</span>
<Select
className="input-medium big-spacer-right"
clearable={false}
onChange={[Function]}
options={
Array [
Object {
"label": "hotspot.filters.status.to_review",
"value": "TO_REVIEW",
},
Object {
"label": "hotspot.filters.status.fixed",
"value": "FIXED",
},
Object {
"label": "hotspot.filters.status.safe",
"value": "SAFE",
},
]
}
searchable={false}
value="TO_REVIEW"
/>
<Select
className="input-medium big-spacer-right"
clearable={false}
onChange={[Function]}
options={
Array [
Object {
"label": "hotspot.filters.period.since_leak_period",
"value": true,
},
Object {
"label": "hotspot.filters.period.overall",
"value": false,
},
]
}
searchable={false}
value={false}
/>
</div>
<div
className="display-flex-center"
>
<span
className="little-spacer-right"
>
metric.security_hotspots_reviewed.name
</span>
<HelpTooltip
className="big-spacer-right"
overlay="hotspots.reviewed.tooltip"
/>
<DeferredSpinner
loading={false}
timeout={100}
>
<Measure
className="spacer-left huge"
metricKey="security_hotspots_reviewed"
metricType="PERCENT"
/>
</DeferredSpinner>
</div>
</div>
</div>
`;

@@ -78,77 +109,194 @@ exports[`should render correctly: logged-in 1`] = `
<div
className="filter-bar display-flex-center"
>
<h3
className="huge-spacer-right"
<div
className="display-flex-space-between width-100"
>
hotspot.filters.title
</h3>
<RadioToggle
className="huge-spacer-right"
disabled={false}
name="assignee-filter"
onCheck={[Function]}
options={
Array [
Object {
"label": "hotspot.filters.assignee.assigned_to_me",
"value": "me",
},
Object {
"label": "hotspot.filters.assignee.all",
"value": "all",
},
]
}
value="all"
/>
<span
className="spacer-right"
<div
className="display-flex-center"
>
<h3
className="huge-spacer-right"
>
hotspot.filters.title
</h3>
<RadioToggle
className="huge-spacer-right"
disabled={false}
name="assignee-filter"
onCheck={[Function]}
options={
Array [
Object {
"label": "hotspot.filters.assignee.assigned_to_me",
"value": "me",
},
Object {
"label": "hotspot.filters.assignee.all",
"value": "all",
},
]
}
value="all"
/>
<span
className="spacer-right"
>
status
</span>
<Select
className="input-medium big-spacer-right"
clearable={false}
onChange={[Function]}
options={
Array [
Object {
"label": "hotspot.filters.status.to_review",
"value": "TO_REVIEW",
},
Object {
"label": "hotspot.filters.status.fixed",
"value": "FIXED",
},
Object {
"label": "hotspot.filters.status.safe",
"value": "SAFE",
},
]
}
searchable={false}
value="TO_REVIEW"
/>
<Select
className="input-medium big-spacer-right"
clearable={false}
onChange={[Function]}
options={
Array [
Object {
"label": "hotspot.filters.period.since_leak_period",
"value": true,
},
Object {
"label": "hotspot.filters.period.overall",
"value": false,
},
]
}
searchable={false}
value={false}
/>
</div>
<div
className="display-flex-center"
>
<span
className="little-spacer-right"
>
metric.security_hotspots_reviewed.name
</span>
<HelpTooltip
className="big-spacer-right"
overlay="hotspots.reviewed.tooltip"
/>
<DeferredSpinner
loading={false}
timeout={100}
>
<Measure
className="spacer-left huge"
metricKey="security_hotspots_reviewed"
metricType="PERCENT"
/>
</DeferredSpinner>
</div>
</div>
</div>
`;

exports[`should render correctly: non-project 1`] = `
<div
className="filter-bar display-flex-center"
>
<div
className="display-flex-space-between width-100"
>
status
</span>
<Select
className="input-medium big-spacer-right"
clearable={false}
onChange={[Function]}
options={
Array [
Object {
"label": "hotspot.filters.status.to_review",
"value": "TO_REVIEW",
},
Object {
"label": "hotspot.filters.status.fixed",
"value": "FIXED",
},
Object {
"label": "hotspot.filters.status.safe",
"value": "SAFE",
},
]
}
searchable={false}
value="TO_REVIEW"
/>
<Select
className="input-medium big-spacer-right"
clearable={false}
onChange={[Function]}
options={
Array [
Object {
"label": "hotspot.filters.period.since_leak_period",
"value": true,
},
Object {
"label": "hotspot.filters.period.overall",
"value": false,
},
]
}
searchable={false}
value={false}
/>
<div
className="display-flex-center"
>
<h3
className="huge-spacer-right"
>
hotspot.filters.title
</h3>
<RadioToggle
className="huge-spacer-right"
disabled={false}
name="assignee-filter"
onCheck={[Function]}
options={
Array [
Object {
"label": "hotspot.filters.assignee.assigned_to_me",
"value": "me",
},
Object {
"label": "hotspot.filters.assignee.all",
"value": "all",
},
]
}
value="all"
/>
<span
className="spacer-right"
>
status
</span>
<Select
className="input-medium big-spacer-right"
clearable={false}
onChange={[Function]}
options={
Array [
Object {
"label": "hotspot.filters.status.to_review",
"value": "TO_REVIEW",
},
Object {
"label": "hotspot.filters.status.fixed",
"value": "FIXED",
},
Object {
"label": "hotspot.filters.status.safe",
"value": "SAFE",
},
]
}
searchable={false}
value="TO_REVIEW"
/>
<Select
className="input-medium big-spacer-right"
clearable={false}
onChange={[Function]}
options={
Array [
Object {
"label": "hotspot.filters.period.since_leak_period",
"value": true,
},
Object {
"label": "hotspot.filters.period.overall",
"value": false,
},
]
}
searchable={false}
value={false}
/>
</div>
</div>
</div>
`;

@@ -156,38 +304,163 @@ exports[`should render correctly: on Pull request 1`] = `
<div
className="filter-bar display-flex-center"
>
<h3
className="huge-spacer-right"
<div
className="display-flex-space-between width-100"
>
hotspot.filters.title
</h3>
<span
className="spacer-right"
<div
className="display-flex-center"
>
<h3
className="huge-spacer-right"
>
hotspot.filters.title
</h3>
<span
className="spacer-right"
>
status
</span>
<Select
className="input-medium big-spacer-right"
clearable={false}
onChange={[Function]}
options={
Array [
Object {
"label": "hotspot.filters.status.to_review",
"value": "TO_REVIEW",
},
Object {
"label": "hotspot.filters.status.fixed",
"value": "FIXED",
},
Object {
"label": "hotspot.filters.status.safe",
"value": "SAFE",
},
]
}
searchable={false}
value="TO_REVIEW"
/>
</div>
<div
className="display-flex-center"
>
<span
className="little-spacer-right"
>
metric.security_hotspots_reviewed.name
</span>
<HelpTooltip
className="big-spacer-right"
overlay="hotspots.reviewed.tooltip"
/>
<DeferredSpinner
loading={false}
timeout={100}
>
<Measure
className="spacer-left huge"
metricKey="new_security_hotspots_reviewed"
metricType="PERCENT"
/>
</DeferredSpinner>
</div>
</div>
</div>
`;

exports[`should render correctly: with hotspots reviewed measure 1`] = `
<div
className="filter-bar display-flex-center"
>
<div
className="display-flex-space-between width-100"
>
status
</span>
<Select
className="input-medium big-spacer-right"
clearable={false}
onChange={[Function]}
options={
Array [
Object {
"label": "hotspot.filters.status.to_review",
"value": "TO_REVIEW",
},
Object {
"label": "hotspot.filters.status.fixed",
"value": "FIXED",
},
Object {
"label": "hotspot.filters.status.safe",
"value": "SAFE",
},
]
}
searchable={false}
value="TO_REVIEW"
/>
<div
className="display-flex-center"
>
<h3
className="huge-spacer-right"
>
hotspot.filters.title
</h3>
<span
className="spacer-right"
>
status
</span>
<Select
className="input-medium big-spacer-right"
clearable={false}
onChange={[Function]}
options={
Array [
Object {
"label": "hotspot.filters.status.to_review",
"value": "TO_REVIEW",
},
Object {
"label": "hotspot.filters.status.fixed",
"value": "FIXED",
},
Object {
"label": "hotspot.filters.status.safe",
"value": "SAFE",
},
]
}
searchable={false}
value="TO_REVIEW"
/>
<Select
className="input-medium big-spacer-right"
clearable={false}
onChange={[Function]}
options={
Array [
Object {
"label": "hotspot.filters.period.since_leak_period",
"value": true,
},
Object {
"label": "hotspot.filters.period.overall",
"value": false,
},
]
}
searchable={false}
value={false}
/>
</div>
<div
className="display-flex-center"
>
<span
className="little-spacer-right"
>
metric.security_hotspots_reviewed.name
</span>
<HelpTooltip
className="big-spacer-right"
overlay="hotspots.reviewed.tooltip"
/>
<DeferredSpinner
loading={false}
timeout={100}
>
<CoverageRating
value="23.30"
/>
<Measure
className="spacer-left huge"
metricKey="security_hotspots_reviewed"
metricType="PERCENT"
value="23.30"
/>
</DeferredSpinner>
</div>
</div>
</div>
`;

+ 1
- 1
server/sonar-web/src/main/js/components/measure/Measure.tsx View File

@@ -42,7 +42,7 @@ export default function Measure({
value
}: Props) {
if (value === undefined) {
return <span>–</span>;
return <span className={className}>–</span>;
}

if (metricType === 'LEVEL') {

+ 2
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -691,6 +691,8 @@ hotspot.filters.period.since_leak_period=New code
hotspot.filters.period.overall=Overall code
hotspot.filters.status.safe=Reviewed as safe
hotspot.filters.show_all=Show all hotspots

hotspots.reviewed.tooltip=Percentage of Security Hotspots reviewed (fixed or safe) among all non-closed Security Hotspots.
hotspots.review_hotspot=Review Hotspot

hotspots.form.title=Mark Security Hotspot as:

Loading…
Cancel
Save