diff options
author | Jean-Baptiste Lievremont <jeanbaptiste.lievremont@sonarsource.com> | 2020-11-12 15:43:50 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2020-11-26 20:06:29 +0000 |
commit | 4a49b28ca62eb6b89e03dc277b255d98d3d11ae3 (patch) | |
tree | ac299b49fd1458571c46237f65b0d57ac49a0ada /server/sonar-web | |
parent | d570089a93d76a2764eec9728a623c2d50e3ae3a (diff) | |
download | sonarqube-4a49b28ca62eb6b89e03dc277b255d98d3d11ae3.tar.gz sonarqube-4a49b28ca62eb6b89e03dc277b255d98d3d11ae3.zip |
SONAR-14111 Allow user to select IDE when several ones are detected
Diffstat (limited to 'server/sonar-web')
6 files changed, 323 insertions, 48 deletions
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<Ide>; } export default class HotspotOpenInIdeButton extends React.PureComponent<Props, State> { + 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 ( - <Button onClick={this.handleOnClick}> - {translate('hotspots.open_in_ide.open')} - <DeferredSpinner loading={this.state.inDiscovery} className="spacer-left" /> - </Button> + <Toggler + open={this.state.ides.length > 1} + onRequestClose={this.cleanState} + overlay={ + <DropdownOverlay> + <HotspotOpenInIdeOverlay ides={this.state.ides} onIdeSelected={this.openHotspot} /> + </DropdownOverlay> + }> + <Button onClick={this.handleOnClick}> + {translate('hotspots.open_in_ide.open')} + <DeferredSpinner loading={this.state.loading} className="spacer-left" /> + </Button> + </Toggler> ); } } 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<Ide>; + onIdeSelected: (ide: Ide) => Promise<void>; +}) => + ides.length > 1 ? ( + <ul className="menu"> + {ides.map(ide => ( + <li key={ide.port}> + <a href="#" onClick={() => onIdeSelected(ide)}> + {ide.ideName} - {ide.description} + </a> + </li> + ))} + </ul> + ) : 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( - <HotspotOpenInIdeButton projectKey={projectKey} hotspotKey={hotspotKey} /> - ); - 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( + <HotspotOpenInIdeButton projectKey={projectKey} hotspotKey={hotspotKey} /> + ); + 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(<HotspotOpenInIdeButton projectKey="polop" hotspotKey="palap" />); + (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( + <HotspotOpenInIdeButton projectKey={projectKey} hotspotKey={hotspotKey} /> + ); + 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(<HotspotOpenInIdeOverlay ides={[]} onIdeSelected={onIdeSelected} />).type() + ).toBeNull(); + expect( + shallow( + <HotspotOpenInIdeOverlay + ides={[{ port: 0, ideName: 'Polop', description: 'Plouf' }]} + onIdeSelected={onIdeSelected} + /> + ).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( + <HotspotOpenInIdeOverlay ides={[ide1, ide2]} onIdeSelected={onIdeSelected} /> + ); + 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`] = ` -<Button - onClick={[Function]} +exports[`HotspotOpenInIdeButton should handle several IDE 1`] = ` +<Toggler + onRequestClose={[Function]} + open={false} + overlay={ + <DropdownOverlay> + <Unknown + ides={Array []} + onIdeSelected={[Function]} + /> + </DropdownOverlay> + } > - hotspots.open_in_ide.open - <DeferredSpinner - className="spacer-left" - loading={false} - /> -</Button> + <Button + onClick={[Function]} + > + hotspots.open_in_ide.open + <DeferredSpinner + className="spacer-left" + loading={false} + /> + </Button> +</Toggler> `; + +exports[`HotspotOpenInIdeButton should handle several IDE: dropdown open 1`] = ` +<Toggler + onRequestClose={[Function]} + open={true} + overlay={ + <DropdownOverlay> + <Unknown + ides={ + Array [ + Object { + "description": "Hello World", + "ideName": "BlueJ IDE", + "port": 42000, + }, + Object { + "description": "Blink", + "ideName": "Arduino IDE", + "port": 42001, + }, + ] + } + onIdeSelected={[Function]} + /> + </DropdownOverlay> + } +> + <Button + onClick={[Function]} + > + hotspots.open_in_ide.open + <DeferredSpinner + className="spacer-left" + loading={false} + /> + </Button> +</Toggler> +`; + +exports[`HotspotOpenInIdeButton should render correctly 1`] = ` +<Toggler + onRequestClose={[Function]} + open={false} + overlay={ + <DropdownOverlay> + <Unknown + ides={Array []} + onIdeSelected={[Function]} + /> + </DropdownOverlay> + } +> + <Button + onClick={[Function]} + > + hotspots.open_in_ide.open + <DeferredSpinner + className="spacer-left" + loading={false} + /> + </Button> +</Toggler> +`;
\ 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`] = ` +<ul + className="menu" +> + <li + key="0" + > + <a + href="#" + onClick={[Function]} + > + Polop + - + Plouf + </a> + </li> + <li + key="1" + > + <a + href="#" + onClick={[Function]} + > + Foo + - + Bar + </a> + </li> +</ul> +`; |