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;
}
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>
);
}
}
--- /dev/null
+/*
+ * 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;
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');
+ });
});
--- /dev/null
+/*
+ * 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);
+});
// 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
--- /dev/null
+// 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>
+`;