From 4a49b28ca62eb6b89e03dc277b255d98d3d11ae3 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lievremont Date: Thu, 12 Nov 2020 15:43:50 +0100 Subject: [PATCH] SONAR-14111 Allow user to select IDE when several ones are detected --- .../components/HotspotOpenInIdeButton.tsx | 88 ++++++++++++----- .../components/HotspotOpenInIdeOverlay.tsx | 41 ++++++++ .../__tests__/HotspotOpenInIdeButton-test.tsx | 63 ++++++++++--- .../HotspotOpenInIdeOverlay-test.tsx | 53 +++++++++++ .../HotspotOpenInIdeButton-test.tsx.snap | 94 +++++++++++++++++-- .../HotspotOpenInIdeOverlay-test.tsx.snap | 32 +++++++ 6 files changed, 323 insertions(+), 48 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeOverlay.tsx create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotOpenInIdeOverlay-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotOpenInIdeOverlay-test.tsx.snap diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeButton.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeButton.tsx index 2a35407be6e..53bfbdb08d7 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeButton.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeButton.tsx @@ -20,11 +20,15 @@ import * as React from 'react'; import { Button } from 'sonar-ui-common/components/controls/buttons'; +import { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown'; +import Toggler from 'sonar-ui-common/components/controls/Toggler'; import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; import { translate } from 'sonar-ui-common/helpers/l10n'; import addGlobalErrorMessage from '../../../app/utils/addGlobalErrorMessage'; import addGlobalSuccessMessage from '../../../app/utils/addGlobalSuccessMessage'; import { openHotspot, probeSonarLintServers } from '../../../helpers/sonarlint'; +import { Ide } from '../../../types/sonarlint'; +import { HotspotOpenInIdeOverlay } from './HotspotOpenInIdeOverlay'; interface Props { projectKey: string; @@ -32,43 +36,75 @@ interface Props { } interface State { - inDiscovery: boolean; + loading: boolean; + ides: Array; } export default class HotspotOpenInIdeButton extends React.PureComponent { + mounted = false; + state = { - inDiscovery: false + loading: false, + ides: [] }; - handleOnClick = () => { + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleOnClick = async () => { + this.setState({ loading: true, ides: [] }); + const ides = await probeSonarLintServers(); + if (ides.length === 0) { + if (this.mounted) { + this.setState({ loading: false }); + } + this.showError(); + } else if (ides.length === 1) { + this.openHotspot(ides[0]); + } else if (this.mounted) { + this.setState({ loading: false, ides }); + } + }; + + openHotspot = (ide: Ide) => { + this.setState({ loading: true, ides: [] }); const { projectKey, hotspotKey } = this.props; - this.setState({ inDiscovery: true }); - return probeSonarLintServers() - .then(ides => { - if (ides.length > 0) { - const calledPort = ides[0].port; - return openHotspot(calledPort, projectKey, hotspotKey); - } else { - return Promise.reject(); - } - }) - .then(() => { - addGlobalSuccessMessage(translate('hotspots.open_in_ide.success')); - }) - .catch(() => { - addGlobalErrorMessage(translate('hotspots.open_in_ide.failure')); - }) - .finally(() => { - this.setState({ inDiscovery: false }); - }); + return openHotspot(ide.port, projectKey, hotspotKey) + .then(this.showSuccess) + .catch(this.showError) + .finally(this.cleanState); + }; + + showError = () => addGlobalErrorMessage(translate('hotspots.open_in_ide.failure')); + + showSuccess = () => addGlobalSuccessMessage(translate('hotspots.open_in_ide.success')); + + cleanState = () => { + if (this.mounted) { + this.setState({ loading: false, ides: [] }); + } }; render() { return ( - + 1} + onRequestClose={this.cleanState} + overlay={ + + + + }> + + ); } } diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeOverlay.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeOverlay.tsx new file mode 100644 index 00000000000..1df109c6fad --- /dev/null +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeOverlay.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 { Ide } from '../../../types/sonarlint'; + +export const HotspotOpenInIdeOverlay = ({ + ides, + onIdeSelected +}: { + ides: Array; + onIdeSelected: (ide: Ide) => Promise; +}) => + ides.length > 1 ? ( + + ) : null; diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotOpenInIdeButton-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotOpenInIdeButton-test.tsx index 025668b120e..8410c0d7977 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotOpenInIdeButton-test.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotOpenInIdeButton-test.tsx @@ -25,21 +25,58 @@ import HotspotOpenInIdeButton from '../HotspotOpenInIdeButton'; jest.mock('../../../../helpers/sonarlint'); -it('should render correctly', async () => { - const projectKey = 'my-project:key'; - const hotspotKey = 'AXWsgE9RpggAQesHYfwm'; +describe('HotspotOpenInIdeButton', () => { + beforeEach(jest.resetAllMocks); - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); + it('should render correctly', async () => { + const projectKey = 'my-project:key'; + const hotspotKey = 'AXWsgE9RpggAQesHYfwm'; + const port = 42001; - (sonarlint.probeSonarLintServers as jest.Mock).mockResolvedValue([ - { port: 42001, ideName: 'BlueJ IDE', description: 'Hello World' } - ]); + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); - wrapper.find(Button).simulate('click'); + (sonarlint.probeSonarLintServers as jest.Mock).mockResolvedValue([ + { port, ideName: 'BlueJ IDE', description: 'Hello World' } + ]); + (sonarlint.openHotspot as jest.Mock).mockResolvedValue(null); - await new Promise(setImmediate); - expect(sonarlint.openHotspot).toBeCalledWith(42001, projectKey, hotspotKey); + wrapper.find(Button).simulate('click'); + + await new Promise(setImmediate); + expect(sonarlint.openHotspot).toBeCalledWith(port, projectKey, hotspotKey); + }); + + it('should gracefully handle zero IDE detected', async () => { + const wrapper = shallow(); + (sonarlint.probeSonarLintServers as jest.Mock).mockResolvedValue([]); + wrapper.find(Button).simulate('click'); + + await new Promise(setImmediate); + expect(sonarlint.openHotspot).not.toHaveBeenCalled(); + }); + + it('should handle several IDE', async () => { + const projectKey = 'my-project:key'; + const hotspotKey = 'AXWsgE9RpggAQesHYfwm'; + const port1 = 42000; + const port2 = 42001; + + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + + (sonarlint.probeSonarLintServers as jest.Mock).mockResolvedValue([ + { port: port1, ideName: 'BlueJ IDE', description: 'Hello World' }, + { port: port2, ideName: 'Arduino IDE', description: 'Blink' } + ]); + + wrapper.find(Button).simulate('click'); + + await new Promise(setImmediate); + expect(wrapper).toMatchSnapshot('dropdown open'); + }); }); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotOpenInIdeOverlay-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotOpenInIdeOverlay-test.tsx new file mode 100644 index 00000000000..d131f21f455 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotOpenInIdeOverlay-test.tsx @@ -0,0 +1,53 @@ +/* + * 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 { HotspotOpenInIdeOverlay } from '../HotspotOpenInIdeOverlay'; + +it('should render nothing with fewer than 2 IDE', () => { + const onIdeSelected = jest.fn(); + expect( + shallow().type() + ).toBeNull(); + expect( + shallow( + + ).type() + ).toBeNull(); +}); + +it('should render menu and select the right IDE', () => { + const onIdeSelected = jest.fn(); + const ide1 = { port: 0, ideName: 'Polop', description: 'Plouf' }; + const ide2 = { port: 1, ideName: 'Foo', description: 'Bar' }; + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + + wrapper + .find('a') + .last() + .simulate('click'); + expect(onIdeSelected).toHaveBeenCalledWith(ide2); +}); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotOpenInIdeButton-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotOpenInIdeButton-test.tsx.snap index 396feb43591..c8e8a39851d 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotOpenInIdeButton-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotOpenInIdeButton-test.tsx.snap @@ -1,13 +1,89 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should render correctly 1`] = ` - + + `; + +exports[`HotspotOpenInIdeButton should handle several IDE: dropdown open 1`] = ` + + + + } +> + + +`; + +exports[`HotspotOpenInIdeButton should render correctly 1`] = ` + + + + } +> + + +`; \ No newline at end of file diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotOpenInIdeOverlay-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotOpenInIdeOverlay-test.tsx.snap new file mode 100644 index 00000000000..7c4ffa6c9ef --- /dev/null +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotOpenInIdeOverlay-test.tsx.snap @@ -0,0 +1,32 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render menu and select the right IDE 1`] = ` + +`; -- 2.39.5