From: Jeremy Davis Date: Tue, 17 Dec 2019 14:17:54 +0000 (+0100) Subject: SONAR-12751 Add Select to filter by status X-Git-Tag: 8.2.0.32929~178 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=acdeca733bb93e0c9d657c577fce7a2cfdd12574;p=sonarqube.git SONAR-12751 Add Select to filter by status --- diff --git a/server/sonar-web/src/main/js/api/security-hotspots.ts b/server/sonar-web/src/main/js/api/security-hotspots.ts index cea427e8c12..5806bed3679 100644 --- a/server/sonar-web/src/main/js/api/security-hotspots.ts +++ b/server/sonar-web/src/main/js/api/security-hotspots.ts @@ -23,8 +23,10 @@ import { BranchParameters } from '../types/branch-like'; import { DetailedHotspot, HotspotAssignRequest, + HotspotResolution, HotspotSearchResponse, - HotspotSetStatusRequest + HotspotSetStatusRequest, + HotspotStatus } from '../types/security-hotspots'; export function assignSecurityHotspot( @@ -48,6 +50,8 @@ export function getSecurityHotspots( projectKey: string; p: number; ps: number; + status?: HotspotStatus; + resolution?: HotspotResolution; } & BranchParameters ): Promise { return getJSON('/api/hotspots/search', data).catch(throwGlobalError); diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsApp.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsApp.tsx index 768643395e2..72f28db4099 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsApp.tsx +++ b/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsApp.tsx @@ -23,7 +23,13 @@ import { getSecurityHotspots } from '../../api/security-hotspots'; import { getBranchLikeQuery } from '../../helpers/branch-like'; import { getStandards } from '../../helpers/security-standard'; import { BranchLike } from '../../types/branch-like'; -import { HotspotUpdate, RawHotspot } from '../../types/security-hotspots'; +import { + HotspotResolution, + HotspotStatus, + HotspotStatusFilters, + HotspotUpdate, + RawHotspot +} from '../../types/security-hotspots'; import SecurityHotspotsAppRenderer from './SecurityHotspotsAppRenderer'; import './styles.css'; import { sortHotspots } from './utils'; @@ -40,6 +46,7 @@ interface State { loading: boolean; securityCategories: T.StandardSecurityCategories; selectedHotspotKey: string | undefined; + statusFilter: HotspotStatusFilters; } export default class SecurityHotspotsApp extends React.PureComponent { @@ -48,7 +55,8 @@ export default class SecurityHotspotsApp extends React.PureComponent { + if (this.mounted) { + this.setState({ loading: false }); + } + }; - return Promise.all([ - getStandards(), - getSecurityHotspots({ - projectKey: component.key, - p: 1, - ps: PAGE_SIZE, - ...getBranchLikeQuery(branchLike) - }) - ]) + fetchInitialData() { + return Promise.all([getStandards(), this.fetchSecurityHotspots()]) .then(([{ sonarsourceSecurity }, response]) => { if (!this.mounted) { return; @@ -94,13 +98,57 @@ export default class SecurityHotspotsApp extends React.PureComponent 0 ? hotspots[0].key : undefined }); }) - .catch(() => { - if (this.mounted) { - this.setState({ loading: false }); - } - }); + .catch(this.handleCallFailure); } + fetchSecurityHotspots() { + const { branchLike, component } = this.props; + const { statusFilter } = this.state; + + const status = + statusFilter === HotspotStatusFilters.TO_REVIEW + ? HotspotStatus.TO_REVIEW + : HotspotStatus.REVIEWED; + + const resolution = + statusFilter === HotspotStatusFilters.TO_REVIEW ? undefined : HotspotResolution[statusFilter]; + + return getSecurityHotspots({ + projectKey: component.key, + p: 1, + ps: PAGE_SIZE, + status, + resolution, + ...getBranchLikeQuery(branchLike) + }); + } + + reloadSecurityHotspotList = () => { + const { securityCategories } = this.state; + + this.setState({ loading: true }); + + return this.fetchSecurityHotspots() + .then(response => { + if (!this.mounted) { + return; + } + + const hotspots = sortHotspots(response.hotspots, securityCategories); + + this.setState({ + hotspots, + loading: false, + selectedHotspotKey: hotspots.length > 0 ? hotspots[0].key : undefined + }); + }) + .catch(this.handleCallFailure); + }; + + handleChangeStatusFilter = (statusFilter: HotspotStatusFilters) => { + this.setState({ statusFilter }, this.reloadSecurityHotspotList); + }; + handleHotspotClick = (key: string) => this.setState({ selectedHotspotKey: key }); handleHotspotUpdate = ({ key, status, resolution }: HotspotUpdate) => { @@ -122,17 +170,19 @@ export default class SecurityHotspotsApp extends React.PureComponent ); } diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx index 9ba9dc43870..cf7d1b7df27 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx @@ -27,7 +27,7 @@ import A11ySkipTarget from '../../app/components/a11y/A11ySkipTarget'; import Suggestions from '../../app/components/embed-docs-modal/Suggestions'; import ScreenPositionHelper from '../../components/common/ScreenPositionHelper'; import { BranchLike } from '../../types/branch-like'; -import { HotspotUpdate, RawHotspot } from '../../types/security-hotspots'; +import { HotspotStatusFilters, HotspotUpdate, RawHotspot } from '../../types/security-hotspots'; import FilterBar from './components/FilterBar'; import HotspotList from './components/HotspotList'; import HotspotViewer from './components/HotspotViewer'; @@ -37,18 +37,27 @@ export interface SecurityHotspotsAppRendererProps { branchLike?: BranchLike; hotspots: RawHotspot[]; loading: boolean; + onChangeStatusFilter: (status: HotspotStatusFilters) => void; onHotspotClick: (key: string) => void; onUpdateHotspot: (hotspot: HotspotUpdate) => void; selectedHotspotKey?: string; securityCategories: T.StandardSecurityCategories; + statusFilter: HotspotStatusFilters; } export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRendererProps) { - const { branchLike, hotspots, loading, securityCategories, selectedHotspotKey } = props; + const { + branchLike, + hotspots, + loading, + securityCategories, + selectedHotspotKey, + statusFilter + } = props; return (
- + {({ top }) => (
@@ -85,6 +94,7 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe onHotspotClick={props.onHotspotClick} securityCategories={securityCategories} selectedHotspotKey={selectedHotspotKey} + statusFilter={statusFilter} />
diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsApp-test.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsApp-test.tsx index 9e73887780f..887a2d53d4d 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsApp-test.tsx +++ b/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsApp-test.tsx @@ -26,7 +26,11 @@ import { mockBranch } from '../../../helpers/mocks/branch-like'; import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots'; import { getStandards } from '../../../helpers/security-standard'; import { mockComponent } from '../../../helpers/testMocks'; -import { HotspotResolution, HotspotStatus } from '../../../types/security-hotspots'; +import { + HotspotResolution, + HotspotStatus, + HotspotStatusFilters +} from '../../../types/security-hotspots'; import SecurityHotspotsApp from '../SecurityHotspotsApp'; import SecurityHotspotsAppRenderer from '../SecurityHotspotsAppRenderer'; @@ -54,7 +58,7 @@ it('should load data correctly', async () => { (getStandards as jest.Mock).mockResolvedValue({ sonarsourceSecurity }); const hotspots = [mockRawHotspot()]; - (getSecurityHotspots as jest.Mock).mockResolvedValue({ + (getSecurityHotspots as jest.Mock).mockResolvedValueOnce({ hotspots }); @@ -104,6 +108,45 @@ it('should handle hotspot update', async () => { }); }); +it('should handle status filter change', async () => { + const hotspots = [mockRawHotspot({ key: 'key1' })]; + const hotspots2 = [mockRawHotspot({ key: 'key2' })]; + (getSecurityHotspots as jest.Mock) + .mockResolvedValueOnce({ hotspots }) + .mockResolvedValueOnce({ hotspots: hotspots2 }) + .mockResolvedValueOnce({ hotspots: [] }); + + const wrapper = shallowRender(); + + expect(getSecurityHotspots).toBeCalledWith( + expect.objectContaining({ status: HotspotStatus.TO_REVIEW, resolution: undefined }) + ); + + await waitAndUpdate(wrapper); + + // Set filter to SAFE: + wrapper.instance().handleChangeStatusFilter(HotspotStatusFilters.SAFE); + + expect(getSecurityHotspots).toBeCalledWith( + expect.objectContaining({ status: HotspotStatus.REVIEWED, resolution: HotspotResolution.SAFE }) + ); + + await waitAndUpdate(wrapper); + + expect(wrapper.state().hotspots[0]).toBe(hotspots2[0]); + + // Set filter to FIXED + wrapper.instance().handleChangeStatusFilter(HotspotStatusFilters.FIXED); + + expect(getSecurityHotspots).toBeCalledWith( + expect.objectContaining({ status: HotspotStatus.REVIEWED, resolution: HotspotResolution.FIXED }) + ); + + await waitAndUpdate(wrapper); + + expect(wrapper.state().hotspots).toHaveLength(0); +}); + function shallowRender(props: Partial = {}) { return shallow( diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx index c46274751bf..2cfb546bbbb 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx +++ b/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx @@ -21,6 +21,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots'; +import { HotspotStatusFilters } from '../../../types/security-hotspots'; import SecurityHotspotsAppRenderer, { SecurityHotspotsAppRendererProps } from '../SecurityHotspotsAppRenderer'; @@ -53,9 +54,11 @@ function shallowRender(props: Partial = {}) { ); diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap index b2a09e10b46..4ff7c272040 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap @@ -12,8 +12,10 @@ exports[`should render correctly 1`] = ` } hotspots={Array []} loading={true} + onChangeStatusFilter={[Function]} onHotspotClick={[Function]} onUpdateHotspot={[Function]} securityCategories={Object {}} + statusFilter="TO_REVIEW" /> `; diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap index 4df9e59c2e9..722fc972730 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap @@ -4,7 +4,10 @@ exports[`should render correctly 1`] = `
- + @@ -144,6 +147,7 @@ exports[`should render correctly with hotspots 1`] = ` } onHotspotClick={[MockFunction]} securityCategories={Object {}} + statusFilter="TO_REVIEW" />
void; + statusFilter: HotspotStatusFilters; +} + +const statusOptions: Array<{ label: string; value: string }> = [ + { label: translate('hotspot.filters.status.to_review'), value: HotspotStatusFilters.TO_REVIEW }, + { label: translate('hotspot.filters.status.fixed'), value: HotspotStatusFilters.FIXED }, + { label: translate('hotspot.filters.status.safe'), value: HotspotStatusFilters.SAFE } +]; export default function FilterBar(props: FilterBarProps) { + const { statusFilter } = props; return (
-

Filter

+

{translate('hotspot.filters.title')}

+ + {translate('status')} + +
+`; diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap index 7df6dbe4ac5..2bf9b48a976 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap @@ -14,7 +14,7 @@ exports[`should render correctly 1`] = `
- issue.status.TO_REVIEW + hotspot.status.TO_REVIEW
`; @@ -33,7 +33,7 @@ exports[`should render correctly 2`] = `
- issue.status.TO_REVIEW + hotspot.status.TO_REVIEW
`; diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/styles.css b/server/sonar-web/src/main/js/apps/securityHotspots/styles.css index 43cfe55e449..146175fc7ce 100644 --- a/server/sonar-web/src/main/js/apps/securityHotspots/styles.css +++ b/server/sonar-web/src/main/js/apps/securityHotspots/styles.css @@ -34,7 +34,7 @@ #security_hotspots .filter-bar { max-width: 1280px; margin: 0 auto; - padding: var(--gridSize) 20px; + padding: calc(2 * var(--gridSize)) 20px; border-bottom: 1px solid var(--barBorderColor); } diff --git a/server/sonar-web/src/main/js/types/security-hotspots.ts b/server/sonar-web/src/main/js/types/security-hotspots.ts index 1a137246339..3d2fb32c7e0 100644 --- a/server/sonar-web/src/main/js/types/security-hotspots.ts +++ b/server/sonar-web/src/main/js/types/security-hotspots.ts @@ -33,6 +33,12 @@ export enum HotspotResolution { SAFE = 'SAFE' } +export enum HotspotStatusFilters { + FIXED = 'FIXED', + SAFE = 'SAFE', + TO_REVIEW = 'TO_REVIEW' +} + export enum HotspotStatusOptions { FIXED = 'FIXED', SAFE = 'SAFE', diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 3ddc3dce610..9cebe11abfb 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -649,7 +649,8 @@ hotspots.no_hotspots.title=There are no Security Hotspots to review hotspots.no_hotspots.description=Next time you analyse a piece of code that contains a potential security risk, it will show up here. hotspots.learn_more=Learn more about Security Hotspots hotspots.list_title.TO_REVIEW={0} Security Hotspots to review -hotspots.list_title.REVIEWED={0} reviewed Security Hotspots +hotspots.list_title.FIXED={0} Security Hotspots reviewed as fixed +hotspots.list_title.SAFE={0} Security Hotspots reviewed as safe hotspots.risk_exposure=Review priority: hotspot.category=Category: @@ -660,6 +661,15 @@ hotspot.tabs.vulnerability_description=Are you vulnerable? hotspot.tabs.fix_recommendations=How can you fix it? hotspots.review_hotspot=Review Hotspot +hotspot.status.TO_REVIEW=To review +hotspot.status.FIXED=Fixed +hotspot.status.SAFE=Safe + +hotspot.filters.title=Filters +hotspot.filters.status.to_review=To review +hotspot.filters.status.fixed=Reviewed as fixed +hotspot.filters.status.safe=Reviewed as safe + hotspots.form.title=Mark Security Hotspot as: hotspots.form.assign_to=Assign to: