aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorPhilippe Perrin <philippe.perrin@sonarsource.com>2020-02-11 18:49:50 +0100
committerSonarTech <sonartech@sonarsource.com>2020-02-21 20:46:19 +0100
commit9e025bf15700eff81e11cb00bcf5e9650f765ee9 (patch)
treea4e482db7368c393f732ca0829cdd8450f19f3ab /server
parent92bdc9faf99a89c0c70945198045b1de81db2ed7 (diff)
downloadsonarqube-9e025bf15700eff81e11cb00bcf5e9650f765ee9.tar.gz
sonarqube-9e025bf15700eff81e11cb00bcf5e9650f765ee9.zip
SONAR-12719 Move the status edition popup in hotspot main screen
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/app/styles/init/icons.css2
-rw-r--r--server/sonar-web/src/main/js/app/styles/init/misc.css5
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts51
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActions.tsx77
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActionsForm.tsx163
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActionsFormRenderer.tsx166
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx21
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActions-test.tsx78
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActionsForm-test.tsx155
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActionsFormRenderer-test.tsx109
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerRenderer-test.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActions-test.tsx.snap406
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActionsForm-test.tsx.snap112
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActionsFormRenderer-test.tsx.snap544
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap4
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap4
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap827
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/AssigneeSelectionRenderer.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/__snapshots__/AssigneeSelectionRenderer-test.tsx.snap8
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.css44
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.tsx107
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusDescription.tsx41
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelection.tsx109
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelectionRenderer.tsx91
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/Status-test.tsx74
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusDescription-test.tsx35
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelection-test.tsx78
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelectionRenderer-test.tsx72
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/Status-test.tsx.snap436
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusDescription-test.tsx.snap24
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelection-test.tsx.snap12
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelectionRenderer-test.tsx.snap140
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/styles.css7
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/utils.ts35
-rw-r--r--server/sonar-web/src/main/js/types/security-hotspots.ts6
36 files changed, 1795 insertions, 2262 deletions
diff --git a/server/sonar-web/src/main/js/app/styles/init/icons.css b/server/sonar-web/src/main/js/app/styles/init/icons.css
index 753fb321eb3..7a04513fabd 100644
--- a/server/sonar-web/src/main/js/app/styles/init/icons.css
+++ b/server/sonar-web/src/main/js/app/styles/init/icons.css
@@ -77,7 +77,7 @@ a[class*=' icon-'] {
transition: opacity 0.3s ease;
}
-a:hover > .icon-radio {
+a:not(.disabled):hover > .icon-radio {
border-color: var(--blue);
}
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 9dd45c38a90..9a8ffd44fd9 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
@@ -384,6 +384,11 @@ th.huge-spacer-right {
justify-content: center;
}
+.display-flex-justify-end {
+ display: flex !important;
+ justify-content: flex-end;
+}
+
.display-flex-space-around {
display: flex !important;
justify-content: space-around;
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 a837324eba8..0f134e3b118 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
@@ -19,8 +19,21 @@
*/
import { mockHotspot, mockRawHotspot } from '../../../helpers/mocks/security-hotspots';
import { mockUser } from '../../../helpers/testMocks';
-import { ReviewHistoryType, RiskExposure } from '../../../types/security-hotspots';
-import { getHotspotReviewHistory, groupByCategory, mapRules, sortHotspots } from '../utils';
+import {
+ HotspotResolution,
+ HotspotStatus,
+ HotspotStatusOption,
+ ReviewHistoryType,
+ RiskExposure
+} from '../../../types/security-hotspots';
+import {
+ getHotspotReviewHistory,
+ getStatusAndResolutionFromStatusOption,
+ getStatusOptionFromStatusAndResolution,
+ groupByCategory,
+ mapRules,
+ sortHotspots
+} from '../utils';
const hotspots = [
mockRawHotspot({
@@ -223,3 +236,37 @@ describe('getHotspotReviewHistory', () => {
);
});
});
+
+describe('getStatusOptionFromStatusAndResolution', () => {
+ it('should return the correct values', () => {
+ expect(
+ getStatusOptionFromStatusAndResolution(HotspotStatus.REVIEWED, HotspotResolution.FIXED)
+ ).toBe(HotspotStatusOption.FIXED);
+ expect(
+ getStatusOptionFromStatusAndResolution(HotspotStatus.REVIEWED, HotspotResolution.SAFE)
+ ).toBe(HotspotStatusOption.SAFE);
+ expect(getStatusOptionFromStatusAndResolution(HotspotStatus.REVIEWED)).toBe(
+ HotspotStatusOption.FIXED
+ );
+ expect(getStatusOptionFromStatusAndResolution(HotspotStatus.TO_REVIEW)).toBe(
+ HotspotStatusOption.TO_REVIEW
+ );
+ });
+});
+
+describe('getStatusAndResolutionFromStatusOption', () => {
+ it('should return the correct values', () => {
+ expect(getStatusAndResolutionFromStatusOption(HotspotStatusOption.TO_REVIEW)).toEqual({
+ status: HotspotStatus.TO_REVIEW,
+ resolution: undefined
+ });
+ expect(getStatusAndResolutionFromStatusOption(HotspotStatusOption.FIXED)).toEqual({
+ status: HotspotStatus.REVIEWED,
+ resolution: HotspotResolution.FIXED
+ });
+ expect(getStatusAndResolutionFromStatusOption(HotspotStatusOption.SAFE)).toEqual({
+ status: HotspotStatus.REVIEWED,
+ resolution: HotspotResolution.SAFE
+ });
+ });
+});
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActions.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActions.tsx
deleted file mode 100644
index 2018a18feb3..00000000000
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActions.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2020 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 React from 'react';
-import { Button } from 'sonar-ui-common/components/controls/buttons';
-import { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown';
-import OutsideClickHandler from 'sonar-ui-common/components/controls/OutsideClickHandler';
-import DropdownIcon from 'sonar-ui-common/components/icons/DropdownIcon';
-import { PopupPlacement } from 'sonar-ui-common/components/ui/popups';
-import { translate } from 'sonar-ui-common/helpers/l10n';
-import { Hotspot } from '../../../types/security-hotspots';
-import HotspotActionsForm from './HotspotActionsForm';
-
-export interface HotspotActionsProps {
- hotspot: Hotspot;
- onSubmit: () => void;
-}
-
-const ESCAPE_KEY = 'Escape';
-
-export default function HotspotActions(props: HotspotActionsProps) {
- const { hotspot } = props;
- const [open, setOpen] = React.useState(false);
-
- React.useEffect(() => {
- const handleKeyDown = (event: KeyboardEvent) => {
- if (event.key === ESCAPE_KEY) {
- setOpen(false);
- }
- };
-
- document.addEventListener('keydown', handleKeyDown, false);
-
- return () => {
- document.removeEventListener('keydown', handleKeyDown, false);
- };
- });
-
- return (
- <div className="dropdown big-spacer-left flex-0">
- <Button onClick={() => setOpen(!open)}>
- {translate('hotspot.change_status', hotspot.status)}
- <DropdownIcon className="little-spacer-left" />
- </Button>
-
- {open && (
- <OutsideClickHandler onClickOutside={() => setOpen(false)}>
- <DropdownOverlay placement={PopupPlacement.BottomRight}>
- <HotspotActionsForm
- hotspot={hotspot}
- onSubmit={() => {
- setOpen(false);
- props.onSubmit();
- }}
- />
- </DropdownOverlay>
- </OutsideClickHandler>
- )}
- </div>
- );
-}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActionsForm.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActionsForm.tsx
deleted file mode 100644
index c5564548006..00000000000
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActionsForm.tsx
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2020 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 React from 'react';
-import {
- assignSecurityHotspot,
- commentSecurityHotspot,
- setSecurityHotspotStatus
-} from '../../../api/security-hotspots';
-import {
- Hotspot,
- HotspotResolution,
- HotspotStatus,
- HotspotStatusOption
-} from '../../../types/security-hotspots';
-import HotspotActionsFormRenderer from './HotspotActionsFormRenderer';
-
-interface Props {
- hotspot: Hotspot;
- onSubmit: () => void;
-}
-
-interface State {
- comment: string;
- selectedOption: HotspotStatusOption;
- selectedUser?: T.UserActive;
- submitting: boolean;
-}
-
-export default class HotspotActionsForm extends React.Component<Props, State> {
- constructor(props: Props) {
- super(props);
-
- let selectedOption = HotspotStatusOption.FIXED;
- if (props.hotspot.status === HotspotStatus.TO_REVIEW) {
- selectedOption = HotspotStatusOption.ADDITIONAL_REVIEW;
- } else if (props.hotspot.resolution) {
- selectedOption = HotspotStatusOption[props.hotspot.resolution];
- }
-
- this.state = {
- comment: '',
- selectedOption,
- submitting: false
- };
- }
-
- handleSelectOption = (selectedOption: HotspotStatusOption) => {
- this.setState({ selectedOption });
- };
-
- handleAssign = (selectedUser: T.UserActive) => {
- this.setState({ selectedUser });
- };
-
- handleCommentChange = (comment: string) => {
- this.setState({ comment });
- };
-
- handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
- event.preventDefault();
-
- const { hotspot } = this.props;
- const { comment, selectedOption, selectedUser } = this.state;
-
- const status =
- selectedOption === HotspotStatusOption.ADDITIONAL_REVIEW
- ? HotspotStatus.TO_REVIEW
- : HotspotStatus.REVIEWED;
-
- const resolution =
- selectedOption !== HotspotStatusOption.ADDITIONAL_REVIEW
- ? HotspotResolution[selectedOption]
- : undefined;
-
- this.setState({ submitting: true });
- /*
- * updateAssignee depends on updateStatus, hence these are chained rather than
- * run in parallel. The comment should also appear last in the changelog.
- */
- return Promise.resolve()
- .then(() => this.updateStatus(hotspot, status, resolution))
- .then(() => this.updateAssignee(hotspot, selectedOption, selectedUser))
- .then(() => this.addComment(hotspot, comment))
- .then(() => {
- this.props.onSubmit();
- // No need to set "submitting", we are closing the window
- })
- .catch(() => {
- this.setState({ submitting: false });
- });
- };
-
- updateStatus = (hotspot: Hotspot, status: HotspotStatus, resolution?: HotspotResolution) => {
- if (
- hotspot.canChangeStatus &&
- (status !== hotspot.status || resolution !== hotspot.resolution)
- ) {
- return setSecurityHotspotStatus(hotspot.key, { status, resolution });
- }
-
- return Promise.resolve();
- };
-
- updateAssignee = (
- hotspot: Hotspot,
- selectedOption: HotspotStatusOption,
- selectedUser?: T.UserActive
- ) => {
- if (
- selectedOption === HotspotStatusOption.ADDITIONAL_REVIEW &&
- selectedUser &&
- selectedUser.login !== hotspot.assignee
- ) {
- return assignSecurityHotspot(hotspot.key, {
- assignee: selectedUser.login
- });
- }
- return Promise.resolve();
- };
-
- addComment = (hotspot: Hotspot, comment: string) => {
- if (comment.length > 0) {
- return commentSecurityHotspot(hotspot.key, comment);
- }
- return Promise.resolve();
- };
-
- render() {
- const { hotspot } = this.props;
- const { comment, selectedOption, selectedUser, submitting } = this.state;
-
- return (
- <HotspotActionsFormRenderer
- comment={comment}
- hotspot={hotspot}
- onAssign={this.handleAssign}
- onChangeComment={this.handleCommentChange}
- onSelectOption={this.handleSelectOption}
- onSubmit={this.handleSubmit}
- selectedOption={selectedOption}
- selectedUser={selectedUser}
- submitting={submitting}
- />
- );
- }
-}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActionsFormRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActionsFormRenderer.tsx
deleted file mode 100644
index d8d0ff6ceb0..00000000000
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActionsFormRenderer.tsx
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2020 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 { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
-import Radio from 'sonar-ui-common/components/controls/Radio';
-import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
-import { translate } from 'sonar-ui-common/helpers/l10n';
-import MarkdownTips from '../../../components/common/MarkdownTips';
-import {
- Hotspot,
- HotspotResolution,
- HotspotStatus,
- HotspotStatusOption
-} from '../../../types/security-hotspots';
-
-export interface HotspotActionsFormRendererProps {
- comment: string;
- hotspot: Hotspot;
- onAssign: (user: T.UserActive) => void;
- onChangeComment: (comment: string) => void;
- onSelectOption: (option: HotspotStatusOption) => void;
- onSubmit: (event: React.SyntheticEvent<HTMLFormElement>) => void;
- selectedOption: HotspotStatusOption;
- selectedUser?: T.UserActive;
- submitting: boolean;
-}
-
-export default function HotspotActionsFormRenderer(props: HotspotActionsFormRendererProps) {
- const { comment, hotspot, selectedOption, submitting } = props;
-
- const disableStatusChange = !hotspot.canChangeStatus;
-
- return (
- <form className="abs-width-400 padded" onSubmit={props.onSubmit}>
- <h2>
- {disableStatusChange
- ? translate('hotspots.form.title.disabled')
- : translate('hotspots.form.title')}
- </h2>
- <div className="display-flex-column big-spacer-bottom">
- {renderOption({
- disabled: disableStatusChange,
- option: HotspotStatusOption.FIXED,
- selectedOption,
- onClick: props.onSelectOption
- })}
- {renderOption({
- disabled: disableStatusChange,
- option: HotspotStatusOption.SAFE,
- selectedOption,
- onClick: props.onSelectOption
- })}
- {renderOption({
- disabled: disableStatusChange,
- option: HotspotStatusOption.ADDITIONAL_REVIEW,
- selectedOption,
- onClick: props.onSelectOption
- })}
- </div>
- <div className="display-flex-column big-spacer-bottom">
- <label className="little-spacer-bottom">{translate('hotspots.form.comment')}</label>
- <textarea
- autoFocus={true}
- className="form-field fixed-width spacer-bottom"
- onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) =>
- props.onChangeComment(event.currentTarget.value)
- }
- placeholder={
- selectedOption === HotspotStatusOption.SAFE
- ? translate('hotspots.form.comment.placeholder')
- : ''
- }
- rows={6}
- value={comment}
- />
- <MarkdownTips />
- </div>
- <div className="text-right">
- {submitting && <i className="spinner spacer-right" />}
- <SubmitButton disabled={submitting || !changes(props)}>
- {translate('hotspots.form.submit', hotspot.status)}
- </SubmitButton>
- </div>
- </form>
- );
-}
-
-const noop = () => {};
-
-function changes(params: {
- comment: string;
- hotspot: Hotspot;
- selectedOption: HotspotStatusOption;
- selectedUser?: T.UserActive;
-}) {
- const { comment, hotspot, selectedOption, selectedUser } = params;
-
- const status =
- selectedOption === HotspotStatusOption.ADDITIONAL_REVIEW
- ? HotspotStatus.TO_REVIEW
- : HotspotStatus.REVIEWED;
-
- const resolution =
- selectedOption !== HotspotStatusOption.ADDITIONAL_REVIEW
- ? HotspotResolution[selectedOption]
- : undefined;
-
- return (
- comment.length > 0 ||
- selectedUser ||
- status !== hotspot.status ||
- resolution !== hotspot.resolution
- );
-}
-
-function renderOption(params: {
- disabled: boolean;
- option: HotspotStatusOption;
- onClick: (option: HotspotStatusOption) => void;
- selectedOption: HotspotStatusOption;
-}) {
- const { disabled, onClick, option, selectedOption } = params;
-
- const optionRender = (
- <div className="big-spacer-top">
- <Radio
- checked={selectedOption === option}
- className={classnames({ disabled })}
- onCheck={disabled ? noop : onClick}
- value={option}>
- <h3 className={classnames({ 'text-muted': disabled })}>
- {translate('hotspots.status_option', option)}
- </h3>
- </Radio>
- <div className={classnames('radio-button-description', { 'text-muted': disabled })}>
- {translate('hotspots.status_option', option, 'description')}
- </div>
- </div>
- );
-
- return disabled ? (
- <Tooltip overlay={translate('hotspots.form.cannot_change_status')} placement="left">
- {optionRender}
- </Tooltip>
- ) : (
- optionRender
- );
-}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx
index 95a9826cc7d..59540a73c4e 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx
@@ -21,6 +21,7 @@ import * as classNames from 'classnames';
import * as React from 'react';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { RawHotspot } from '../../../types/security-hotspots';
+import { getStatusOptionFromStatusAndResolution } from '../utils';
export interface HotspotListItemProps {
hotspot: RawHotspot;
@@ -37,7 +38,10 @@ export default function HotspotListItem(props: HotspotListItemProps) {
onClick={() => !selected && props.onClick(hotspot.key)}>
<div className="little-spacer-left">{hotspot.message}</div>
<div className="badge spacer-top">
- {translate('hotspot.status', hotspot.resolution || hotspot.status)}
+ {translate(
+ 'hotspots.status_option',
+ getStatusOptionFromStatusAndResolution(hotspot.status, hotspot.resolution)
+ )}
</div>
</a>
);
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 179f461cda7..3a841add34a 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
@@ -20,26 +20,23 @@
import * as React from 'react';
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import { translate } from 'sonar-ui-common/helpers/l10n';
-import { withCurrentUser } from '../../../components/hoc/withCurrentUser';
-import { isLoggedIn } from '../../../helpers/users';
import { BranchLike } from '../../../types/branch-like';
import { Hotspot } from '../../../types/security-hotspots';
import Assignee from './assignee/Assignee';
-import HotspotActions from './HotspotActions';
import HotspotSnippetContainer from './HotspotSnippetContainer';
import HotspotViewerTabs from './HotspotViewerTabs';
+import Status from './status/Status';
export interface HotspotViewerRendererProps {
branchLike?: BranchLike;
- currentUser: T.CurrentUser;
hotspot?: Hotspot;
loading: boolean;
onUpdateHotspot: () => void;
securityCategories: T.StandardSecurityCategories;
}
-export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
- const { branchLike, currentUser, hotspot, loading, securityCategories } = props;
+export default function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
+ const { branchLike, hotspot, loading, securityCategories } = props;
return (
<DeferredSpinner loading={loading}>
@@ -48,9 +45,6 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
<div className="big-spacer-bottom">
<div className="display-flex-space-between">
<h1>{hotspot.message}</h1>
- {isLoggedIn(currentUser) && (
- <HotspotActions hotspot={hotspot} onSubmit={props.onUpdateHotspot} />
- )}
</div>
<div className="text-muted">
<span>{translate('category')}:</span>
@@ -59,12 +53,9 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
</span>
</div>
</div>
- <div className="huge-spacer-bottom">
- <span>{translate('status')}:</span>
- <span className="badge little-spacer-left">
- {translate('hotspot.status', hotspot.resolution || hotspot.status)}
- </span>
+ <div className="display-flex-row huge-spacer-bottom">
<Assignee hotspot={hotspot} onAssigneeChange={props.onUpdateHotspot} />
+ <Status hotspot={hotspot} onStatusChange={props.onUpdateHotspot} />
</div>
<HotspotSnippetContainer branchLike={branchLike} hotspot={hotspot} />
<HotspotViewerTabs hotspot={hotspot} onUpdateHotspot={props.onUpdateHotspot} />
@@ -73,5 +64,3 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
</DeferredSpinner>
);
}
-
-export default withCurrentUser(HotspotViewerRenderer);
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActions-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActions-test.tsx
deleted file mode 100644
index 1f67a35002f..00000000000
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActions-test.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2020 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 { Button } from 'sonar-ui-common/components/controls/buttons';
-import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
-import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
-import { HotspotStatus } from '../../../../types/security-hotspots';
-import HotspotActions, { HotspotActionsProps } from '../HotspotActions';
-
-it('should render correctly', () => {
- expect(shallowRender()).toMatchSnapshot();
-});
-
-it('should open when clicked', async () => {
- const wrapper = shallowRender();
-
- wrapper.find(Button).simulate('click');
-
- await waitAndUpdate(wrapper);
-
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should register an eventlistener', () => {
- let useEffectCleanup: void | (() => void | undefined) = () =>
- fail('useEffect should clean after itself');
- jest.spyOn(React, 'useEffect').mockImplementationOnce(f => {
- useEffectCleanup = f() || useEffectCleanup;
- });
- let listenerCallback = (_event: { key: string }) =>
- fail('Effect should have registered callback');
- const addEventListener = jest.fn((_event, callback) => {
- listenerCallback = callback;
- });
- jest.spyOn(document, 'addEventListener').mockImplementation(addEventListener);
- const removeEventListener = jest.spyOn(document, 'removeEventListener');
- const wrapper = shallowRender();
-
- wrapper.find(Button).simulate('click');
- expect(wrapper).toMatchSnapshot('Dropdown open');
-
- listenerCallback({ key: 'whatever' });
- expect(wrapper).toMatchSnapshot('Dropdown still open');
-
- listenerCallback({ key: 'Escape' });
- expect(wrapper).toMatchSnapshot('Dropdown closed');
-
- useEffectCleanup();
- expect(removeEventListener).toBeCalledWith('keydown', listenerCallback, false);
-});
-
-function shallowRender(props: Partial<HotspotActionsProps> = {}) {
- return shallow(
- <HotspotActions
- hotspot={mockHotspot({ key: 'key', status: HotspotStatus.TO_REVIEW })}
- onSubmit={jest.fn()}
- {...props}
- />
- );
-}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActionsForm-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActionsForm-test.tsx
deleted file mode 100644
index 11fb8aa26ef..00000000000
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActionsForm-test.tsx
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2020 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 { mockEvent, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
-import {
- assignSecurityHotspot,
- commentSecurityHotspot,
- setSecurityHotspotStatus
-} from '../../../../api/security-hotspots';
-import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
-import { mockLoggedInUser } from '../../../../helpers/testMocks';
-import {
- HotspotResolution,
- HotspotStatus,
- HotspotStatusOption
-} from '../../../../types/security-hotspots';
-import HotspotActionsForm from '../HotspotActionsForm';
-
-jest.mock('../../../../api/security-hotspots', () => ({
- assignSecurityHotspot: jest.fn().mockResolvedValue(undefined),
- commentSecurityHotspot: jest.fn().mockResolvedValue(undefined),
- setSecurityHotspotStatus: jest.fn().mockResolvedValue(undefined)
-}));
-
-it('should render correctly', () => {
- expect(shallowRender()).toMatchSnapshot();
-});
-
-it('should handle option selection', () => {
- const wrapper = shallowRender();
- expect(wrapper.state().selectedOption).toBe(HotspotStatusOption.FIXED);
- wrapper.instance().handleSelectOption(HotspotStatusOption.SAFE);
- expect(wrapper.state().selectedOption).toBe(HotspotStatusOption.SAFE);
-});
-
-it('should handle comment change', () => {
- const wrapper = shallowRender();
- wrapper.instance().handleCommentChange('new comment');
- expect(wrapper.state().comment).toBe('new comment');
-});
-
-describe('submit', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- it('should be handled for additional review', async () => {
- const onSubmit = jest.fn();
- const wrapper = shallowRender({ onSubmit });
- wrapper.setState({ selectedOption: HotspotStatusOption.ADDITIONAL_REVIEW });
-
- const promise = wrapper.instance().handleSubmit(mockEvent());
-
- expect(wrapper.state().submitting).toBe(true);
- await promise;
- expect(setSecurityHotspotStatus).toBeCalledWith('key', {
- status: HotspotStatus.TO_REVIEW
- });
- expect(onSubmit).toBeCalled();
- });
-
- it('should be handled for SAFE', async () => {
- const wrapper = shallowRender();
- wrapper.setState({ comment: 'commentsafe', selectedOption: HotspotStatusOption.SAFE });
- await wrapper.instance().handleSubmit(mockEvent());
- expect(setSecurityHotspotStatus).toBeCalledWith('key', {
- status: HotspotStatus.REVIEWED,
- resolution: HotspotResolution.SAFE
- });
- expect(commentSecurityHotspot).toBeCalledWith('key', 'commentsafe');
- });
-
- it('should be handled for FIXED', async () => {
- const wrapper = shallowRender({
- hotspot: mockHotspot({ key: 'key', status: HotspotStatus.TO_REVIEW })
- });
- wrapper.setState({ comment: 'commentfixed', selectedOption: HotspotStatusOption.FIXED });
- await wrapper.instance().handleSubmit(mockEvent());
- expect(setSecurityHotspotStatus).toBeCalledWith('key', {
- status: HotspotStatus.REVIEWED,
- resolution: HotspotResolution.FIXED
- });
- expect(commentSecurityHotspot).toBeCalledWith('key', 'commentfixed');
- });
-
- it('should ignore no change', async () => {
- const wrapper = shallowRender();
- wrapper.setState({ selectedOption: HotspotStatusOption.FIXED });
- await wrapper.instance().handleSubmit(mockEvent());
- expect(setSecurityHotspotStatus).not.toBeCalled();
- });
-});
-
-it('should handle assignment', async () => {
- const onSubmit = jest.fn();
- const wrapper = shallowRender({ onSubmit });
- wrapper.setState({
- comment: 'assignment comment',
- selectedOption: HotspotStatusOption.ADDITIONAL_REVIEW
- });
-
- wrapper.instance().handleAssign(mockLoggedInUser({ login: 'userLogin' }));
- await waitAndUpdate(wrapper);
-
- const promise = wrapper.instance().handleSubmit({ preventDefault: jest.fn() } as any);
-
- expect(wrapper.state().submitting).toBe(true);
- await promise;
-
- expect(setSecurityHotspotStatus).toBeCalledWith('key', {
- status: HotspotStatus.TO_REVIEW
- });
- expect(assignSecurityHotspot).toBeCalledWith('key', {
- assignee: 'userLogin'
- });
- expect(commentSecurityHotspot).toBeCalledWith('key', 'assignment comment');
- expect(onSubmit).toBeCalled();
-});
-
-it('should handle submit failure', async () => {
- const onSubmit = jest.fn();
- (setSecurityHotspotStatus as jest.Mock).mockRejectedValueOnce('failure');
- const wrapper = shallowRender({ onSubmit });
- wrapper.setState({ selectedOption: HotspotStatusOption.ADDITIONAL_REVIEW });
- const promise = wrapper.instance().handleSubmit({ preventDefault: jest.fn() } as any);
- expect(wrapper.state().submitting).toBe(true);
- await promise;
- await waitAndUpdate(wrapper);
- expect(wrapper.state().submitting).toBe(false);
- expect(onSubmit).not.toBeCalled();
-});
-
-function shallowRender(props: Partial<HotspotActionsForm['props']> = {}) {
- return shallow<HotspotActionsForm>(
- <HotspotActionsForm hotspot={mockHotspot({ key: 'key' })} onSubmit={jest.fn()} {...props} />
- );
-}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActionsFormRenderer-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActionsFormRenderer-test.tsx
deleted file mode 100644
index 77dc7596bd0..00000000000
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActionsFormRenderer-test.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2020 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 { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
-import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
-import { mockLoggedInUser } from '../../../../helpers/testMocks';
-import {
- HotspotResolution,
- HotspotStatus,
- HotspotStatusOption
-} from '../../../../types/security-hotspots';
-import HotspotActionsForm from '../HotspotActionsForm';
-import HotspotActionsFormRenderer, {
- HotspotActionsFormRendererProps
-} from '../HotspotActionsFormRenderer';
-
-it('should render correctly', () => {
- expect(shallowRender()).toMatchSnapshot();
- expect(shallowRender({ submitting: true })).toMatchSnapshot('Submitting');
- expect(shallowRender({ selectedOption: HotspotStatusOption.SAFE })).toMatchSnapshot(
- 'safe option selected'
- );
- expect(
- shallowRender({
- selectedOption: HotspotStatusOption.ADDITIONAL_REVIEW,
- selectedUser: mockLoggedInUser()
- })
- ).toMatchSnapshot('user selected');
- expect(shallowRender({ hotspot: mockHotspot({ canChangeStatus: false }) })).toMatchSnapshot(
- 'restricted access'
- );
-});
-
-it('should enable the submit button if anything has changed', () => {
- const hotspot = mockHotspot({
- status: HotspotStatus.REVIEWED,
- resolution: HotspotResolution.SAFE
- });
- const selectedOption = HotspotStatusOption.SAFE;
- expect(
- shallowRender({ comment: '', hotspot, selectedOption, selectedUser: undefined })
- .find(SubmitButton)
- .props().disabled
- ).toBe(true);
- expect(
- shallowRender({ comment: 'some comment', hotspot, selectedOption, selectedUser: undefined })
- .find(SubmitButton)
- .props().disabled
- ).toBe(false);
- expect(
- shallowRender({ comment: '', hotspot, selectedOption, selectedUser: mockLoggedInUser() })
- .find(SubmitButton)
- .props().disabled
- ).toBe(false);
- expect(
- shallowRender({
- comment: '',
- hotspot,
- selectedOption: HotspotStatusOption.FIXED,
- selectedUser: undefined
- })
- .find(SubmitButton)
- .props().disabled
- ).toBe(false);
- expect(
- shallowRender({
- comment: '',
- hotspot,
- selectedOption: HotspotStatusOption.ADDITIONAL_REVIEW,
- selectedUser: undefined
- })
- .find(SubmitButton)
- .props().disabled
- ).toBe(false);
-});
-
-function shallowRender(props: Partial<HotspotActionsFormRendererProps> = {}) {
- return shallow<HotspotActionsForm>(
- <HotspotActionsFormRenderer
- comment="written comment"
- hotspot={mockHotspot({ key: 'key' })}
- onAssign={jest.fn()}
- onChangeComment={jest.fn()}
- onSelectOption={jest.fn()}
- onSubmit={jest.fn()}
- selectedOption={HotspotStatusOption.FIXED}
- submitting={false}
- {...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 68820dd230e..de1a77e5c77 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
@@ -20,8 +20,8 @@
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
-import { mockCurrentUser, mockLoggedInUser, mockUser } from '../../../../helpers/testMocks';
-import { HotspotViewerRenderer, HotspotViewerRendererProps } from '../HotspotViewerRenderer';
+import { mockUser } from '../../../../helpers/testMocks';
+import HotspotViewerRenderer, { HotspotViewerRendererProps } from '../HotspotViewerRenderer';
it('should render correctly', () => {
const wrapper = shallowRender();
@@ -41,13 +41,11 @@ it('should render correctly', () => {
})
).toMatchSnapshot('assignee without name');
expect(shallowRender()).toMatchSnapshot('anonymous user');
- expect(shallowRender({ currentUser: mockLoggedInUser() })).toMatchSnapshot('user logged in');
});
function shallowRender(props?: Partial<HotspotViewerRendererProps>) {
return shallow(
<HotspotViewerRenderer
- currentUser={mockCurrentUser()}
hotspot={mockHotspot()}
loading={false}
onUpdateHotspot={jest.fn()}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActions-test.tsx.snap
deleted file mode 100644
index e0d10a202a4..00000000000
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActions-test.tsx.snap
+++ /dev/null
@@ -1,406 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should open when clicked 1`] = `
-<div
- className="dropdown big-spacer-left flex-0"
->
- <Button
- onClick={[Function]}
- >
- hotspot.change_status.TO_REVIEW
- <DropdownIcon
- className="little-spacer-left"
- />
- </Button>
- <OutsideClickHandler
- onClickOutside={[Function]}
- >
- <DropdownOverlay
- placement="bottom-right"
- >
- <HotspotActionsForm
- 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 {
- "breadcrumbs": Array [],
- "key": "my-project",
- "name": "MyProject",
- "organization": "foo",
- "qualifier": "FIL",
- "qualityGate": Object {
- "isDefault": true,
- "key": "30",
- "name": "Sonar way",
- },
- "qualityProfiles": Array [
- Object {
- "deleted": false,
- "key": "my-qp",
- "language": "ts",
- "name": "Sonar way",
- },
- ],
- "tags": Array [],
- },
- "creationDate": "2013-05-13T17:55:41+0200",
- "key": "key",
- "line": 142,
- "message": "'3' is a magic number.",
- "project": Object {
- "breadcrumbs": Array [],
- "key": "my-project",
- "name": "MyProject",
- "organization": "foo",
- "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 [],
- },
- "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": "TO_REVIEW",
- "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",
- },
- ],
- }
- }
- onSubmit={[Function]}
- />
- </DropdownOverlay>
- </OutsideClickHandler>
-</div>
-`;
-
-exports[`should register an eventlistener: Dropdown closed 1`] = `
-<div
- className="dropdown big-spacer-left flex-0"
->
- <Button
- onClick={[Function]}
- >
- hotspot.change_status.TO_REVIEW
- <DropdownIcon
- className="little-spacer-left"
- />
- </Button>
-</div>
-`;
-
-exports[`should register an eventlistener: Dropdown open 1`] = `
-<div
- className="dropdown big-spacer-left flex-0"
->
- <Button
- onClick={[Function]}
- >
- hotspot.change_status.TO_REVIEW
- <DropdownIcon
- className="little-spacer-left"
- />
- </Button>
- <OutsideClickHandler
- onClickOutside={[Function]}
- >
- <DropdownOverlay
- placement="bottom-right"
- >
- <HotspotActionsForm
- 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 {
- "breadcrumbs": Array [],
- "key": "my-project",
- "name": "MyProject",
- "organization": "foo",
- "qualifier": "FIL",
- "qualityGate": Object {
- "isDefault": true,
- "key": "30",
- "name": "Sonar way",
- },
- "qualityProfiles": Array [
- Object {
- "deleted": false,
- "key": "my-qp",
- "language": "ts",
- "name": "Sonar way",
- },
- ],
- "tags": Array [],
- },
- "creationDate": "2013-05-13T17:55:41+0200",
- "key": "key",
- "line": 142,
- "message": "'3' is a magic number.",
- "project": Object {
- "breadcrumbs": Array [],
- "key": "my-project",
- "name": "MyProject",
- "organization": "foo",
- "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 [],
- },
- "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": "TO_REVIEW",
- "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",
- },
- ],
- }
- }
- onSubmit={[Function]}
- />
- </DropdownOverlay>
- </OutsideClickHandler>
-</div>
-`;
-
-exports[`should register an eventlistener: Dropdown still open 1`] = `
-<div
- className="dropdown big-spacer-left flex-0"
->
- <Button
- onClick={[Function]}
- >
- hotspot.change_status.TO_REVIEW
- <DropdownIcon
- className="little-spacer-left"
- />
- </Button>
- <OutsideClickHandler
- onClickOutside={[Function]}
- >
- <DropdownOverlay
- placement="bottom-right"
- >
- <HotspotActionsForm
- 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 {
- "breadcrumbs": Array [],
- "key": "my-project",
- "name": "MyProject",
- "organization": "foo",
- "qualifier": "FIL",
- "qualityGate": Object {
- "isDefault": true,
- "key": "30",
- "name": "Sonar way",
- },
- "qualityProfiles": Array [
- Object {
- "deleted": false,
- "key": "my-qp",
- "language": "ts",
- "name": "Sonar way",
- },
- ],
- "tags": Array [],
- },
- "creationDate": "2013-05-13T17:55:41+0200",
- "key": "key",
- "line": 142,
- "message": "'3' is a magic number.",
- "project": Object {
- "breadcrumbs": Array [],
- "key": "my-project",
- "name": "MyProject",
- "organization": "foo",
- "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 [],
- },
- "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": "TO_REVIEW",
- "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",
- },
- ],
- }
- }
- onSubmit={[Function]}
- />
- </DropdownOverlay>
- </OutsideClickHandler>
-</div>
-`;
-
-exports[`should render correctly 1`] = `
-<div
- className="dropdown big-spacer-left flex-0"
->
- <Button
- onClick={[Function]}
- >
- hotspot.change_status.TO_REVIEW
- <DropdownIcon
- className="little-spacer-left"
- />
- </Button>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActionsForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActionsForm-test.tsx.snap
deleted file mode 100644
index 9ad46c82552..00000000000
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActionsForm-test.tsx.snap
+++ /dev/null
@@ -1,112 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<HotspotActionsFormRenderer
- comment=""
- 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 {
- "breadcrumbs": Array [],
- "key": "my-project",
- "name": "MyProject",
- "organization": "foo",
- "qualifier": "FIL",
- "qualityGate": Object {
- "isDefault": true,
- "key": "30",
- "name": "Sonar way",
- },
- "qualityProfiles": Array [
- Object {
- "deleted": false,
- "key": "my-qp",
- "language": "ts",
- "name": "Sonar way",
- },
- ],
- "tags": Array [],
- },
- "creationDate": "2013-05-13T17:55:41+0200",
- "key": "key",
- "line": 142,
- "message": "'3' is a magic number.",
- "project": Object {
- "breadcrumbs": Array [],
- "key": "my-project",
- "name": "MyProject",
- "organization": "foo",
- "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 [],
- },
- "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",
- },
- ],
- }
- }
- onAssign={[Function]}
- onChangeComment={[Function]}
- onSelectOption={[Function]}
- onSubmit={[Function]}
- selectedOption="FIXED"
- submitting={false}
-/>
-`;
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActionsFormRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActionsFormRenderer-test.tsx.snap
deleted file mode 100644
index 1f5255ea037..00000000000
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActionsFormRenderer-test.tsx.snap
+++ /dev/null
@@ -1,544 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<form
- className="abs-width-400 padded"
- onSubmit={[MockFunction]}
->
- <h2>
- hotspots.form.title
- </h2>
- <div
- className="display-flex-column big-spacer-bottom"
- >
- <div
- className="big-spacer-top"
- >
- <Radio
- checked={true}
- className=""
- onCheck={[MockFunction]}
- value="FIXED"
- >
- <h3
- className=""
- >
- hotspots.status_option.FIXED
- </h3>
- </Radio>
- <div
- className="radio-button-description"
- >
- hotspots.status_option.FIXED.description
- </div>
- </div>
- <div
- className="big-spacer-top"
- >
- <Radio
- checked={false}
- className=""
- onCheck={[MockFunction]}
- value="SAFE"
- >
- <h3
- className=""
- >
- hotspots.status_option.SAFE
- </h3>
- </Radio>
- <div
- className="radio-button-description"
- >
- hotspots.status_option.SAFE.description
- </div>
- </div>
- <div
- className="big-spacer-top"
- >
- <Radio
- checked={false}
- className=""
- onCheck={[MockFunction]}
- value="ADDITIONAL_REVIEW"
- >
- <h3
- className=""
- >
- hotspots.status_option.ADDITIONAL_REVIEW
- </h3>
- </Radio>
- <div
- className="radio-button-description"
- >
- hotspots.status_option.ADDITIONAL_REVIEW.description
- </div>
- </div>
- </div>
- <div
- className="display-flex-column big-spacer-bottom"
- >
- <label
- className="little-spacer-bottom"
- >
- hotspots.form.comment
- </label>
- <textarea
- autoFocus={true}
- className="form-field fixed-width spacer-bottom"
- onChange={[Function]}
- placeholder=""
- rows={6}
- value="written comment"
- />
- <MarkdownTips />
- </div>
- <div
- className="text-right"
- >
- <SubmitButton
- disabled={false}
- >
- hotspots.form.submit.REVIEWED
- </SubmitButton>
- </div>
-</form>
-`;
-
-exports[`should render correctly: Submitting 1`] = `
-<form
- className="abs-width-400 padded"
- onSubmit={[MockFunction]}
->
- <h2>
- hotspots.form.title
- </h2>
- <div
- className="display-flex-column big-spacer-bottom"
- >
- <div
- className="big-spacer-top"
- >
- <Radio
- checked={true}
- className=""
- onCheck={[MockFunction]}
- value="FIXED"
- >
- <h3
- className=""
- >
- hotspots.status_option.FIXED
- </h3>
- </Radio>
- <div
- className="radio-button-description"
- >
- hotspots.status_option.FIXED.description
- </div>
- </div>
- <div
- className="big-spacer-top"
- >
- <Radio
- checked={false}
- className=""
- onCheck={[MockFunction]}
- value="SAFE"
- >
- <h3
- className=""
- >
- hotspots.status_option.SAFE
- </h3>
- </Radio>
- <div
- className="radio-button-description"
- >
- hotspots.status_option.SAFE.description
- </div>
- </div>
- <div
- className="big-spacer-top"
- >
- <Radio
- checked={false}
- className=""
- onCheck={[MockFunction]}
- value="ADDITIONAL_REVIEW"
- >
- <h3
- className=""
- >
- hotspots.status_option.ADDITIONAL_REVIEW
- </h3>
- </Radio>
- <div
- className="radio-button-description"
- >
- hotspots.status_option.ADDITIONAL_REVIEW.description
- </div>
- </div>
- </div>
- <div
- className="display-flex-column big-spacer-bottom"
- >
- <label
- className="little-spacer-bottom"
- >
- hotspots.form.comment
- </label>
- <textarea
- autoFocus={true}
- className="form-field fixed-width spacer-bottom"
- onChange={[Function]}
- placeholder=""
- rows={6}
- value="written comment"
- />
- <MarkdownTips />
- </div>
- <div
- className="text-right"
- >
- <i
- className="spinner spacer-right"
- />
- <SubmitButton
- disabled={true}
- >
- hotspots.form.submit.REVIEWED
- </SubmitButton>
- </div>
-</form>
-`;
-
-exports[`should render correctly: restricted access 1`] = `
-<form
- className="abs-width-400 padded"
- onSubmit={[MockFunction]}
->
- <h2>
- hotspots.form.title.disabled
- </h2>
- <div
- className="display-flex-column big-spacer-bottom"
- >
- <Tooltip
- overlay="hotspots.form.cannot_change_status"
- placement="left"
- >
- <div
- className="big-spacer-top"
- >
- <Radio
- checked={true}
- className="disabled"
- onCheck={[Function]}
- value="FIXED"
- >
- <h3
- className="text-muted"
- >
- hotspots.status_option.FIXED
- </h3>
- </Radio>
- <div
- className="radio-button-description text-muted"
- >
- hotspots.status_option.FIXED.description
- </div>
- </div>
- </Tooltip>
- <Tooltip
- overlay="hotspots.form.cannot_change_status"
- placement="left"
- >
- <div
- className="big-spacer-top"
- >
- <Radio
- checked={false}
- className="disabled"
- onCheck={[Function]}
- value="SAFE"
- >
- <h3
- className="text-muted"
- >
- hotspots.status_option.SAFE
- </h3>
- </Radio>
- <div
- className="radio-button-description text-muted"
- >
- hotspots.status_option.SAFE.description
- </div>
- </div>
- </Tooltip>
- <Tooltip
- overlay="hotspots.form.cannot_change_status"
- placement="left"
- >
- <div
- className="big-spacer-top"
- >
- <Radio
- checked={false}
- className="disabled"
- onCheck={[Function]}
- value="ADDITIONAL_REVIEW"
- >
- <h3
- className="text-muted"
- >
- hotspots.status_option.ADDITIONAL_REVIEW
- </h3>
- </Radio>
- <div
- className="radio-button-description text-muted"
- >
- hotspots.status_option.ADDITIONAL_REVIEW.description
- </div>
- </div>
- </Tooltip>
- </div>
- <div
- className="display-flex-column big-spacer-bottom"
- >
- <label
- className="little-spacer-bottom"
- >
- hotspots.form.comment
- </label>
- <textarea
- autoFocus={true}
- className="form-field fixed-width spacer-bottom"
- onChange={[Function]}
- placeholder=""
- rows={6}
- value="written comment"
- />
- <MarkdownTips />
- </div>
- <div
- className="text-right"
- >
- <SubmitButton
- disabled={false}
- >
- hotspots.form.submit.REVIEWED
- </SubmitButton>
- </div>
-</form>
-`;
-
-exports[`should render correctly: safe option selected 1`] = `
-<form
- className="abs-width-400 padded"
- onSubmit={[MockFunction]}
->
- <h2>
- hotspots.form.title
- </h2>
- <div
- className="display-flex-column big-spacer-bottom"
- >
- <div
- className="big-spacer-top"
- >
- <Radio
- checked={false}
- className=""
- onCheck={[MockFunction]}
- value="FIXED"
- >
- <h3
- className=""
- >
- hotspots.status_option.FIXED
- </h3>
- </Radio>
- <div
- className="radio-button-description"
- >
- hotspots.status_option.FIXED.description
- </div>
- </div>
- <div
- className="big-spacer-top"
- >
- <Radio
- checked={true}
- className=""
- onCheck={[MockFunction]}
- value="SAFE"
- >
- <h3
- className=""
- >
- hotspots.status_option.SAFE
- </h3>
- </Radio>
- <div
- className="radio-button-description"
- >
- hotspots.status_option.SAFE.description
- </div>
- </div>
- <div
- className="big-spacer-top"
- >
- <Radio
- checked={false}
- className=""
- onCheck={[MockFunction]}
- value="ADDITIONAL_REVIEW"
- >
- <h3
- className=""
- >
- hotspots.status_option.ADDITIONAL_REVIEW
- </h3>
- </Radio>
- <div
- className="radio-button-description"
- >
- hotspots.status_option.ADDITIONAL_REVIEW.description
- </div>
- </div>
- </div>
- <div
- className="display-flex-column big-spacer-bottom"
- >
- <label
- className="little-spacer-bottom"
- >
- hotspots.form.comment
- </label>
- <textarea
- autoFocus={true}
- className="form-field fixed-width spacer-bottom"
- onChange={[Function]}
- placeholder="hotspots.form.comment.placeholder"
- rows={6}
- value="written comment"
- />
- <MarkdownTips />
- </div>
- <div
- className="text-right"
- >
- <SubmitButton
- disabled={false}
- >
- hotspots.form.submit.REVIEWED
- </SubmitButton>
- </div>
-</form>
-`;
-
-exports[`should render correctly: user selected 1`] = `
-<form
- className="abs-width-400 padded"
- onSubmit={[MockFunction]}
->
- <h2>
- hotspots.form.title
- </h2>
- <div
- className="display-flex-column big-spacer-bottom"
- >
- <div
- className="big-spacer-top"
- >
- <Radio
- checked={false}
- className=""
- onCheck={[MockFunction]}
- value="FIXED"
- >
- <h3
- className=""
- >
- hotspots.status_option.FIXED
- </h3>
- </Radio>
- <div
- className="radio-button-description"
- >
- hotspots.status_option.FIXED.description
- </div>
- </div>
- <div
- className="big-spacer-top"
- >
- <Radio
- checked={false}
- className=""
- onCheck={[MockFunction]}
- value="SAFE"
- >
- <h3
- className=""
- >
- hotspots.status_option.SAFE
- </h3>
- </Radio>
- <div
- className="radio-button-description"
- >
- hotspots.status_option.SAFE.description
- </div>
- </div>
- <div
- className="big-spacer-top"
- >
- <Radio
- checked={true}
- className=""
- onCheck={[MockFunction]}
- value="ADDITIONAL_REVIEW"
- >
- <h3
- className=""
- >
- hotspots.status_option.ADDITIONAL_REVIEW
- </h3>
- </Radio>
- <div
- className="radio-button-description"
- >
- hotspots.status_option.ADDITIONAL_REVIEW.description
- </div>
- </div>
- </div>
- <div
- className="display-flex-column big-spacer-bottom"
- >
- <label
- className="little-spacer-bottom"
- >
- hotspots.form.comment
- </label>
- <textarea
- autoFocus={true}
- className="form-field fixed-width spacer-bottom"
- onChange={[Function]}
- placeholder=""
- rows={6}
- value="written comment"
- />
- <MarkdownTips />
- </div>
- <div
- className="text-right"
- >
- <SubmitButton
- disabled={false}
- >
- hotspots.form.submit.REVIEWED
- </SubmitButton>
- </div>
-</form>
-`;
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap
index 2bf9b48a976..b959dae2d97 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap
@@ -14,7 +14,7 @@ exports[`should render correctly 1`] = `
<div
className="badge spacer-top"
>
- hotspot.status.TO_REVIEW
+ hotspots.status_option.TO_REVIEW
</div>
</a>
`;
@@ -33,7 +33,7 @@ exports[`should render correctly 2`] = `
<div
className="badge spacer-top"
>
- hotspot.status.TO_REVIEW
+ hotspots.status_option.TO_REVIEW
</div>
</a>
`;
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 c9bd0adb0ab..c069dd4ca30 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
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render correctly 1`] = `
-<Connect(withCurrentUser(HotspotViewerRenderer))
+<HotspotViewerRenderer
loading={true}
onUpdateHotspot={[Function]}
securityCategories={
@@ -15,7 +15,7 @@ exports[`should render correctly 1`] = `
`;
exports[`should render correctly 2`] = `
-<Connect(withCurrentUser(HotspotViewerRenderer))
+<HotspotViewerRenderer
hotspot={
Object {
"id": "I am a detailled hotspot",
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 6b2e8cf1ad4..9f8802f8c9e 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
@@ -33,17 +33,8 @@ exports[`should render correctly 1`] = `
</div>
</div>
<div
- className="huge-spacer-bottom"
+ className="display-flex-row huge-spacer-bottom"
>
- <span>
- status
- :
- </span>
- <span
- className="badge little-spacer-left"
- >
- hotspot.status.FIXED
- </span>
<Connect(withCurrentUser(Assignee))
hotspot={
Object {
@@ -146,6 +137,108 @@ exports[`should render correctly 1`] = `
}
onAssigneeChange={[MockFunction]}
/>
+ <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 {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "FIL",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ },
+ "creationDate": "2013-05-13T17:55:41+0200",
+ "key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
+ "line": 142,
+ "message": "'3' is a magic number.",
+ "project": Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "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 [],
+ },
+ "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={[MockFunction]}
+ />
</div>
<HotspotSnippetContainer
hotspot={
@@ -387,17 +480,8 @@ exports[`should render correctly: anonymous user 1`] = `
</div>
</div>
<div
- className="huge-spacer-bottom"
+ className="display-flex-row huge-spacer-bottom"
>
- <span>
- status
- :
- </span>
- <span
- className="badge little-spacer-left"
- >
- hotspot.status.FIXED
- </span>
<Connect(withCurrentUser(Assignee))
hotspot={
Object {
@@ -500,6 +584,108 @@ exports[`should render correctly: anonymous user 1`] = `
}
onAssigneeChange={[MockFunction]}
/>
+ <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 {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "FIL",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ },
+ "creationDate": "2013-05-13T17:55:41+0200",
+ "key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
+ "line": 142,
+ "message": "'3' is a magic number.",
+ "project": Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "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 [],
+ },
+ "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={[MockFunction]}
+ />
</div>
<HotspotSnippetContainer
hotspot={
@@ -741,17 +927,8 @@ exports[`should render correctly: assignee without name 1`] = `
</div>
</div>
<div
- className="huge-spacer-bottom"
+ className="display-flex-row huge-spacer-bottom"
>
- <span>
- status
- :
- </span>
- <span
- className="badge little-spacer-left"
- >
- hotspot.status.FIXED
- </span>
<Connect(withCurrentUser(Assignee))
hotspot={
Object {
@@ -854,6 +1031,108 @@ exports[`should render correctly: assignee without name 1`] = `
}
onAssigneeChange={[MockFunction]}
/>
+ <Connect(withCurrentUser(Status))
+ hotspot={
+ Object {
+ "assignee": "assignee",
+ "assigneeUser": Object {
+ "active": true,
+ "local": true,
+ "login": "assignee_login",
+ "name": undefined,
+ },
+ "author": "author",
+ "authorUser": Object {
+ "active": true,
+ "local": true,
+ "login": "author",
+ "name": "John Doe",
+ },
+ "canChangeStatus": true,
+ "changelog": Array [],
+ "comment": Array [],
+ "component": Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "FIL",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ },
+ "creationDate": "2013-05-13T17:55:41+0200",
+ "key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
+ "line": 142,
+ "message": "'3' is a magic number.",
+ "project": Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "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 [],
+ },
+ "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={[MockFunction]}
+ />
</div>
<HotspotSnippetContainer
hotspot={
@@ -1095,17 +1374,8 @@ exports[`should render correctly: deleted assignee 1`] = `
</div>
</div>
<div
- className="huge-spacer-bottom"
+ className="display-flex-row huge-spacer-bottom"
>
- <span>
- status
- :
- </span>
- <span
- className="badge little-spacer-left"
- >
- hotspot.status.FIXED
- </span>
<Connect(withCurrentUser(Assignee))
hotspot={
Object {
@@ -1208,6 +1478,108 @@ exports[`should render correctly: deleted assignee 1`] = `
}
onAssigneeChange={[MockFunction]}
/>
+ <Connect(withCurrentUser(Status))
+ hotspot={
+ Object {
+ "assignee": "assignee",
+ "assigneeUser": Object {
+ "active": false,
+ "local": true,
+ "login": "john.doe",
+ "name": "John Doe",
+ },
+ "author": "author",
+ "authorUser": Object {
+ "active": true,
+ "local": true,
+ "login": "author",
+ "name": "John Doe",
+ },
+ "canChangeStatus": true,
+ "changelog": Array [],
+ "comment": Array [],
+ "component": Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "FIL",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ },
+ "creationDate": "2013-05-13T17:55:41+0200",
+ "key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
+ "line": 142,
+ "message": "'3' is a magic number.",
+ "project": Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "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 [],
+ },
+ "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={[MockFunction]}
+ />
</div>
<HotspotSnippetContainer
hotspot={
@@ -1456,17 +1828,8 @@ exports[`should render correctly: unassigned 1`] = `
</div>
</div>
<div
- className="huge-spacer-bottom"
+ className="display-flex-row huge-spacer-bottom"
>
- <span>
- status
- :
- </span>
- <span
- className="badge little-spacer-left"
- >
- hotspot.status.FIXED
- </span>
<Connect(withCurrentUser(Assignee))
hotspot={
Object {
@@ -1569,364 +1932,10 @@ exports[`should render correctly: unassigned 1`] = `
}
onAssigneeChange={[MockFunction]}
/>
- </div>
- <HotspotSnippetContainer
- hotspot={
- Object {
- "assignee": undefined,
- "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 {
- "breadcrumbs": Array [],
- "key": "my-project",
- "name": "MyProject",
- "organization": "foo",
- "qualifier": "FIL",
- "qualityGate": Object {
- "isDefault": true,
- "key": "30",
- "name": "Sonar way",
- },
- "qualityProfiles": Array [
- Object {
- "deleted": false,
- "key": "my-qp",
- "language": "ts",
- "name": "Sonar way",
- },
- ],
- "tags": Array [],
- },
- "creationDate": "2013-05-13T17:55:41+0200",
- "key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
- "line": 142,
- "message": "'3' is a magic number.",
- "project": Object {
- "breadcrumbs": Array [],
- "key": "my-project",
- "name": "MyProject",
- "organization": "foo",
- "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 [],
- },
- "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": undefined,
- "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 {
- "breadcrumbs": Array [],
- "key": "my-project",
- "name": "MyProject",
- "organization": "foo",
- "qualifier": "FIL",
- "qualityGate": Object {
- "isDefault": true,
- "key": "30",
- "name": "Sonar way",
- },
- "qualityProfiles": Array [
- Object {
- "deleted": false,
- "key": "my-qp",
- "language": "ts",
- "name": "Sonar way",
- },
- ],
- "tags": Array [],
- },
- "creationDate": "2013-05-13T17:55:41+0200",
- "key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
- "line": 142,
- "message": "'3' is a magic number.",
- "project": Object {
- "breadcrumbs": Array [],
- "key": "my-project",
- "name": "MyProject",
- "organization": "foo",
- "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 [],
- },
- "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",
- },
- ],
- }
- }
- onUpdateHotspot={[MockFunction]}
- />
- </div>
-</DeferredSpinner>
-`;
-
-exports[`should render correctly: user logged in 1`] = `
-<DeferredSpinner
- loading={false}
- timeout={100}
->
- <div
- className="big-padded"
- >
- <div
- className="big-spacer-bottom"
- >
- <div
- className="display-flex-space-between"
- >
- <h1>
- '3' is a magic number.
- </h1>
- <HotspotActions
- 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 {
- "breadcrumbs": Array [],
- "key": "my-project",
- "name": "MyProject",
- "organization": "foo",
- "qualifier": "FIL",
- "qualityGate": Object {
- "isDefault": true,
- "key": "30",
- "name": "Sonar way",
- },
- "qualityProfiles": Array [
- Object {
- "deleted": false,
- "key": "my-qp",
- "language": "ts",
- "name": "Sonar way",
- },
- ],
- "tags": Array [],
- },
- "creationDate": "2013-05-13T17:55:41+0200",
- "key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
- "line": 142,
- "message": "'3' is a magic number.",
- "project": Object {
- "breadcrumbs": Array [],
- "key": "my-project",
- "name": "MyProject",
- "organization": "foo",
- "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 [],
- },
- "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",
- },
- ],
- }
- }
- onSubmit={[MockFunction]}
- />
- </div>
- <div
- className="text-muted"
- >
- <span>
- category
- :
- </span>
- <span
- className="little-spacer-left"
- >
- SQL injection
- </span>
- </div>
- </div>
- <div
- className="huge-spacer-bottom"
- >
- <span>
- status
- :
- </span>
- <span
- className="badge little-spacer-left"
- >
- hotspot.status.FIXED
- </span>
- <Connect(withCurrentUser(Assignee))
+ <Connect(withCurrentUser(Status))
hotspot={
Object {
- "assignee": "assignee",
+ "assignee": undefined,
"assigneeUser": Object {
"active": true,
"local": true,
@@ -2023,13 +2032,13 @@ exports[`should render correctly: user logged in 1`] = `
],
}
}
- onAssigneeChange={[MockFunction]}
+ onStatusChange={[MockFunction]}
/>
</div>
<HotspotSnippetContainer
hotspot={
Object {
- "assignee": "assignee",
+ "assignee": undefined,
"assigneeUser": Object {
"active": true,
"local": true,
@@ -2130,7 +2139,7 @@ exports[`should render correctly: user logged in 1`] = `
<HotspotViewerTabs
hotspot={
Object {
- "assignee": "assignee",
+ "assignee": undefined,
"assigneeUser": Object {
"active": true,
"local": true,
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/AssigneeSelectionRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/AssigneeSelectionRenderer.tsx
index 2132adb5998..23fedd178a9 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/AssigneeSelectionRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/AssigneeSelectionRenderer.tsx
@@ -47,7 +47,7 @@ export default function AssigneeSelectionRenderer(props: HotspotAssigneeSelectRe
autoFocus={true}
onChange={props.onSearch}
onKeyDown={props.onKeyDown}
- placeholder={translate('hotspots.form.select_user')}
+ placeholder={translate('hotspots.assignee.select_user')}
value={query}
/>
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/__snapshots__/AssigneeSelectionRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/__snapshots__/AssigneeSelectionRenderer-test.tsx.snap
index 93774eb9f81..004aedfae6c 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/__snapshots__/AssigneeSelectionRenderer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/__snapshots__/AssigneeSelectionRenderer-test.tsx.snap
@@ -9,7 +9,7 @@ exports[`should render correctly 1`] = `
autoFocus={true}
onChange={[MockFunction]}
onKeyDown={[MockFunction]}
- placeholder="hotspots.form.select_user"
+ placeholder="hotspots.assignee.select_user"
/>
</div>
</Fragment>
@@ -24,7 +24,7 @@ exports[`should render correctly: loading 1`] = `
autoFocus={true}
onChange={[MockFunction]}
onKeyDown={[MockFunction]}
- placeholder="hotspots.form.select_user"
+ placeholder="hotspots.assignee.select_user"
/>
<DeferredSpinner
className="spacer-left"
@@ -43,7 +43,7 @@ exports[`should render correctly: open 1`] = `
autoFocus={true}
onChange={[MockFunction]}
onKeyDown={[MockFunction]}
- placeholder="hotspots.form.select_user"
+ placeholder="hotspots.assignee.select_user"
/>
</div>
<div
@@ -73,7 +73,7 @@ exports[`should render correctly: open with results 1`] = `
autoFocus={true}
onChange={[MockFunction]}
onKeyDown={[MockFunction]}
- placeholder="hotspots.form.select_user"
+ placeholder="hotspots.assignee.select_user"
/>
</div>
<div
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.css b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.css
new file mode 100644
index 00000000000..b48cb0745fc
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.css
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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.
+ */
+
+#status-trigger,
+.popup {
+ width: 400px;
+ box-sizing: border-box;
+}
+
+#status-trigger {
+ height: 80px;
+ border-radius: 4px;
+ outline: none;
+}
+
+#status-trigger.readonly {
+ cursor: not-allowed;
+}
+
+#status-trigger:not(.readonly) {
+ cursor: pointer;
+ background-color: var(--darkBlue);
+}
+
+#status-trigger:not(.readonly) * {
+ color: white;
+}
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
new file mode 100644
index 00000000000..006b7f58932
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.tsx
@@ -0,0 +1,107 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown';
+import Toggler from 'sonar-ui-common/components/controls/Toggler';
+import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
+import ChevronDownIcon from 'sonar-ui-common/components/icons/ChevronDownIcon';
+import { PopupPlacement } from 'sonar-ui-common/components/ui/popups';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { withCurrentUser } from '../../../../components/hoc/withCurrentUser';
+import { isLoggedIn } from '../../../../helpers/users';
+import { Hotspot } from '../../../../types/security-hotspots';
+import { getStatusOptionFromStatusAndResolution } from '../../utils';
+import './Status.css';
+import StatusDescription from './StatusDescription';
+import StatusSelection from './StatusSelection';
+
+export interface StatusProps {
+ currentUser: T.CurrentUser;
+ hotspot: Hotspot;
+
+ onStatusChange: () => void;
+}
+
+export function Status(props: StatusProps) {
+ const { currentUser, hotspot } = props;
+ const [isOpen, setIsOpen] = React.useState(false);
+
+ const statusOption = getStatusOptionFromStatusAndResolution(hotspot.status, hotspot.resolution);
+ const readonly = !hotspot.canChangeStatus || !isLoggedIn(currentUser);
+
+ const trigger = (
+ <div
+ aria-expanded={isOpen}
+ aria-haspopup={true}
+ className={classNames('padded bordered display-flex-column display-flex-justify-center', {
+ readonly
+ })}
+ id="status-trigger"
+ onClick={() => !readonly && setIsOpen(true)}
+ role="button"
+ tabIndex={0}>
+ <div className="display-flex-center display-flex-space-between">
+ {isOpen ? (
+ <span className="h3">{translate('hotspots.status.select_status')}</span>
+ ) : (
+ <StatusDescription showTitle={true} statusOption={statusOption} />
+ )}
+ {!readonly && <ChevronDownIcon className="big-spacer-left" />}
+ </div>
+ </div>
+ );
+
+ const actionableTrigger = (
+ <Toggler
+ closeOnClickOutside={true}
+ closeOnEscape={true}
+ onRequestClose={() => setIsOpen(false)}
+ open={isOpen}
+ overlay={
+ <DropdownOverlay noPadding={true} placement={PopupPlacement.Bottom}>
+ <StatusSelection
+ hotspot={hotspot}
+ onStatusOptionChange={() => {
+ setIsOpen(false);
+ props.onStatusChange();
+ }}
+ />
+ </DropdownOverlay>
+ }>
+ {trigger}
+ </Toggler>
+ );
+
+ return (
+ <div className="dropdown huge-spacer-left">
+ {readonly ? (
+ <Tooltip overlay={translate('hotspots.status.cannot_change_status')} placement="bottom">
+ {actionableTrigger}
+ </Tooltip>
+ ) : (
+ actionableTrigger
+ )}
+ </div>
+ );
+}
+
+export default withCurrentUser(Status);
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusDescription.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusDescription.tsx
new file mode 100644
index 00000000000..ae853a786c6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusDescription.tsx
@@ -0,0 +1,41 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 React from 'react';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { HotspotStatusOption } from '../../../../types/security-hotspots';
+
+export interface StatusDescriptionProps {
+ statusOption: HotspotStatusOption;
+ showTitle?: boolean;
+}
+
+export default function StatusDescription(props: StatusDescriptionProps) {
+ const { statusOption, showTitle } = props;
+
+ return (
+ <div>
+ <h3>
+ {showTitle && `${translate('status')}: `}
+ {translate('hotspots.status_option', statusOption)}
+ </h3>
+ <span>{translate('hotspots.status_option', statusOption, 'description')}</span>
+ </div>
+ );
+}
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
new file mode 100644
index 00000000000..943fc94f904
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelection.tsx
@@ -0,0 +1,109 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 React from 'react';
+import { setSecurityHotspotStatus } from '../../../../api/security-hotspots';
+import { Hotspot, HotspotStatusOption } from '../../../../types/security-hotspots';
+import {
+ getStatusAndResolutionFromStatusOption,
+ getStatusOptionFromStatusAndResolution
+} from '../../utils';
+import StatusSelectionRenderer from './StatusSelectionRenderer';
+
+interface Props {
+ hotspot: Hotspot;
+ onStatusOptionChange: (statusOption: HotspotStatusOption) => void;
+}
+
+interface State {
+ comment?: string;
+ loading: boolean;
+ initialStatus: HotspotStatusOption;
+ selectedStatus: HotspotStatusOption;
+}
+
+export default class StatusSelection extends React.PureComponent<Props, State> {
+ mounted = false;
+
+ constructor(props: Props) {
+ super(props);
+
+ const initialStatus = getStatusOptionFromStatusAndResolution(
+ props.hotspot.status,
+ props.hotspot.resolution
+ );
+
+ this.state = {
+ loading: false,
+ initialStatus,
+ selectedStatus: initialStatus
+ };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleStatusChange = (selectedStatus: HotspotStatusOption) => {
+ this.setState({ selectedStatus });
+ };
+
+ handleCommentChange = (comment: string) => {
+ this.setState({ comment });
+ };
+
+ handleSubmit = () => {
+ const { hotspot } = this.props;
+ const { comment, initialStatus, selectedStatus } = this.state;
+
+ if (selectedStatus && selectedStatus !== initialStatus) {
+ this.setState({ loading: true });
+ setSecurityHotspotStatus(hotspot.key, {
+ ...getStatusAndResolutionFromStatusOption(selectedStatus),
+ comment: comment || undefined
+ })
+ .then(() => {
+ this.setState({ loading: false });
+ this.props.onStatusOptionChange(selectedStatus);
+ })
+ .catch(() => this.setState({ loading: false }));
+ }
+ };
+
+ render() {
+ const { comment, initialStatus, loading, selectedStatus } = this.state;
+ const submitDisabled = selectedStatus === initialStatus;
+
+ return (
+ <StatusSelectionRenderer
+ comment={comment}
+ loading={loading}
+ onCommentChange={this.handleCommentChange}
+ onStatusChange={this.handleStatusChange}
+ onSubmit={this.handleSubmit}
+ selectedStatus={selectedStatus}
+ submitDisabled={submitDisabled}
+ />
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelectionRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelectionRenderer.tsx
new file mode 100644
index 00000000000..16487044701
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelectionRenderer.tsx
@@ -0,0 +1,91 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 React from 'react';
+import { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
+import Radio from 'sonar-ui-common/components/controls/Radio';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import MarkdownTips from '../../../../components/common/MarkdownTips';
+import { HotspotStatusOption } from '../../../../types/security-hotspots';
+import StatusDescription from './StatusDescription';
+
+export interface StatusSelectionRendererProps {
+ selectedStatus: HotspotStatusOption;
+ onStatusChange: (statusOption: HotspotStatusOption) => void;
+
+ comment?: string;
+ onCommentChange: (comment: string) => void;
+
+ onSubmit: () => void;
+
+ loading: boolean;
+ submitDisabled: boolean;
+}
+
+export default function StatusSelectionRenderer(props: StatusSelectionRendererProps) {
+ const { comment, loading, selectedStatus, submitDisabled } = props;
+
+ const renderOption = (status: HotspotStatusOption) => {
+ return (
+ <Radio
+ checked={selectedStatus === status}
+ className="big-spacer-bottom"
+ onCheck={props.onStatusChange}
+ value={status}>
+ <StatusDescription statusOption={status} />
+ </Radio>
+ );
+ };
+
+ return (
+ <>
+ <div className="big-padded">
+ {renderOption(HotspotStatusOption.TO_REVIEW)}
+ {renderOption(HotspotStatusOption.FIXED)}
+ {renderOption(HotspotStatusOption.SAFE)}
+ </div>
+
+ <hr />
+ <div className="big-padded display-flex-column">
+ <label className="text-bold" htmlFor="comment-textarea">
+ {translate('hotspots.status.add_comment')}
+ </label>
+ <textarea
+ className="spacer-top form-field fixed-width spacer-bottom"
+ id="comment-textarea"
+ onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) =>
+ props.onCommentChange(event.currentTarget.value)
+ }
+ rows={4}
+ value={comment}
+ />
+ <MarkdownTips />
+
+ <div className="big-spacer-top display-flex-justify-end display-flex-center">
+ <SubmitButton disabled={submitDisabled || loading} onClick={props.onSubmit}>
+ {translate('hotspots.status.change_status')}
+ </SubmitButton>
+
+ {loading && <i className="spacer-left spinner" />}
+ </div>
+ </div>
+ </>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/Status-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/Status-test.tsx
new file mode 100644
index 00000000000..309dba42348
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/Status-test.tsx
@@ -0,0 +1,74 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown';
+import Toggler from 'sonar-ui-common/components/controls/Toggler';
+import { click } from 'sonar-ui-common/helpers/testUtils';
+import { mockHotspot } from '../../../../../helpers/mocks/security-hotspots';
+import { mockCurrentUser } from '../../../../../helpers/testMocks';
+import { HotspotStatusOption } from '../../../../../types/security-hotspots';
+import { Status, StatusProps } from '../Status';
+import StatusSelection from '../StatusSelection';
+
+it('should render correctly', () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot('closed');
+
+ click(wrapper.find('#status-trigger'));
+ expect(wrapper).toMatchSnapshot('open');
+
+ wrapper
+ .find(Toggler)
+ .props()
+ .onRequestClose();
+ expect(wrapper.find(DropdownOverlay).length).toBe(0);
+
+ expect(shallowRender({ hotspot: mockHotspot({ canChangeStatus: false }) })).toMatchSnapshot(
+ 'readonly'
+ );
+});
+
+it('should properly deal with status changes', () => {
+ const onStatusChange = jest.fn();
+ const wrapper = shallowRender({ onStatusChange });
+
+ click(wrapper.find('#status-trigger'));
+ wrapper
+ .find(Toggler)
+ .dive()
+ .find(StatusSelection)
+ .props()
+ .onStatusOptionChange(HotspotStatusOption.SAFE);
+ expect(onStatusChange).toHaveBeenCalled();
+ expect(wrapper.find(DropdownOverlay).length).toBe(0);
+});
+
+function shallowRender(props?: Partial<StatusProps>) {
+ return shallow<StatusProps>(
+ <Status
+ currentUser={mockCurrentUser({ isLoggedIn: true })}
+ hotspot={mockHotspot()}
+ onStatusChange={jest.fn()}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusDescription-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusDescription-test.tsx
new file mode 100644
index 00000000000..048f5c6aec5
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusDescription-test.tsx
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 StatusDescription, { StatusDescriptionProps } from '../StatusDescription';
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+ expect(shallowRender({ showTitle: true })).toMatchSnapshot('with title');
+});
+
+function shallowRender(props?: Partial<StatusDescriptionProps>) {
+ return shallow<StatusDescriptionProps>(
+ <StatusDescription statusOption={HotspotStatusOption.TO_REVIEW} {...props} />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelection-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelection-test.tsx
new file mode 100644
index 00000000000..9b1d44c0fa8
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelection-test.tsx
@@ -0,0 +1,78 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import { setSecurityHotspotStatus } from '../../../../../api/security-hotspots';
+import { mockHotspot } from '../../../../../helpers/mocks/security-hotspots';
+import { HotspotStatus, HotspotStatusOption } from '../../../../../types/security-hotspots';
+import StatusSelection from '../StatusSelection';
+import StatusSelectionRenderer from '../StatusSelectionRenderer';
+
+jest.mock('../../../../../api/security-hotspots', () => ({
+ setSecurityHotspotStatus: jest.fn()
+}));
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should properly deal with comment/status/submit events', async () => {
+ const hotspot = mockHotspot();
+ const onStatusOptionChange = jest.fn();
+ const wrapper = shallowRender({ hotspot, onStatusOptionChange });
+
+ const newStatusOption = HotspotStatusOption.SAFE;
+ wrapper
+ .find(StatusSelectionRenderer)
+ .props()
+ .onStatusChange(newStatusOption);
+ expect(wrapper.state().selectedStatus).toBe(newStatusOption);
+ expect(wrapper.find(StatusSelectionRenderer).props().submitDisabled).toBe(false);
+
+ const newComment = 'TEST-COMMENT';
+ wrapper
+ .find(StatusSelectionRenderer)
+ .props()
+ .onCommentChange(newComment);
+ expect(wrapper.state().comment).toBe(newComment);
+
+ (setSecurityHotspotStatus as jest.Mock).mockResolvedValueOnce({});
+ wrapper
+ .find(StatusSelectionRenderer)
+ .props()
+ .onSubmit();
+ expect(setSecurityHotspotStatus).toHaveBeenCalledWith(hotspot.key, {
+ status: HotspotStatus.REVIEWED,
+ resolution: HotspotStatusOption.SAFE,
+ comment: newComment
+ });
+
+ await waitAndUpdate(wrapper);
+
+ expect(onStatusOptionChange).toHaveBeenCalledWith(newStatusOption);
+});
+
+function shallowRender(props?: Partial<StatusSelection['props']>) {
+ return shallow<StatusSelection>(
+ <StatusSelection hotspot={mockHotspot()} onStatusOptionChange={jest.fn()} {...props} />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelectionRenderer-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelectionRenderer-test.tsx
new file mode 100644
index 00000000000..f63e47f45c6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelectionRenderer-test.tsx
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
+import Radio from 'sonar-ui-common/components/controls/Radio';
+import { change, click } from 'sonar-ui-common/helpers/testUtils';
+import { HotspotStatusOption } from '../../../../../types/security-hotspots';
+import StatusSelectionRenderer, { StatusSelectionRendererProps } from '../StatusSelectionRenderer';
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+ expect(shallowRender({ loading: true })).toMatchSnapshot('loading');
+ expect(
+ shallowRender({ submitDisabled: true })
+ .find(SubmitButton)
+ .props().disabled
+ ).toBe(true);
+});
+
+it('should call proper callbacks on actions', () => {
+ const onCommentChange = jest.fn();
+ const onStatusChange = jest.fn();
+ const onSubmit = jest.fn();
+ const wrapper = shallowRender({ onCommentChange, onStatusChange, onSubmit });
+
+ change(wrapper.find('textarea'), 'TATA');
+ expect(onCommentChange).toHaveBeenCalledWith('TATA');
+
+ wrapper
+ .find(Radio)
+ .first()
+ .props()
+ .onCheck(HotspotStatusOption.SAFE);
+ expect(onStatusChange).toHaveBeenCalledWith(HotspotStatusOption.SAFE);
+
+ click(wrapper.find(SubmitButton));
+ expect(onSubmit).toHaveBeenCalled();
+});
+
+function shallowRender(props?: Partial<StatusSelectionRendererProps>) {
+ return shallow<StatusSelectionRendererProps>(
+ <StatusSelectionRenderer
+ comment="TEST-COMMENT"
+ loading={false}
+ onCommentChange={jest.fn()}
+ onStatusChange={jest.fn()}
+ onSubmit={jest.fn()}
+ selectedStatus={HotspotStatusOption.TO_REVIEW}
+ submitDisabled={false}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/Status-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/Status-test.tsx.snap
new file mode 100644
index 00000000000..36c5ed7da90
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/Status-test.tsx.snap
@@ -0,0 +1,436 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: closed 1`] = `
+<div
+ className="dropdown huge-spacer-left"
+>
+ <Toggler
+ closeOnClickOutside={true}
+ closeOnEscape={true}
+ onRequestClose={[Function]}
+ open={false}
+ overlay={
+ <DropdownOverlay
+ noPadding={true}
+ placement="bottom"
+ >
+ <StatusSelection
+ 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 {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "FIL",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ },
+ "creationDate": "2013-05-13T17:55:41+0200",
+ "key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
+ "line": 142,
+ "message": "'3' is a magic number.",
+ "project": Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "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 [],
+ },
+ "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",
+ },
+ ],
+ }
+ }
+ onStatusOptionChange={[Function]}
+ />
+ </DropdownOverlay>
+ }
+ >
+ <div
+ aria-expanded={false}
+ aria-haspopup={true}
+ className="padded bordered display-flex-column display-flex-justify-center"
+ id="status-trigger"
+ onClick={[Function]}
+ role="button"
+ tabIndex={0}
+ >
+ <div
+ className="display-flex-center display-flex-space-between"
+ >
+ <StatusDescription
+ showTitle={true}
+ statusOption="FIXED"
+ />
+ <ChevronDownIcon
+ className="big-spacer-left"
+ />
+ </div>
+ </div>
+ </Toggler>
+</div>
+`;
+
+exports[`should render correctly: open 1`] = `
+<div
+ className="dropdown huge-spacer-left"
+>
+ <Toggler
+ closeOnClickOutside={true}
+ closeOnEscape={true}
+ onRequestClose={[Function]}
+ open={true}
+ overlay={
+ <DropdownOverlay
+ noPadding={true}
+ placement="bottom"
+ >
+ <StatusSelection
+ 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 {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "FIL",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ },
+ "creationDate": "2013-05-13T17:55:41+0200",
+ "key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
+ "line": 142,
+ "message": "'3' is a magic number.",
+ "project": Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "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 [],
+ },
+ "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",
+ },
+ ],
+ }
+ }
+ onStatusOptionChange={[Function]}
+ />
+ </DropdownOverlay>
+ }
+ >
+ <div
+ aria-expanded={true}
+ aria-haspopup={true}
+ className="padded bordered display-flex-column display-flex-justify-center"
+ id="status-trigger"
+ onClick={[Function]}
+ role="button"
+ tabIndex={0}
+ >
+ <div
+ className="display-flex-center display-flex-space-between"
+ >
+ <span
+ className="h3"
+ >
+ hotspots.status.select_status
+ </span>
+ <ChevronDownIcon
+ className="big-spacer-left"
+ />
+ </div>
+ </div>
+ </Toggler>
+</div>
+`;
+
+exports[`should render correctly: readonly 1`] = `
+<div
+ className="dropdown huge-spacer-left"
+>
+ <Tooltip
+ overlay="hotspots.status.cannot_change_status"
+ placement="bottom"
+ >
+ <Toggler
+ closeOnClickOutside={true}
+ closeOnEscape={true}
+ onRequestClose={[Function]}
+ open={false}
+ overlay={
+ <DropdownOverlay
+ noPadding={true}
+ placement="bottom"
+ >
+ <StatusSelection
+ 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": false,
+ "changelog": Array [],
+ "comment": Array [],
+ "component": Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "FIL",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ },
+ "creationDate": "2013-05-13T17:55:41+0200",
+ "key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
+ "line": 142,
+ "message": "'3' is a magic number.",
+ "project": Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "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 [],
+ },
+ "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",
+ },
+ ],
+ }
+ }
+ onStatusOptionChange={[Function]}
+ />
+ </DropdownOverlay>
+ }
+ >
+ <div
+ aria-expanded={false}
+ aria-haspopup={true}
+ className="padded bordered display-flex-column display-flex-justify-center readonly"
+ id="status-trigger"
+ onClick={[Function]}
+ role="button"
+ tabIndex={0}
+ >
+ <div
+ className="display-flex-center display-flex-space-between"
+ >
+ <StatusDescription
+ showTitle={true}
+ statusOption="FIXED"
+ />
+ </div>
+ </div>
+ </Toggler>
+ </Tooltip>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusDescription-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusDescription-test.tsx.snap
new file mode 100644
index 00000000000..b77051910de
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusDescription-test.tsx.snap
@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div>
+ <h3>
+ hotspots.status_option.TO_REVIEW
+ </h3>
+ <span>
+ hotspots.status_option.TO_REVIEW.description
+ </span>
+</div>
+`;
+
+exports[`should render correctly: with title 1`] = `
+<div>
+ <h3>
+ status:
+ hotspots.status_option.TO_REVIEW
+ </h3>
+ <span>
+ hotspots.status_option.TO_REVIEW.description
+ </span>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelection-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelection-test.tsx.snap
new file mode 100644
index 00000000000..61d22f512e6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelection-test.tsx.snap
@@ -0,0 +1,12 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<StatusSelectionRenderer
+ loading={false}
+ onCommentChange={[Function]}
+ onStatusChange={[Function]}
+ onSubmit={[Function]}
+ selectedStatus="FIXED"
+ submitDisabled={true}
+/>
+`;
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelectionRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelectionRenderer-test.tsx.snap
new file mode 100644
index 00000000000..482dd86bb54
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelectionRenderer-test.tsx.snap
@@ -0,0 +1,140 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<Fragment>
+ <div
+ className="big-padded"
+ >
+ <Radio
+ checked={true}
+ className="big-spacer-bottom"
+ onCheck={[MockFunction]}
+ value="TO_REVIEW"
+ >
+ <StatusDescription
+ statusOption="TO_REVIEW"
+ />
+ </Radio>
+ <Radio
+ checked={false}
+ className="big-spacer-bottom"
+ onCheck={[MockFunction]}
+ value="FIXED"
+ >
+ <StatusDescription
+ statusOption="FIXED"
+ />
+ </Radio>
+ <Radio
+ checked={false}
+ className="big-spacer-bottom"
+ onCheck={[MockFunction]}
+ value="SAFE"
+ >
+ <StatusDescription
+ statusOption="SAFE"
+ />
+ </Radio>
+ </div>
+ <hr />
+ <div
+ className="big-padded display-flex-column"
+ >
+ <label
+ className="text-bold"
+ htmlFor="comment-textarea"
+ >
+ hotspots.status.add_comment
+ </label>
+ <textarea
+ className="spacer-top form-field fixed-width spacer-bottom"
+ id="comment-textarea"
+ onChange={[Function]}
+ rows={4}
+ value="TEST-COMMENT"
+ />
+ <MarkdownTips />
+ <div
+ className="big-spacer-top display-flex-justify-end display-flex-center"
+ >
+ <SubmitButton
+ disabled={false}
+ onClick={[MockFunction]}
+ >
+ hotspots.status.change_status
+ </SubmitButton>
+ </div>
+ </div>
+</Fragment>
+`;
+
+exports[`should render correctly: loading 1`] = `
+<Fragment>
+ <div
+ className="big-padded"
+ >
+ <Radio
+ checked={true}
+ className="big-spacer-bottom"
+ onCheck={[MockFunction]}
+ value="TO_REVIEW"
+ >
+ <StatusDescription
+ statusOption="TO_REVIEW"
+ />
+ </Radio>
+ <Radio
+ checked={false}
+ className="big-spacer-bottom"
+ onCheck={[MockFunction]}
+ value="FIXED"
+ >
+ <StatusDescription
+ statusOption="FIXED"
+ />
+ </Radio>
+ <Radio
+ checked={false}
+ className="big-spacer-bottom"
+ onCheck={[MockFunction]}
+ value="SAFE"
+ >
+ <StatusDescription
+ statusOption="SAFE"
+ />
+ </Radio>
+ </div>
+ <hr />
+ <div
+ className="big-padded display-flex-column"
+ >
+ <label
+ className="text-bold"
+ htmlFor="comment-textarea"
+ >
+ hotspots.status.add_comment
+ </label>
+ <textarea
+ className="spacer-top form-field fixed-width spacer-bottom"
+ id="comment-textarea"
+ onChange={[Function]}
+ rows={4}
+ value="TEST-COMMENT"
+ />
+ <MarkdownTips />
+ <div
+ className="big-spacer-top display-flex-justify-end display-flex-center"
+ >
+ <SubmitButton
+ disabled={true}
+ onClick={[MockFunction]}
+ >
+ hotspots.status.change_status
+ </SubmitButton>
+ <i
+ className="spacer-left spinner"
+ />
+ </div>
+ </div>
+</Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/styles.css b/server/sonar-web/src/main/js/apps/security-hotspots/styles.css
index 9a327c158d7..f95c95b6331 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/styles.css
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/styles.css
@@ -51,10 +51,3 @@
overflow-y: auto;
background-color: white;
}
-
-/*
-* Align description with label by offsetting by width of radio + margin
-*/
-#security_hotspots .radio-button-description {
- margin-left: 23px;
-}
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 cfa1e81f5ed..0f3165da554 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
@@ -20,6 +20,9 @@
import { groupBy, sortBy } from 'lodash';
import {
Hotspot,
+ HotspotResolution,
+ HotspotStatus,
+ HotspotStatusOption,
RawHotspot,
ReviewHistoryElement,
ReviewHistoryType,
@@ -137,3 +140,35 @@ export function getHotspotReviewHistory(
functionalCount
};
}
+
+const STATUS_AND_RESOLUTION_TO_STATUS_OPTION = {
+ [HotspotStatus.TO_REVIEW]: HotspotStatusOption.TO_REVIEW,
+ [HotspotStatus.REVIEWED]: HotspotStatusOption.FIXED,
+ [HotspotResolution.FIXED]: HotspotStatusOption.FIXED,
+ [HotspotResolution.SAFE]: HotspotStatusOption.SAFE
+};
+
+export function getStatusOptionFromStatusAndResolution(
+ status: HotspotStatus,
+ resolution?: HotspotResolution
+) {
+ // Resolution is the most determinist info here, so we use it first to get the matching status option
+ // If not provided, we use the status (which will be TO_REVIEW)
+ return STATUS_AND_RESOLUTION_TO_STATUS_OPTION[resolution ?? status];
+}
+
+const STATUS_OPTION_TO_STATUS_AND_RESOLUTION_MAP = {
+ [HotspotStatusOption.TO_REVIEW]: { status: HotspotStatus.TO_REVIEW, resolution: undefined },
+ [HotspotStatusOption.FIXED]: {
+ status: HotspotStatus.REVIEWED,
+ resolution: HotspotResolution.FIXED
+ },
+ [HotspotStatusOption.SAFE]: {
+ status: HotspotStatus.REVIEWED,
+ resolution: HotspotResolution.SAFE
+ }
+};
+
+export function getStatusAndResolutionFromStatusOption(statusOption: HotspotStatusOption) {
+ return STATUS_OPTION_TO_STATUS_AND_RESOLUTION_MAP[statusOption];
+}
diff --git a/server/sonar-web/src/main/js/types/security-hotspots.ts b/server/sonar-web/src/main/js/types/security-hotspots.ts
index 00ac45c8c14..6ab8963e914 100644
--- a/server/sonar-web/src/main/js/types/security-hotspots.ts
+++ b/server/sonar-web/src/main/js/types/security-hotspots.ts
@@ -42,7 +42,7 @@ export enum HotspotStatusFilter {
export enum HotspotStatusOption {
FIXED = 'FIXED',
SAFE = 'SAFE',
- ADDITIONAL_REVIEW = 'ADDITIONAL_REVIEW'
+ TO_REVIEW = 'TO_REVIEW'
}
export interface HotspotFilters {
@@ -60,10 +60,10 @@ export interface RawHotspot {
line?: number;
message: string;
project: string;
- resolution?: string;
+ resolution?: HotspotResolution;
rule: string;
securityCategory: string;
- status: string;
+ status: HotspotStatus;
subProject?: string;
updateDate: string;
vulnerabilityProbability: RiskExposure;