@@ -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} |
@@ -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} |
@@ -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 |
@@ -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()} |
@@ -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]} |
@@ -12,7 +12,9 @@ exports[`should render correctly 1`] = ` | |||
"status": "TO_REVIEW", | |||
} | |||
} | |||
isProject={true} | |||
isStaticListOfHotspots={true} | |||
loadingMeasure={false} | |||
onBranch={false} | |||
onChangeFilters={[MockFunction]} | |||
onShowAllHotspots={[MockFunction]} |
@@ -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> | |||
); |
@@ -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()} |
@@ -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> | |||
`; |
@@ -42,7 +42,7 @@ export default function Measure({ | |||
value | |||
}: Props) { | |||
if (value === undefined) { | |||
return <span>–</span>; | |||
return <span className={className}>–</span>; | |||
} | |||
if (metricType === 'LEVEL') { |
@@ -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: |