From 0d1d5eb5c92f664e3f8b3324a6ba01036ad5a3e2 Mon Sep 17 00:00:00 2001 From: Wouter Admiraal Date: Tue, 7 Sep 2021 09:52:54 +0200 Subject: [PATCH] SONAR-14511 Improve Security Hotspot status change flow --- .../src/main/js/app/styles/init/misc.css | 2 +- .../src/main/js/app/styles/init/type.css | 10 +- .../security-hotspots/SecurityHotspotsApp.tsx | 5 + .../SecurityHotspotsAppRenderer.tsx | 3 + .../__tests__/SecurityHotspotsApp-test.tsx | 4 +- .../SecurityHotspotsAppRenderer-test.tsx | 1 + .../SecurityHotspotsApp-test.tsx.snap | 1 + .../security-hotspots/__tests__/utils-test.ts | 16 + .../components/HotspotViewer.tsx | 42 +- .../components/HotspotViewerRenderer.tsx | 27 +- .../components/StatusUpdateSuccessModal.tsx | 102 +++ .../__tests__/HotspotViewer-test.tsx | 32 + .../__tests__/HotspotViewerRenderer-test.tsx | 25 +- .../StatusUpdateSuccessModal-test.tsx | 45 ++ .../__snapshots__/HotspotViewer-test.tsx.snap | 6 + .../HotspotViewerRenderer-test.tsx.snap | 588 +++++++++++++++++- .../StatusUpdateSuccessModal-test.tsx.snap | 110 ++++ .../components/status/Status.tsx | 8 +- .../components/status/StatusSelection.tsx | 10 - .../main/js/apps/security-hotspots/utils.ts | 11 + .../src/main/js/components/controls/Modal.css | 12 + .../resources/org/sonar/l10n/core.properties | 8 +- 22 files changed, 1012 insertions(+), 56 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/StatusUpdateSuccessModal.tsx create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/StatusUpdateSuccessModal-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/StatusUpdateSuccessModal-test.tsx.snap diff --git a/server/sonar-web/src/main/js/app/styles/init/misc.css b/server/sonar-web/src/main/js/app/styles/init/misc.css index 047066b05db..c9fd156cc2b 100644 --- a/server/sonar-web/src/main/js/app/styles/init/misc.css +++ b/server/sonar-web/src/main/js/app/styles/init/misc.css @@ -154,7 +154,7 @@ th.hide-overflow { } .padded { - padding: var(--gridSize); + padding: var(--gridSize) !important; } .big-padded { diff --git a/server/sonar-web/src/main/js/app/styles/init/type.css b/server/sonar-web/src/main/js/app/styles/init/type.css index 0ce0bb1a647..508fe38d2f5 100644 --- a/server/sonar-web/src/main/js/app/styles/init/type.css +++ b/server/sonar-web/src/main/js/app/styles/init/type.css @@ -164,23 +164,23 @@ blockquote cite { small, .small { - font-size: var(--smallFontSize); + font-size: var(--smallFontSize) !important; } .medium { - font-size: var(--mediumFontSize); + font-size: var(--mediumFontSize) !important; } .big { - font-size: var(--bigFontSize); + font-size: var(--bigFontSize) !important; } .huge { - font-size: var(--hugeFontSize); + font-size: var(--hugeFontSize) !important; } .gigantic { - font-size: var(--giganticFontSize); + font-size: var(--giganticFontSize) !important; } .zero-font-size { 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 931e98fc8e9..1a91178d9b5 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 @@ -347,6 +347,10 @@ export class SecurityHotspotsApp extends React.PureComponent { ); }; + handleChangeStatusFilter = (status: HotspotStatusFilter) => { + this.handleChangeFilters({ status }); + }; + handleHotspotClick = (selectedHotspot: RawHotspot) => this.setState({ selectedHotspot }); handleHotspotUpdate = (hotspotKey: string) => { @@ -452,6 +456,7 @@ export class SecurityHotspotsApp extends React.PureComponent { onHotspotClick={this.handleHotspotClick} onLoadMore={this.handleLoadMore} onShowAllHotspots={this.handleShowAllHotspots} + onSwitchStatusFilter={this.handleChangeStatusFilter} onUpdateHotspot={this.handleHotspotUpdate} securityCategories={standards[SecurityStandard.SONARSOURCE]} selectedHotspot={selectedHotspot} 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 178e7d6fd79..8976984bdde 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 @@ -57,6 +57,7 @@ export interface SecurityHotspotsAppRendererProps { onHotspotClick: (hotspot: RawHotspot) => void; onLoadMore: () => void; onShowAllHotspots: () => void; + onSwitchStatusFilter: (option: HotspotStatusFilter) => void; onUpdateHotspot: (hotspotKey: string) => Promise; selectedHotspot: RawHotspot | undefined; securityCategories: T.StandardSecurityCategories; @@ -172,6 +173,8 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe branchLike={branchLike} component={component} hotspotKey={selectedHotspot.key} + hotspotsReviewedMeasure={hotspotsReviewedMeasure} + onSwitchStatusFilter={props.onSwitchStatusFilter} onUpdateHotspot={props.onUpdateHotspot} securityCategories={securityCategories} /> 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 5389d134014..b6d22c370c1 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 @@ -343,8 +343,8 @@ it('should handle status filter change', async () => { expect(wrapper.state().hotspots[0]).toBe(hotspots2[0]); - // Set filter to FIXED - wrapper.instance().handleChangeFilters({ status: HotspotStatusFilter.FIXED }); + // Set filter to FIXED (use the other method to check this one): + wrapper.instance().handleChangeStatusFilter(HotspotStatusFilter.FIXED); expect(getSecurityHotspots).toBeCalledWith( expect.objectContaining({ status: HotspotStatus.REVIEWED, resolution: HotspotResolution.FIXED }) diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx index 49d32e3817c..1eea18597c5 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx @@ -144,6 +144,7 @@ function shallowRender(props: Partial = {}) { onHotspotClick={jest.fn()} onLoadMore={jest.fn()} onShowAllHotspots={jest.fn()} + onSwitchStatusFilter={jest.fn()} onUpdateHotspot={jest.fn()} securityCategories={{}} selectedHotspot={undefined} diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap index 4d28ac2ed8f..4775fd8a595 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap @@ -49,6 +49,7 @@ exports[`should render correctly 1`] = ` onHotspotClick={[Function]} onLoadMore={[Function]} onShowAllHotspots={[Function]} + onSwitchStatusFilter={[Function]} onUpdateHotspot={[Function]} securityCategories={Object {}} standards={ diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts index 2dceed54b54..f62e89a980e 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts +++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts @@ -22,6 +22,7 @@ import { mockUser } from '../../../helpers/testMocks'; import { HotspotResolution, HotspotStatus, + HotspotStatusFilter, HotspotStatusOption, ReviewHistoryType, RiskExposure @@ -29,6 +30,7 @@ import { import { getHotspotReviewHistory, getStatusAndResolutionFromStatusOption, + getStatusFilterFromStatusOption, getStatusOptionFromStatusAndResolution, groupByCategory, mapRules, @@ -260,3 +262,17 @@ describe('getStatusAndResolutionFromStatusOption', () => { }); }); }); + +describe('getStatusFilterFromStatusOption', () => { + it('should return the correct values', () => { + expect(getStatusFilterFromStatusOption(HotspotStatusOption.TO_REVIEW)).toEqual( + HotspotStatusFilter.TO_REVIEW + ); + expect(getStatusFilterFromStatusOption(HotspotStatusOption.SAFE)).toEqual( + HotspotStatusFilter.SAFE + ); + expect(getStatusFilterFromStatusOption(HotspotStatusOption.FIXED)).toEqual( + HotspotStatusFilter.FIXED + ); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx index a5063559257..1b8fa64017b 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx @@ -21,21 +21,30 @@ import * as React from 'react'; import { getSecurityHotspotDetails } from '../../../api/security-hotspots'; import { scrollToElement } from '../../../helpers/scrolling'; import { BranchLike } from '../../../types/branch-like'; -import { Hotspot } from '../../../types/security-hotspots'; +import { + Hotspot, + HotspotStatusFilter, + HotspotStatusOption +} from '../../../types/security-hotspots'; +import { getStatusFilterFromStatusOption } from '../utils'; import HotspotViewerRenderer from './HotspotViewerRenderer'; interface Props { branchLike?: BranchLike; component: T.Component; hotspotKey: string; + hotspotsReviewedMeasure?: string; + onSwitchStatusFilter: (option: HotspotStatusFilter) => void; onUpdateHotspot: (hotspotKey: string) => Promise; securityCategories: T.StandardSecurityCategories; } interface State { hotspot?: Hotspot; + lastStatusChangedTo?: HotspotStatusOption; loading: boolean; commentVisible: boolean; + showStatusUpdateSuccessModal: boolean; } export default class HotspotViewer extends React.PureComponent { @@ -46,7 +55,7 @@ export default class HotspotViewer extends React.PureComponent { constructor(props: Props) { super(props); this.commentTextRef = React.createRef(); - this.state = { loading: false, commentVisible: false }; + this.state = { loading: false, commentVisible: false, showStatusUpdateSuccessModal: false }; } componentDidMount() { @@ -79,10 +88,11 @@ export default class HotspotViewer extends React.PureComponent { .catch(() => this.mounted && this.setState({ loading: false })); }; - handleHotspotUpdate = async (statusUpdate = false) => { + handleHotspotUpdate = async (statusUpdate = false, statusOption?: HotspotStatusOption) => { const { hotspotKey } = this.props; if (statusUpdate) { + this.setState({ lastStatusChangedTo: statusOption, showStatusUpdateSuccessModal: true }); await this.props.onUpdateHotspot(hotspotKey); } else { await this.fetchHotspot(); @@ -106,9 +116,26 @@ export default class HotspotViewer extends React.PureComponent { this.setState({ commentVisible: false }); }; + handleSwitchFilterToStatusOfUpdatedHotspot = () => { + const { lastStatusChangedTo } = this.state; + if (lastStatusChangedTo) { + this.props.onSwitchStatusFilter(getStatusFilterFromStatusOption(lastStatusChangedTo)); + } + }; + + handleCloseStatusUpdateSuccessModal = () => { + this.setState({ showStatusUpdateSuccessModal: false }); + }; + render() { - const { branchLike, component, securityCategories } = this.props; - const { hotspot, loading, commentVisible } = this.state; + const { branchLike, component, hotspotsReviewedMeasure, securityCategories } = this.props; + const { + hotspot, + lastStatusChangedTo, + loading, + commentVisible, + showStatusUpdateSuccessModal + } = this.state; return ( { commentTextRef={this.commentTextRef} commentVisible={commentVisible} hotspot={hotspot} + hotspotsReviewedMeasure={hotspotsReviewedMeasure} + lastStatusChangedTo={lastStatusChangedTo} loading={loading} onCloseComment={this.handleCloseComment} + onCloseStatusUpdateSuccessModal={this.handleCloseStatusUpdateSuccessModal} onOpenComment={this.handleOpenComment} + onSwitchFilterToStatusOfUpdatedHotspot={this.handleSwitchFilterToStatusOfUpdatedHotspot} onUpdateHotspot={this.handleHotspotUpdate} + showStatusUpdateSuccessModal={showStatusUpdateSuccessModal} securityCategories={securityCategories} /> ); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx index 3f9f57d225d..19142ca7c0f 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx @@ -34,7 +34,7 @@ import { } from '../../../helpers/urls'; import { isLoggedIn } from '../../../helpers/users'; import { BranchLike } from '../../../types/branch-like'; -import { Hotspot } from '../../../types/security-hotspots'; +import { Hotspot, HotspotStatusOption } from '../../../types/security-hotspots'; import Assignee from './assignee/Assignee'; import HotspotOpenInIdeButton from './HotspotOpenInIdeButton'; import HotspotReviewHistoryAndComments from './HotspotReviewHistoryAndComments'; @@ -42,18 +42,24 @@ import HotspotSnippetContainer from './HotspotSnippetContainer'; import './HotspotViewer.css'; import HotspotViewerTabs from './HotspotViewerTabs'; import Status from './status/Status'; +import StatusUpdateSuccessModal from './StatusUpdateSuccessModal'; export interface HotspotViewerRendererProps { branchLike?: BranchLike; component: T.Component; currentUser: T.CurrentUser; hotspot?: Hotspot; + hotspotsReviewedMeasure?: string; + lastStatusChangedTo?: HotspotStatusOption; loading: boolean; commentVisible: boolean; commentTextRef: React.RefObject; onOpenComment: () => void; onCloseComment: () => void; - onUpdateHotspot: (statusUpdate?: boolean) => Promise; + onCloseStatusUpdateSuccessModal: () => void; + onUpdateHotspot: (statusUpdate?: boolean, statusOption?: HotspotStatusOption) => Promise; + onSwitchFilterToStatusOfUpdatedHotspot: () => void; + showStatusUpdateSuccessModal: boolean; securityCategories: T.StandardSecurityCategories; } @@ -63,7 +69,10 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) { component, currentUser, hotspot, + hotspotsReviewedMeasure, loading, + lastStatusChangedTo, + showStatusUpdateSuccessModal, securityCategories, commentTextRef, commentVisible @@ -79,6 +88,15 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) { return ( + {showStatusUpdateSuccessModal && ( + + )} + {hotspot && (
@@ -142,7 +160,10 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
- props.onUpdateHotspot(true)} /> + props.onUpdateHotspot(true, statusOption)} + />
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/StatusUpdateSuccessModal.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/StatusUpdateSuccessModal.tsx new file mode 100644 index 00000000000..5af63885f06 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/StatusUpdateSuccessModal.tsx @@ -0,0 +1,102 @@ +/* + * 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 * as classNames from 'classnames'; +import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Button, ButtonLink } from '../../../components/controls/buttons'; +import Modal from '../../../components/controls/Modal'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { formatMeasure } from '../../../helpers/measures'; +import { HotspotStatusOption } from '../../../types/security-hotspots'; + +export interface StatusUpdateSuccessModalProps { + hotspotsReviewedMeasure?: string; + lastStatusChangedTo?: HotspotStatusOption; + onClose: () => void; + onSwitchFilterToStatusOfUpdatedHotspot: () => void; +} + +export default function StatusUpdateSuccessModal(props: StatusUpdateSuccessModalProps) { + const { hotspotsReviewedMeasure, lastStatusChangedTo } = props; + + if (!lastStatusChangedTo) { + return null; + } + + const closingHotspots = lastStatusChangedTo !== HotspotStatusOption.TO_REVIEW; + const statusLabel = translate('hotspots.status_option', lastStatusChangedTo); + const modalTitle = closingHotspots + ? translate('hotspots.congratulations') + : translate('hotspots.update.success'); + + return ( + +
+

+ {modalTitle} +

+
+ +
+ + {translateWithParameters('hotspots.successful_status_change_to_x', statusLabel)} + + ) + }} + /> + {closingHotspots && ( +

+ + {formatMeasure(hotspotsReviewedMeasure, 'PERCENT', { + omitExtraDecimalZeros: true + })} + + ) + }} + /> +

+ )} +
+ +
+ + {translateWithParameters('hotspots.see_x_hotspots', statusLabel)} + + +
+
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewer-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewer-test.tsx index 022472ae760..43410b47afe 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewer-test.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewer-test.tsx @@ -24,6 +24,7 @@ import { getSecurityHotspotDetails } from '../../../../api/security-hotspots'; import { mockComponent } from '../../../../helpers/mocks/component'; import { scrollToElement } from '../../../../helpers/scrolling'; import { waitAndUpdate } from '../../../../helpers/testUtils'; +import { HotspotStatusOption } from '../../../../types/security-hotspots'; import HotspotViewer from '../HotspotViewer'; import HotspotViewerRenderer from '../HotspotViewerRenderer'; @@ -63,6 +64,36 @@ it('should refresh hotspot list on status update', () => { expect(onUpdateHotspot).toHaveBeenCalled(); }); +it('should store last status selected when updating a hotspot status', () => { + const wrapper = shallowRender(); + + expect(wrapper.state().lastStatusChangedTo).toBeUndefined(); + wrapper + .find(HotspotViewerRenderer) + .props() + .onUpdateHotspot(true, HotspotStatusOption.FIXED); + expect(wrapper.state().lastStatusChangedTo).toBe(HotspotStatusOption.FIXED); +}); + +it('should correctly propagate a request to switch the status filter', () => { + const onSwitchStatusFilter = jest.fn(); + const wrapper = shallowRender({ onSwitchStatusFilter }); + + wrapper.instance().handleSwitchFilterToStatusOfUpdatedHotspot(); + expect(onSwitchStatusFilter).not.toBeCalled(); + + wrapper.setState({ lastStatusChangedTo: HotspotStatusOption.FIXED }); + wrapper.instance().handleSwitchFilterToStatusOfUpdatedHotspot(); + expect(onSwitchStatusFilter).toBeCalledWith(HotspotStatusOption.FIXED); +}); + +it('should correctly close the success modal', () => { + const wrapper = shallowRender(); + wrapper.setState({ showStatusUpdateSuccessModal: true }); + wrapper.instance().handleCloseStatusUpdateSuccessModal(); + expect(wrapper.state().showStatusUpdateSuccessModal).toBe(false); +}); + it('should NOT refresh hotspot list on assignee/comment updates', () => { const onUpdateHotspot = jest.fn(); const wrapper = shallowRender({ onUpdateHotspot }); @@ -118,6 +149,7 @@ function shallowRender(props?: Partial) { ({ isLoggedIn: jest.fn(() => true) })); it('should render correctly', () => { - const wrapper = shallowRender(); - expect(wrapper).toMatchSnapshot(); + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ showStatusUpdateSuccessModal: true })).toMatchSnapshot( + 'show success modal' + ); expect(shallowRender({ hotspot: undefined })).toMatchSnapshot('no hotspot'); expect(shallowRender({ hotspot: mockHotspot({ assignee: undefined }) })).toMatchSnapshot( 'unassigned' @@ -47,6 +51,18 @@ it('should render correctly', () => { expect(shallowRender()).toMatchSnapshot('anonymous user'); }); +it('correctly propagates the status change', () => { + const onUpdateHotspot = jest.fn(); + const wrapper = shallowRender({ onUpdateHotspot }); + + wrapper + .find(Status) + .props() + .onStatusChange(HotspotStatusOption.FIXED); + + expect(onUpdateHotspot).toHaveBeenCalledWith(true, HotspotStatusOption.FIXED); +}); + function shallowRender(props?: Partial) { return shallow( ) { commentVisible={false} currentUser={mockCurrentUser()} hotspot={mockHotspot()} + hotspotsReviewedMeasure="75" + lastStatusChangedTo={HotspotStatusOption.FIXED} loading={false} onCloseComment={jest.fn()} + onCloseStatusUpdateSuccessModal={jest.fn()} onOpenComment={jest.fn()} + onSwitchFilterToStatusOfUpdatedHotspot={jest.fn()} onUpdateHotspot={jest.fn()} securityCategories={{ 'sql-injection': { title: 'SQL injection' } }} + showStatusUpdateSuccessModal={false} {...props} /> ); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/StatusUpdateSuccessModal-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/StatusUpdateSuccessModal-test.tsx new file mode 100644 index 00000000000..cbfeb7370e6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/StatusUpdateSuccessModal-test.tsx @@ -0,0 +1,45 @@ +/* + * 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 { HotspotStatusOption } from '../../../../types/security-hotspots'; +import StatusUpdateSuccessModal, { + StatusUpdateSuccessModalProps +} from '../StatusUpdateSuccessModal'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ lastStatusChangedTo: HotspotStatusOption.TO_REVIEW })).toMatchSnapshot( + 'opening hotspots again' + ); + expect(shallowRender({ lastStatusChangedTo: undefined }).type()).toBeNull(); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap index 146963cf4a7..44e0fda6ace 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap @@ -32,7 +32,9 @@ exports[`should render correctly 1`] = ` } loading={true} onCloseComment={[Function]} + onCloseStatusUpdateSuccessModal={[Function]} onOpenComment={[Function]} + onSwitchFilterToStatusOfUpdatedHotspot={[Function]} onUpdateHotspot={[Function]} securityCategories={ Object { @@ -41,6 +43,7 @@ exports[`should render correctly 1`] = ` }, } } + showStatusUpdateSuccessModal={false} /> `; @@ -81,7 +84,9 @@ exports[`should render correctly 2`] = ` } loading={false} onCloseComment={[Function]} + onCloseStatusUpdateSuccessModal={[Function]} onOpenComment={[Function]} + onSwitchFilterToStatusOfUpdatedHotspot={[Function]} onUpdateHotspot={[Function]} securityCategories={ Object { @@ -90,5 +95,6 @@ exports[`should render correctly 2`] = ` }, } } + showStatusUpdateSuccessModal={false} /> `; diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap index 2bb8c5b587c..1180deb78c9 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should render correctly 1`] = ` +exports[`should render correctly: anonymous user 1`] = ` `; -exports[`should render correctly: anonymous user 1`] = ` +exports[`should render correctly: assignee without name 1`] = ` `; -exports[`should render correctly: assignee without name 1`] = ` +exports[`should render correctly: default 1`] = ` `; +exports[`should render correctly: show success modal 1`] = ` + + +
+
+
+ + '3' is a magic number. + +
+ + That rule + + + squid:S2077 + +
+
+
+
+ +
+
+ +
+ + + + hotspots.get_permalink + + +
+
+
+
+
+ + category + + + SQL injection + +
+
+ + hotspots.risk_exposure + +
+ risk_exposure.HIGH +
+
+
+ + assignee + +
+ This a strong message about fixing !

", + "key": "squid:S2077", + "name": "That rule", + "riskDescription": "

This a strong message about risk !

", + "securityCategory": "sql-injection", + "vulnerabilityDescription": "

This a strong message about vulnerability !

", + "vulnerabilityProbability": "HIGH", + }, + "status": "REVIEWED", + "textRange": Object { + "endLine": 142, + "endOffset": 83, + "startLine": 142, + "startOffset": 26, + }, + "updateDate": "2013-05-13T17:55:42+0200", + "users": Array [ + Object { + "active": true, + "local": true, + "login": "assignee", + "name": "John Doe", + }, + Object { + "active": true, + "local": true, + "login": "author", + "name": "John Doe", + }, + ], + } + } + onAssigneeChange={[MockFunction]} + /> +
+
+
+
+ This a strong message about fixing !

", + "key": "squid:S2077", + "name": "That rule", + "riskDescription": "

This a strong message about risk !

", + "securityCategory": "sql-injection", + "vulnerabilityDescription": "

This a strong message about vulnerability !

", + "vulnerabilityProbability": "HIGH", + }, + "status": "REVIEWED", + "textRange": Object { + "endLine": 142, + "endOffset": 83, + "startLine": 142, + "startOffset": 26, + }, + "updateDate": "2013-05-13T17:55:42+0200", + "users": Array [ + Object { + "active": true, + "local": true, + "login": "assignee", + "name": "John Doe", + }, + Object { + "active": true, + "local": true, + "login": "author", + "name": "John Doe", + }, + ], + } + } + onStatusChange={[Function]} + /> +
+
+ This a strong message about fixing !

", + "key": "squid:S2077", + "name": "That rule", + "riskDescription": "

This a strong message about risk !

", + "securityCategory": "sql-injection", + "vulnerabilityDescription": "

This a strong message about vulnerability !

", + "vulnerabilityProbability": "HIGH", + }, + "status": "REVIEWED", + "textRange": Object { + "endLine": 142, + "endOffset": 83, + "startLine": 142, + "startOffset": 26, + }, + "updateDate": "2013-05-13T17:55:42+0200", + "users": Array [ + Object { + "active": true, + "local": true, + "login": "assignee", + "name": "John Doe", + }, + Object { + "active": true, + "local": true, + "login": "author", + "name": "John Doe", + }, + ], + } + } + /> + This a strong message about fixing !

", + "key": "squid:S2077", + "name": "That rule", + "riskDescription": "

This a strong message about risk !

", + "securityCategory": "sql-injection", + "vulnerabilityDescription": "

This a strong message about vulnerability !

", + "vulnerabilityProbability": "HIGH", + }, + "status": "REVIEWED", + "textRange": Object { + "endLine": 142, + "endOffset": 83, + "startLine": 142, + "startOffset": 26, + }, + "updateDate": "2013-05-13T17:55:42+0200", + "users": Array [ + Object { + "active": true, + "local": true, + "login": "assignee", + "name": "John Doe", + }, + Object { + "active": true, + "local": true, + "login": "author", + "name": "John Doe", + }, + ], + } + } + /> + This a strong message about fixing !

", + "key": "squid:S2077", + "name": "That rule", + "riskDescription": "

This a strong message about risk !

", + "securityCategory": "sql-injection", + "vulnerabilityDescription": "

This a strong message about vulnerability !

", + "vulnerabilityProbability": "HIGH", + }, + "status": "REVIEWED", + "textRange": Object { + "endLine": 142, + "endOffset": 83, + "startLine": 142, + "startOffset": 26, + }, + "updateDate": "2013-05-13T17:55:42+0200", + "users": Array [ + Object { + "active": true, + "local": true, + "login": "assignee", + "name": "John Doe", + }, + Object { + "active": true, + "local": true, + "login": "author", + "name": "John Doe", + }, + ], + } + } + onCloseComment={[MockFunction]} + onCommentUpdate={[MockFunction]} + onOpenComment={[MockFunction]} + /> +
+
+`; + exports[`should render correctly: unassigned 1`] = ` +
+

+ hotspots.congratulations +

+
+
+ + hotspots.successful_status_change_to_x.hotspots.status_option.FIXED + , + "status_label": "hotspots.status_option.FIXED", + } + } + /> +

+ + + , + } + } + /> +

+
+
+ + hotspots.see_x_hotspots.hotspots.status_option.FIXED + + +
+ +`; + +exports[`should render correctly: opening hotspots again 1`] = ` + +
+

+ hotspots.update.success +

+
+
+ + hotspots.successful_status_change_to_x.hotspots.status_option.TO_REVIEW + , + "status_label": "hotspots.status_option.TO_REVIEW", + } + } + /> +
+
+ + hotspots.see_x_hotspots.hotspots.status_option.TO_REVIEW + + +
+
+`; diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.tsx index b8538b9743b..a174ee4943b 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.tsx @@ -28,7 +28,7 @@ import DropdownIcon from '../../../../components/icons/DropdownIcon'; import { PopupPlacement } from '../../../../components/ui/popups'; import { translate } from '../../../../helpers/l10n'; import { isLoggedIn } from '../../../../helpers/users'; -import { Hotspot } from '../../../../types/security-hotspots'; +import { Hotspot, HotspotStatusOption } from '../../../../types/security-hotspots'; import { getStatusOptionFromStatusAndResolution } from '../../utils'; import StatusDescription from './StatusDescription'; import StatusSelection from './StatusSelection'; @@ -37,7 +37,7 @@ export interface StatusProps { currentUser: T.CurrentUser; hotspot: Hotspot; - onStatusChange: () => Promise; + onStatusChange: (statusOption: HotspotStatusOption) => Promise; } export function Status(props: StatusProps) { @@ -64,8 +64,8 @@ export function Status(props: StatusProps) { { - await props.onStatusChange(); + onStatusOptionChange={async status => { + await props.onStatusChange(status); setIsOpen(false); }} /> diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelection.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelection.tsx index 8cc96c275ec..3775c2bb329 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelection.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelection.tsx @@ -19,8 +19,6 @@ */ import * as React from 'react'; import { setSecurityHotspotStatus } from '../../../../api/security-hotspots'; -import addGlobalSuccessMessage from '../../../../app/utils/addGlobalSuccessMessage'; -import { translate, translateWithParameters } from '../../../../helpers/l10n'; import { Hotspot, HotspotStatusOption } from '../../../../types/security-hotspots'; import { getStatusAndResolutionFromStatusOption, @@ -88,14 +86,6 @@ export default class StatusSelection extends React.PureComponent { await this.props.onStatusOptionChange(selectedStatus); this.setState({ loading: false }); }) - .then(() => - addGlobalSuccessMessage( - translateWithParameters( - 'hotspots.update.success', - translate('hotspots.status_option', selectedStatus) - ) - ) - ) .catch(() => this.setState({ loading: false })); } }; diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts b/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts index 99f3d242d76..7f7db63d9dd 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts +++ b/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts @@ -29,6 +29,7 @@ import { Hotspot, HotspotResolution, HotspotStatus, + HotspotStatusFilter, HotspotStatusOption, RawHotspot, ReviewHistoryElement, @@ -181,3 +182,13 @@ const STATUS_OPTION_TO_STATUS_AND_RESOLUTION_MAP = { export function getStatusAndResolutionFromStatusOption(statusOption: HotspotStatusOption) { return STATUS_OPTION_TO_STATUS_AND_RESOLUTION_MAP[statusOption]; } + +const STATUS_OPTION_TO_STATUS_FILTER = { + [HotspotStatusOption.TO_REVIEW]: HotspotStatusFilter.TO_REVIEW, + [HotspotStatusOption.FIXED]: HotspotStatusFilter.FIXED, + [HotspotStatusOption.SAFE]: HotspotStatusFilter.SAFE +}; + +export function getStatusFilterFromStatusOption(statusOption: HotspotStatusOption) { + return STATUS_OPTION_TO_STATUS_FILTER[statusOption]; +} diff --git a/server/sonar-web/src/main/js/components/controls/Modal.css b/server/sonar-web/src/main/js/components/controls/Modal.css index 04e3de15a21..ee3d12cd24b 100644 --- a/server/sonar-web/src/main/js/components/controls/Modal.css +++ b/server/sonar-web/src/main/js/components/controls/Modal.css @@ -209,3 +209,15 @@ .modal-foot input[type='button'] { margin-left: var(--gridSize); } + +.modal-foot button:first-of-type, +.modal-foot .button:first-of-type, +.modal-foot input[type='submit']:first-of-type, +.modal-foot input[type='button']:first-of-type { + margin-left: 0; +} + +.modal-foot-clear { + border-top: 0; + background-color: transparent; +} 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 ac4f56e3c04..ba0dde5de24 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -768,6 +768,12 @@ hotspots.status_option.SAFE=Safe hotspots.status_option.SAFE.description=The code is not at risk and doesn't need to be modified. hotspots.get_permalink=Get Permalink hotspots.no_associated_lines=Security Hotspot raised on the following file: +hotspots.congratulations=Congratulations! +hotspots.successfully_changed_to_x=The Security Hotspot was {status_change}. You can find it by changing the top filter to display "{status_label}" Security Hotspots. +hotspots.successful_status_change_to_x=successfully changed to "{0}" +hotspots.x_done_keep_going={percentage} of the Security Hotspots have been reviewed, keep going! +hotspots.see_x_hotspots=See "{0}" Security Hotspots +hotspots.continue_to_next_hotspot=Continue reviewing next Security Hotspot hotspot.filters.title=Filters hotspot.filters.assignee.assigned_to_me=Assigned to me @@ -785,7 +791,7 @@ hotspots.review_hotspot=Review Hotspot hotspots.assign.success=Security Hotspot was successfully assigned to {0} hotspots.assign.unassign.success=Security Hotspot was successfully unassigned -hotspots.update.success=Security Hotspot status was successfully changed to {0} +hotspots.update.success=Update successful #------------------------------------------------------------------------------ # -- 2.39.5