aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js
diff options
context:
space:
mode:
authorWouter Admiraal <wouter.admiraal@sonarsource.com>2021-09-07 09:52:54 +0200
committersonartech <sonartech@sonarsource.com>2021-09-07 20:03:17 +0000
commit0d1d5eb5c92f664e3f8b3324a6ba01036ad5a3e2 (patch)
tree9433d37aac129792978580598182d9feecbbddfc /server/sonar-web/src/main/js
parentc70d5b85ce6aab51f5bd9271705c76cac584f547 (diff)
downloadsonarqube-0d1d5eb5c92f664e3f8b3324a6ba01036ad5a3e2.tar.gz
sonarqube-0d1d5eb5c92f664e3f8b3324a6ba01036ad5a3e2.zip
SONAR-14511 Improve Security Hotspot status change flow
Diffstat (limited to 'server/sonar-web/src/main/js')
-rw-r--r--server/sonar-web/src/main/js/app/styles/init/misc.css2
-rw-r--r--server/sonar-web/src/main/js/app/styles/init/type.css10
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap1
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts16
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx42
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx27
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/StatusUpdateSuccessModal.tsx102
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewer-test.tsx32
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerRenderer-test.tsx25
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/StatusUpdateSuccessModal-test.tsx45
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap6
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap588
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/StatusUpdateSuccessModal-test.tsx.snap110
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelection.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/utils.ts11
-rw-r--r--server/sonar-web/src/main/js/components/controls/Modal.css12
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;
+}