diff options
author | Philippe Perrin <philippe.perrin@sonarsource.com> | 2020-02-11 18:49:50 +0100 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2020-02-21 20:46:19 +0100 |
commit | 9e025bf15700eff81e11cb00bcf5e9650f765ee9 (patch) | |
tree | a4e482db7368c393f732ca0829cdd8450f19f3ab /server | |
parent | 92bdc9faf99a89c0c70945198045b1de81db2ed7 (diff) | |
download | sonarqube-9e025bf15700eff81e11cb00bcf5e9650f765ee9.tar.gz sonarqube-9e025bf15700eff81e11cb00bcf5e9650f765ee9.zip |
SONAR-12719 Move the status edition popup in hotspot main screen
Diffstat (limited to 'server')
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; |