diff options
author | Wouter Admiraal <wouter.admiraal@sonarsource.com> | 2021-09-07 09:52:54 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2021-09-07 20:03:17 +0000 |
commit | 0d1d5eb5c92f664e3f8b3324a6ba01036ad5a3e2 (patch) | |
tree | 9433d37aac129792978580598182d9feecbbddfc /server/sonar-web/src/main/js | |
parent | c70d5b85ce6aab51f5bd9271705c76cac584f547 (diff) | |
download | sonarqube-0d1d5eb5c92f664e3f8b3324a6ba01036ad5a3e2.tar.gz sonarqube-0d1d5eb5c92f664e3f8b3324a6ba01036ad5a3e2.zip |
SONAR-14511 Improve Security Hotspot status change flow
Diffstat (limited to 'server/sonar-web/src/main/js')
21 files changed, 1005 insertions, 55 deletions
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<Props, State> { ); }; + 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<Props, State> { 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<void>; 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<SecurityHotspotsAppRendererProps> = {}) { 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<void>; securityCategories: T.StandardSecurityCategories; } interface State { hotspot?: Hotspot; + lastStatusChangedTo?: HotspotStatusOption; loading: boolean; commentVisible: boolean; + showStatusUpdateSuccessModal: boolean; } export default class HotspotViewer extends React.PureComponent<Props, State> { @@ -46,7 +55,7 @@ export default class HotspotViewer extends React.PureComponent<Props, State> { constructor(props: Props) { super(props); this.commentTextRef = React.createRef<HTMLTextAreaElement>(); - 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<Props, State> { .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<Props, State> { 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 ( <HotspotViewerRenderer @@ -117,10 +144,15 @@ export default class HotspotViewer extends React.PureComponent<Props, State> { 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<HTMLTextAreaElement>; onOpenComment: () => void; onCloseComment: () => void; - onUpdateHotspot: (statusUpdate?: boolean) => Promise<void>; + onCloseStatusUpdateSuccessModal: () => void; + onUpdateHotspot: (statusUpdate?: boolean, statusOption?: HotspotStatusOption) => Promise<void>; + 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 ( <DeferredSpinner className="big-spacer-left big-spacer-top" loading={loading}> + {showStatusUpdateSuccessModal && ( + <StatusUpdateSuccessModal + hotspotsReviewedMeasure={hotspotsReviewedMeasure} + lastStatusChangedTo={lastStatusChangedTo} + onClose={props.onCloseStatusUpdateSuccessModal} + onSwitchFilterToStatusOfUpdatedHotspot={props.onSwitchFilterToStatusOfUpdatedHotspot} + /> + )} + {hotspot && ( <div className="big-padded hotspot-content"> <div className="huge-spacer-bottom display-flex-space-between"> @@ -142,7 +160,10 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) { </div> </div> <div className="huge-spacer-left abs-width-400"> - <Status hotspot={hotspot} onStatusChange={() => props.onUpdateHotspot(true)} /> + <Status + hotspot={hotspot} + onStatusChange={statusOption => props.onUpdateHotspot(true, statusOption)} + /> </div> </div> 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 ( + <Modal contentLabel={modalTitle}> + <div className="modal-head"> + <h2 + className={classNames('huge text-normal', { + 'text-success': closingHotspots + })}> + {modalTitle} + </h2> + </div> + + <div className="modal-body"> + <FormattedMessage + id="hotspots.successfully_changed_to_x" + defaultMessage={translate('hotspots.successfully_changed_to_x')} + values={{ + status_label: statusLabel, + status_change: ( + <strong> + {translateWithParameters('hotspots.successful_status_change_to_x', statusLabel)} + </strong> + ) + }} + /> + {closingHotspots && ( + <p className="spacer-top"> + <FormattedMessage + id="hotspots.x_done_keep_going" + defaultMessage={translate('hotspots.x_done_keep_going')} + values={{ + percentage: ( + <strong> + {formatMeasure(hotspotsReviewedMeasure, 'PERCENT', { + omitExtraDecimalZeros: true + })} + </strong> + ) + }} + /> + </p> + )} + </div> + + <div className="modal-foot modal-foot-clear display-flex-center display-flex-space-between"> + <ButtonLink onClick={props.onSwitchFilterToStatusOfUpdatedHotspot}> + {translateWithParameters('hotspots.see_x_hotspots', statusLabel)} + </ButtonLink> + <Button className="button-primary padded" onClick={props.onClose}> + {translate('hotspots.continue_to_next_hotspot')} + </Button> + </div> + </Modal> + ); +} 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<HotspotViewer['props']>) { <HotspotViewer component={mockComponent()} hotspotKey={hotspotKey} + onSwitchStatusFilter={jest.fn()} onUpdateHotspot={jest.fn()} securityCategories={{ cat1: { title: 'cat1' } }} {...props} diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerRenderer-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerRenderer-test.tsx index 24d1ec995bf..1a681d315fc 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerRenderer-test.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerRenderer-test.tsx @@ -23,13 +23,17 @@ import { mockBranch } from '../../../../helpers/mocks/branch-like'; import { mockComponent } from '../../../../helpers/mocks/component'; import { mockHotspot } from '../../../../helpers/mocks/security-hotspots'; import { mockCurrentUser, mockUser } from '../../../../helpers/testMocks'; +import { HotspotStatusOption } from '../../../../types/security-hotspots'; import { HotspotViewerRenderer, HotspotViewerRendererProps } from '../HotspotViewerRenderer'; +import Status from '../status/Status'; jest.mock('../../../../helpers/users', () => ({ 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<HotspotViewerRendererProps>) { return shallow( <HotspotViewerRenderer @@ -56,11 +72,16 @@ function shallowRender(props?: Partial<HotspotViewerRendererProps>) { 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<StatusUpdateSuccessModalProps> = {}) { + return shallow<StatusUpdateSuccessModalProps>( + <StatusUpdateSuccessModal + onClose={jest.fn()} + lastStatusChangedTo={HotspotStatusOption.FIXED} + onSwitchFilterToStatusOfUpdatedHotspot={jest.fn()} + {...props} + /> + ); +} 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`] = ` <DeferredSpinner className="big-spacer-left big-spacer-top" loading={false} @@ -536,7 +536,7 @@ exports[`should render correctly 1`] = ` </DeferredSpinner> `; -exports[`should render correctly: anonymous user 1`] = ` +exports[`should render correctly: assignee without name 1`] = ` <DeferredSpinner className="big-spacer-left big-spacer-top" loading={false} @@ -664,8 +664,8 @@ exports[`should render correctly: anonymous user 1`] = ` "assigneeUser": Object { "active": true, "local": true, - "login": "assignee", - "name": "John Doe", + "login": "assignee_login", + "name": undefined, }, "author": "author", "authorUser": Object { @@ -744,8 +744,8 @@ exports[`should render correctly: anonymous user 1`] = ` "assigneeUser": Object { "active": true, "local": true, - "login": "assignee", - "name": "John Doe", + "login": "assignee_login", + "name": undefined, }, "author": "author", "authorUser": Object { @@ -842,8 +842,8 @@ exports[`should render correctly: anonymous user 1`] = ` "assigneeUser": Object { "active": true, "local": true, - "login": "assignee", - "name": "John Doe", + "login": "assignee_login", + "name": undefined, }, "author": "author", "authorUser": Object { @@ -915,8 +915,8 @@ exports[`should render correctly: anonymous user 1`] = ` "assigneeUser": Object { "active": true, "local": true, - "login": "assignee", - "name": "John Doe", + "login": "assignee_login", + "name": undefined, }, "author": "author", "authorUser": Object { @@ -999,8 +999,8 @@ exports[`should render correctly: anonymous user 1`] = ` "assigneeUser": Object { "active": true, "local": true, - "login": "assignee", - "name": "John Doe", + "login": "assignee_login", + "name": undefined, }, "author": "author", "authorUser": Object { @@ -1072,7 +1072,7 @@ exports[`should render correctly: anonymous user 1`] = ` </DeferredSpinner> `; -exports[`should render correctly: assignee without name 1`] = ` +exports[`should render correctly: default 1`] = ` <DeferredSpinner className="big-spacer-left big-spacer-top" loading={false} @@ -1200,8 +1200,8 @@ exports[`should render correctly: assignee without name 1`] = ` "assigneeUser": Object { "active": true, "local": true, - "login": "assignee_login", - "name": undefined, + "login": "assignee", + "name": "John Doe", }, "author": "author", "authorUser": Object { @@ -1280,8 +1280,8 @@ exports[`should render correctly: assignee without name 1`] = ` "assigneeUser": Object { "active": true, "local": true, - "login": "assignee_login", - "name": undefined, + "login": "assignee", + "name": "John Doe", }, "author": "author", "authorUser": Object { @@ -1378,8 +1378,8 @@ exports[`should render correctly: assignee without name 1`] = ` "assigneeUser": Object { "active": true, "local": true, - "login": "assignee_login", - "name": undefined, + "login": "assignee", + "name": "John Doe", }, "author": "author", "authorUser": Object { @@ -1451,8 +1451,8 @@ exports[`should render correctly: assignee without name 1`] = ` "assigneeUser": Object { "active": true, "local": true, - "login": "assignee_login", - "name": undefined, + "login": "assignee", + "name": "John Doe", }, "author": "author", "authorUser": Object { @@ -1535,8 +1535,8 @@ exports[`should render correctly: assignee without name 1`] = ` "assigneeUser": Object { "active": true, "local": true, - "login": "assignee_login", - "name": undefined, + "login": "assignee", + "name": "John Doe", }, "author": "author", "authorUser": Object { @@ -2151,6 +2151,548 @@ exports[`should render correctly: no hotspot 1`] = ` /> `; +exports[`should render correctly: show success modal 1`] = ` +<DeferredSpinner + className="big-spacer-left big-spacer-top" + loading={false} +> + <StatusUpdateSuccessModal + hotspotsReviewedMeasure="75" + lastStatusChangedTo="FIXED" + onClose={[MockFunction]} + onSwitchFilterToStatusOfUpdatedHotspot={[MockFunction]} + /> + <div + className="big-padded hotspot-content" + > + <div + className="huge-spacer-bottom display-flex-space-between" + > + <div + className="display-flex-column" + > + <strong + className="big big-spacer-right little-spacer-bottom" + > + '3' is a magic number. + </strong> + <div> + <span + className="note padded-right" + > + That rule + </span> + <Link + className="small" + onlyActiveOnIndex={false} + style={Object {}} + target="_blank" + to={ + Object { + "pathname": "/coding_rules", + "query": Object { + "open": "squid:S2077", + "rule_key": "squid:S2077", + }, + } + } + > + squid:S2077 + </Link> + </div> + </div> + <div + className="display-flex-row flex-0" + > + <div + className="dropdown spacer-right flex-1-0-auto" + > + <Button + className="it__hs-add-comment" + onClick={[MockFunction]} + > + hotspots.comment.open + </Button> + </div> + <div + className="dropdown spacer-right flex-1-0-auto" + > + <HotspotOpenInIdeButton + hotspotKey="01fc972e-2a3c-433e-bcae-0bd7f88f5123" + projectKey="hotspot-component" + /> + </div> + <ClipboardButton + className="flex-1-0-auto" + copyValue="http://localhost/security_hotspots?id=my-project&branch=branch-6.7&hotspots=01fc972e-2a3c-433e-bcae-0bd7f88f5123" + > + <LinkIcon + className="spacer-right" + /> + <span> + hotspots.get_permalink + </span> + </ClipboardButton> + </div> + </div> + <div + className="huge-spacer-bottom display-flex-row display-flex-space-between" + > + <div + className="hotspot-information display-flex-column display-flex-space-between" + > + <div + className="display-flex-center" + > + <span + className="big-spacer-right" + > + category + </span> + <strong + className="nowrap" + > + SQL injection + </strong> + </div> + <div + className="display-flex-center" + > + <span + className="big-spacer-right" + > + hotspots.risk_exposure + </span> + <div + className="hotspot-risk-badge HIGH" + > + risk_exposure.HIGH + </div> + </div> + <div + className="display-flex-center it__hs-assignee" + > + <span + className="big-spacer-right" + > + assignee + </span> + <div> + <Connect(withCurrentUser(Assignee)) + hotspot={ + Object { + "assignee": "assignee", + "assigneeUser": Object { + "active": true, + "local": true, + "login": "assignee", + "name": "John Doe", + }, + "author": "author", + "authorUser": Object { + "active": true, + "local": true, + "login": "author", + "name": "John Doe", + }, + "canChangeStatus": true, + "changelog": Array [], + "comment": Array [], + "component": Object { + "key": "hotspot-component", + "longName": "Hotspot component long name", + "name": "Hotspot Component", + "path": "path/to/component", + "qualifier": "FIL", + }, + "creationDate": "2013-05-13T17:55:41+0200", + "key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123", + "line": 142, + "message": "'3' is a magic number.", + "project": Object { + "key": "hotspot-component", + "longName": "Hotspot component long name", + "name": "Hotspot Component", + "path": "path/to/component", + "qualifier": "TRK", + }, + "resolution": "FIXED", + "rule": Object { + "fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>", + "key": "squid:S2077", + "name": "That rule", + "riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>", + "securityCategory": "sql-injection", + "vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>", + "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]} + /> + </div> + </div> + </div> + <div + className="huge-spacer-left abs-width-400" + > + <Connect(withCurrentUser(Status)) + hotspot={ + Object { + "assignee": "assignee", + "assigneeUser": Object { + "active": true, + "local": true, + "login": "assignee", + "name": "John Doe", + }, + "author": "author", + "authorUser": Object { + "active": true, + "local": true, + "login": "author", + "name": "John Doe", + }, + "canChangeStatus": true, + "changelog": Array [], + "comment": Array [], + "component": Object { + "key": "hotspot-component", + "longName": "Hotspot component long name", + "name": "Hotspot Component", + "path": "path/to/component", + "qualifier": "FIL", + }, + "creationDate": "2013-05-13T17:55:41+0200", + "key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123", + "line": 142, + "message": "'3' is a magic number.", + "project": Object { + "key": "hotspot-component", + "longName": "Hotspot component long name", + "name": "Hotspot Component", + "path": "path/to/component", + "qualifier": "TRK", + }, + "resolution": "FIXED", + "rule": Object { + "fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>", + "key": "squid:S2077", + "name": "That rule", + "riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>", + "securityCategory": "sql-injection", + "vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>", + "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]} + /> + </div> + </div> + <HotspotSnippetContainer + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } + } + hotspot={ + Object { + "assignee": "assignee", + "assigneeUser": Object { + "active": true, + "local": true, + "login": "assignee", + "name": "John Doe", + }, + "author": "author", + "authorUser": Object { + "active": true, + "local": true, + "login": "author", + "name": "John Doe", + }, + "canChangeStatus": true, + "changelog": Array [], + "comment": Array [], + "component": Object { + "key": "hotspot-component", + "longName": "Hotspot component long name", + "name": "Hotspot Component", + "path": "path/to/component", + "qualifier": "FIL", + }, + "creationDate": "2013-05-13T17:55:41+0200", + "key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123", + "line": 142, + "message": "'3' is a magic number.", + "project": Object { + "key": "hotspot-component", + "longName": "Hotspot component long name", + "name": "Hotspot Component", + "path": "path/to/component", + "qualifier": "TRK", + }, + "resolution": "FIXED", + "rule": Object { + "fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>", + "key": "squid:S2077", + "name": "That rule", + "riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>", + "securityCategory": "sql-injection", + "vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>", + "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", + }, + ], + } + } + /> + <HotspotViewerTabs + hotspot={ + Object { + "assignee": "assignee", + "assigneeUser": Object { + "active": true, + "local": true, + "login": "assignee", + "name": "John Doe", + }, + "author": "author", + "authorUser": Object { + "active": true, + "local": true, + "login": "author", + "name": "John Doe", + }, + "canChangeStatus": true, + "changelog": Array [], + "comment": Array [], + "component": Object { + "key": "hotspot-component", + "longName": "Hotspot component long name", + "name": "Hotspot Component", + "path": "path/to/component", + "qualifier": "FIL", + }, + "creationDate": "2013-05-13T17:55:41+0200", + "key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123", + "line": 142, + "message": "'3' is a magic number.", + "project": Object { + "key": "hotspot-component", + "longName": "Hotspot component long name", + "name": "Hotspot Component", + "path": "path/to/component", + "qualifier": "TRK", + }, + "resolution": "FIXED", + "rule": Object { + "fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>", + "key": "squid:S2077", + "name": "That rule", + "riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>", + "securityCategory": "sql-injection", + "vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>", + "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", + }, + ], + } + } + /> + <HotspotReviewHistoryAndComments + commentTextRef={ + Object { + "current": null, + } + } + commentVisible={false} + currentUser={ + Object { + "isLoggedIn": false, + } + } + hotspot={ + Object { + "assignee": "assignee", + "assigneeUser": Object { + "active": true, + "local": true, + "login": "assignee", + "name": "John Doe", + }, + "author": "author", + "authorUser": Object { + "active": true, + "local": true, + "login": "author", + "name": "John Doe", + }, + "canChangeStatus": true, + "changelog": Array [], + "comment": Array [], + "component": Object { + "key": "hotspot-component", + "longName": "Hotspot component long name", + "name": "Hotspot Component", + "path": "path/to/component", + "qualifier": "FIL", + }, + "creationDate": "2013-05-13T17:55:41+0200", + "key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123", + "line": 142, + "message": "'3' is a magic number.", + "project": Object { + "key": "hotspot-component", + "longName": "Hotspot component long name", + "name": "Hotspot Component", + "path": "path/to/component", + "qualifier": "TRK", + }, + "resolution": "FIXED", + "rule": Object { + "fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>", + "key": "squid:S2077", + "name": "That rule", + "riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>", + "securityCategory": "sql-injection", + "vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>", + "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]} + /> + </div> +</DeferredSpinner> +`; + exports[`should render correctly: unassigned 1`] = ` <DeferredSpinner className="big-spacer-left big-spacer-top" diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/StatusUpdateSuccessModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/StatusUpdateSuccessModal-test.tsx.snap new file mode 100644 index 00000000000..f6f4cc4189a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/StatusUpdateSuccessModal-test.tsx.snap @@ -0,0 +1,110 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly: default 1`] = ` +<Modal + contentLabel="hotspots.congratulations" +> + <div + className="modal-head" + > + <h2 + className="huge text-normal text-success" + > + hotspots.congratulations + </h2> + </div> + <div + className="modal-body" + > + <FormattedMessage + defaultMessage="hotspots.successfully_changed_to_x" + id="hotspots.successfully_changed_to_x" + values={ + Object { + "status_change": <strong> + hotspots.successful_status_change_to_x.hotspots.status_option.FIXED + </strong>, + "status_label": "hotspots.status_option.FIXED", + } + } + /> + <p + className="spacer-top" + > + <FormattedMessage + defaultMessage="hotspots.x_done_keep_going" + id="hotspots.x_done_keep_going" + values={ + Object { + "percentage": <strong> + + </strong>, + } + } + /> + </p> + </div> + <div + className="modal-foot modal-foot-clear display-flex-center display-flex-space-between" + > + <ButtonLink + onClick={[MockFunction]} + > + hotspots.see_x_hotspots.hotspots.status_option.FIXED + </ButtonLink> + <Button + className="button-primary padded" + onClick={[MockFunction]} + > + hotspots.continue_to_next_hotspot + </Button> + </div> +</Modal> +`; + +exports[`should render correctly: opening hotspots again 1`] = ` +<Modal + contentLabel="hotspots.update.success" +> + <div + className="modal-head" + > + <h2 + className="huge text-normal" + > + hotspots.update.success + </h2> + </div> + <div + className="modal-body" + > + <FormattedMessage + defaultMessage="hotspots.successfully_changed_to_x" + id="hotspots.successfully_changed_to_x" + values={ + Object { + "status_change": <strong> + hotspots.successful_status_change_to_x.hotspots.status_option.TO_REVIEW + </strong>, + "status_label": "hotspots.status_option.TO_REVIEW", + } + } + /> + </div> + <div + className="modal-foot modal-foot-clear display-flex-center display-flex-space-between" + > + <ButtonLink + onClick={[MockFunction]} + > + hotspots.see_x_hotspots.hotspots.status_option.TO_REVIEW + </ButtonLink> + <Button + className="button-primary padded" + onClick={[MockFunction]} + > + hotspots.continue_to_next_hotspot + </Button> + </div> +</Modal> +`; 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<void>; + onStatusChange: (statusOption: HotspotStatusOption) => Promise<void>; } export function Status(props: StatusProps) { @@ -64,8 +64,8 @@ export function Status(props: StatusProps) { <DropdownOverlay noPadding={true} placement={PopupPlacement.Bottom}> <StatusSelection hotspot={hotspot} - onStatusOptionChange={async () => { - 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<Props, State> { 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; +} |