From 9e025bf15700eff81e11cb00bcf5e9650f765ee9 Mon Sep 17 00:00:00 2001 From: Philippe Perrin Date: Tue, 11 Feb 2020 18:49:50 +0100 Subject: [PATCH] SONAR-12719 Move the status edition popup in hotspot main screen --- .../src/main/js/app/styles/init/icons.css | 2 +- .../src/main/js/app/styles/init/misc.css | 5 + .../security-hotspots/__tests__/utils-test.ts | 51 +- .../components/HotspotActions.tsx | 77 -- .../components/HotspotActionsForm.tsx | 163 ---- .../components/HotspotActionsFormRenderer.tsx | 166 ---- .../components/HotspotListItem.tsx | 6 +- .../components/HotspotViewerRenderer.tsx | 21 +- .../__tests__/HotspotActions-test.tsx | 78 -- .../__tests__/HotspotActionsForm-test.tsx | 155 --- .../HotspotActionsFormRenderer-test.tsx | 109 --- .../__tests__/HotspotViewerRenderer-test.tsx | 6 +- .../HotspotActions-test.tsx.snap | 406 -------- .../HotspotActionsForm-test.tsx.snap | 112 --- .../HotspotActionsFormRenderer-test.tsx.snap | 544 ----------- .../HotspotListItem-test.tsx.snap | 4 +- .../__snapshots__/HotspotViewer-test.tsx.snap | 4 +- .../HotspotViewerRenderer-test.tsx.snap | 891 +++++++++--------- .../assignee/AssigneeSelectionRenderer.tsx | 2 +- .../AssigneeSelectionRenderer-test.tsx.snap | 8 +- .../components/status/Status.css | 44 + .../components/status/Status.tsx | 107 +++ .../components/status/StatusDescription.tsx | 41 + .../components/status/StatusSelection.tsx | 109 +++ .../status/StatusSelectionRenderer.tsx | 91 ++ .../status/__tests__/Status-test.tsx | 74 ++ .../__tests__/StatusDescription-test.tsx | 35 + .../status/__tests__/StatusSelection-test.tsx | 78 ++ .../StatusSelectionRenderer-test.tsx | 72 ++ .../__snapshots__/Status-test.tsx.snap | 436 +++++++++ .../StatusDescription-test.tsx.snap | 24 + .../StatusSelection-test.tsx.snap | 12 + .../StatusSelectionRenderer-test.tsx.snap | 140 +++ .../main/js/apps/security-hotspots/styles.css | 7 - .../main/js/apps/security-hotspots/utils.ts | 35 + .../src/main/js/types/security-hotspots.ts | 6 +- .../resources/org/sonar/l10n/core.properties | 38 +- 37 files changed, 1838 insertions(+), 2321 deletions(-) delete mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActions.tsx delete mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActionsForm.tsx delete mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotActionsFormRenderer.tsx delete mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActions-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActionsForm-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotActionsFormRenderer-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActions-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActionsForm-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotActionsFormRenderer-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.css create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/status/Status.tsx create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusDescription.tsx create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelection.tsx create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelectionRenderer.tsx create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/Status-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusDescription-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelection-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/StatusSelectionRenderer-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/Status-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusDescription-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelection-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/status/__tests__/__snapshots__/StatusSelectionRenderer-test.tsx.snap 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 ( -
- - - {open && ( - setOpen(false)}> - - { - setOpen(false); - props.onSubmit(); - }} - /> - - - )} -
- ); -} 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 { - 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) => { - 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 ( - - ); - } -} 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) => 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 ( -
-

- {disableStatusChange - ? translate('hotspots.form.title.disabled') - : translate('hotspots.form.title')} -

-
- {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 - })} -
-
- -