--- /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 globalMessages from '../../store/globalMessages';
+import getStore from './getStore';
+
+export default function addGlobalErrorMessage(message: string): void {
+ const store = getStore();
+ store.dispatch(globalMessages.addGlobalErrorMessage(message));
+}
--- /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 { Button } from 'sonar-ui-common/components/controls/buttons';
+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';
+
+interface Props {
+ projectKey: string;
+ hotspotKey: string;
+}
+
+interface State {
+ inDiscovery: boolean;
+}
+
+export default class HotspotOpenInIdeButton extends React.PureComponent<Props, State> {
+ state = {
+ inDiscovery: false
+ };
+
+ handleOnClick = () => {
+ 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 });
+ });
+ };
+
+ render() {
+ return (
+ <Button onClick={this.handleOnClick}>
+ {translate('hotspots.open_in_ide.open')}
+ <DeferredSpinner loading={this.state.inDiscovery} className="spacer-left" />
+ </Button>
+ );
+ }
+}
import { BranchLike } from '../../../types/branch-like';
import { Hotspot } from '../../../types/security-hotspots';
import Assignee from './assignee/Assignee';
+import HotspotOpenInIdeButton from './HotspotOpenInIdeButton';
import HotspotReviewHistoryAndComments from './HotspotReviewHistoryAndComments';
import HotspotSnippetContainer from './HotspotSnippetContainer';
import './HotspotViewer.css';
<strong className="big big-spacer-right">{hotspot.message}</strong>
<div className="display-flex-row flex-0">
{isLoggedIn(currentUser) && (
- <div className="dropdown spacer-right flex-1-0-auto">
- <Button onClick={props.onOpenComment}>
- {translate('hotspots.comment.open')}
- </Button>
- </div>
+ <>
+ <div className="dropdown spacer-right flex-1-0-auto">
+ <Button onClick={props.onOpenComment}>
+ {translate('hotspots.comment.open')}
+ </Button>
+ </div>
+ <div className="dropdown spacer-right flex-1-0-auto">
+ <HotspotOpenInIdeButton
+ hotspotKey={hotspot.key}
+ projectKey={hotspot.project.key}
+ />
+ </div>
+ </>
)}
<ClipboardButton className="flex-1-0-auto" copyValue={permalink}>
<LinkIcon className="spacer-right" />
--- /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 { Button } from 'sonar-ui-common/components/controls/buttons';
+import * as sonarlint from '../../../../helpers/sonarlint';
+import HotspotOpenInIdeButton from '../HotspotOpenInIdeButton';
+
+jest.mock('../../../../helpers/sonarlint');
+
+it('should render correctly', async () => {
+ const projectKey = 'my-project:key';
+ const hotspotKey = 'AXWsgE9RpggAQesHYfwm';
+
+ const wrapper = shallow(
+ <HotspotOpenInIdeButton projectKey={projectKey} hotspotKey={hotspotKey} />
+ );
+ expect(wrapper).toMatchSnapshot();
+
+ (sonarlint.probeSonarLintServers as jest.Mock).mockResolvedValue([
+ { port: 42001, ideName: 'BlueJ IDE', description: 'Hello World' }
+ ]);
+
+ wrapper.find(Button).simulate('click');
+
+ await new Promise(setImmediate);
+ expect(sonarlint.openHotspot).toBeCalledWith(42001, projectKey, hotspotKey);
+});
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<Button
+ onClick={[Function]}
+>
+ hotspots.open_in_ide.open
+ <DeferredSpinner
+ className="spacer-left"
+ loading={false}
+ />
+</Button>
+`;
hotspots.comment.open
</Button>
</div>
+ <div
+ className="dropdown spacer-right flex-1-0-auto"
+ >
+ <HotspotOpenInIdeButton
+ hotspotKey="01fc972e-2a3c-433e-bcae-0bd7f88f5123"
+ projectKey="my-project"
+ />
+ </div>
<ClipboardButton
className="flex-1-0-auto"
copyValue="http://localhost/security_hotspots?id=my-project&branch=branch-6.7&hotspots=01fc972e-2a3c-433e-bcae-0bd7f88f5123"
hotspots.comment.open
</Button>
</div>
+ <div
+ className="dropdown spacer-right flex-1-0-auto"
+ >
+ <HotspotOpenInIdeButton
+ hotspotKey="01fc972e-2a3c-433e-bcae-0bd7f88f5123"
+ projectKey="my-project"
+ />
+ </div>
<ClipboardButton
className="flex-1-0-auto"
copyValue="http://localhost/security_hotspots?id=my-project&branch=branch-6.7&hotspots=01fc972e-2a3c-433e-bcae-0bd7f88f5123"
hotspots.comment.open
</Button>
</div>
+ <div
+ className="dropdown spacer-right flex-1-0-auto"
+ >
+ <HotspotOpenInIdeButton
+ hotspotKey="01fc972e-2a3c-433e-bcae-0bd7f88f5123"
+ projectKey="my-project"
+ />
+ </div>
<ClipboardButton
className="flex-1-0-auto"
copyValue="http://localhost/security_hotspots?id=my-project&branch=branch-6.7&hotspots=01fc972e-2a3c-433e-bcae-0bd7f88f5123"
hotspots.comment.open
</Button>
</div>
+ <div
+ className="dropdown spacer-right flex-1-0-auto"
+ >
+ <HotspotOpenInIdeButton
+ hotspotKey="01fc972e-2a3c-433e-bcae-0bd7f88f5123"
+ projectKey="my-project"
+ />
+ </div>
<ClipboardButton
className="flex-1-0-auto"
copyValue="http://localhost/security_hotspots?id=my-project&branch=branch-6.7&hotspots=01fc972e-2a3c-433e-bcae-0bd7f88f5123"
hotspots.comment.open
</Button>
</div>
+ <div
+ className="dropdown spacer-right flex-1-0-auto"
+ >
+ <HotspotOpenInIdeButton
+ hotspotKey="01fc972e-2a3c-433e-bcae-0bd7f88f5123"
+ projectKey="my-project"
+ />
+ </div>
<ClipboardButton
className="flex-1-0-auto"
copyValue="http://localhost/security_hotspots?id=my-project&branch=branch-6.7&hotspots=01fc972e-2a3c-433e-bcae-0bd7f88f5123"
--- /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 { buildPortRange, openHotspot, probeSonarLintServers } from '../sonarlint';
+
+describe('buildPortRange', () => {
+ it('should build a port range of size <size> starting at port <port>', () => {
+ expect(buildPortRange(10000, 5)).toStrictEqual([10000, 10001, 10002, 10003, 10004]);
+ });
+});
+
+describe('probeSonarLintServers', () => {
+ const sonarLintResponse = { ideName: 'BlueJ IDE', description: 'Hello World' };
+
+ window.fetch = jest.fn((input: RequestInfo) => {
+ const calledPort = new URL(input.toString()).port;
+ if (calledPort === '64120') {
+ const resp = new Response();
+ resp.json = () => Promise.resolve(sonarLintResponse);
+ return Promise.resolve(resp);
+ } else {
+ return Promise.reject('oops');
+ }
+ });
+
+ it('should probe all ports in range', async () => {
+ const results = await probeSonarLintServers();
+ expect(results).toStrictEqual([{ port: 64120, ...sonarLintResponse }]);
+ });
+});
+
+describe('openHotspot', () => {
+ it('should send request to IDE on the right port', async () => {
+ const resp = new Response();
+ window.fetch = jest.fn((input: RequestInfo) => {
+ const calledUrl = new URL(input.toString());
+ try {
+ expect(calledUrl.searchParams.get('server')).toStrictEqual('http://localhost');
+ expect(calledUrl.searchParams.get('project')).toStrictEqual('my-project:key');
+ expect(calledUrl.searchParams.get('hotspot')).toStrictEqual('my-hotspot-key');
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ return Promise.resolve(resp);
+ });
+
+ const result = await openHotspot(42000, 'my-project:key', 'my-hotspot-key');
+ expect(result).toBe(resp);
+ });
+});
--- /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 { getHostUrl } from 'sonar-ui-common/helpers/urls';
+import { Ide } from '../types/sonarlint';
+
+const SONARLINT_PORT_START = 64120;
+const SONARLINT_PORT_RANGE = 11;
+
+export async function probeSonarLintServers(): Promise<Array<Ide>> {
+ const probedPorts = buildPortRange();
+ const probeRequests = probedPorts.map(p =>
+ fetch(buildSonarLintEndpoint(p, '/status'))
+ .then(r => r.json())
+ .then(json => {
+ const { ideName, description } = json;
+ return { port: p, ideName, description } as Ide;
+ })
+ .catch(() => undefined)
+ );
+ const results = await Promise.all(probeRequests);
+ return results.filter(r => r !== undefined) as Ide[];
+}
+
+export function openHotspot(calledPort: number, projectKey: string, hotspotKey: string) {
+ const showUrl = new URL(buildSonarLintEndpoint(calledPort, '/hotspots/show'));
+ showUrl.searchParams.set('server', getHostUrl());
+ showUrl.searchParams.set('project', projectKey);
+ showUrl.searchParams.set('hotspot', hotspotKey);
+ return fetch(showUrl.toString());
+}
+
+/**
+ * @returns [ start , ... , start + size - 1 ]
+ */
+export function buildPortRange(start = SONARLINT_PORT_START, size = SONARLINT_PORT_RANGE) {
+ return Array.from(Array(size).keys()).map(p => start + p);
+}
+
+function buildSonarLintEndpoint(port: number, path: string) {
+ return `http://localhost:${port}/sonarlint/api${path}`;
+}
--- /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.
+ */
+
+export interface Ide {
+ port: number;
+ ideName: string;
+ description: string;
+}
hotspots.comment.field=Comment:
hotspots.comment.open=Add Comment
hotspots.comment.submit=Comment
+hotspots.open_in_ide.open=Open in IDE
+hotspots.open_in_ide.success=Success. Switch to your IDE to see the security hotspot.
+hotspots.open_in_ide.failure=Unable to connect to your IDE to open the Security Hotspot. Please make sure you're running the latest version of SonarLint.
hotspots.assignee.select_user=Select a user...
hotspots.status.cannot_change_status=Changing a hotspot's status requires permission.
hotspot.filters.assignee.all=All
hotspot.filters.status.to_review=To review
hotspot.filters.status.fixed=Reviewed as fixed
-hotspot.filters.period.since_leak_period=New code
-hotspot.filters.period.overall=Overall code
+hotspot.filters.period.since_leak_period=New code
+hotspot.filters.period.overall=Overall code
hotspot.filters.status.safe=Reviewed as safe
hotspot.filters.show_all=Show all hotspots
hotspot.section.activity=Activity:
keyboard_shortcuts.shortcut=Shortcut
keyboard_shortcuts.action=Action
keyboard_shortcuts.global.title=Global
-keyboard_shortcuts.global.search=Open the search bar
+keyboard_shortcuts.global.search=Open the search bar
keyboard_shortcuts.global.open_shortcuts=Open this panel
keyboard_shortcuts.code_page.title=Code Page
keyboard_shortcuts.code_page.select_files=Select files
overview.measures.empty_explanation=Measures on New Code will appear after the second analysis of this branch.
overview.measures.empty_link={learn_more_link} about the Clean as You Code approach.
overview.measures.same_reference.explanation=This branch is configured to use itself as reference branch. It will never have New Code.
-overview.measures.bad_reference.explanation=This branch could not be compared to its reference branch. See the SCM or analysis report for more details.
+overview.measures.bad_reference.explanation=This branch could not be compared to its reference branch. See the SCM or analysis report for more details.
overview.measures.bad_setting.link=This can be fixed in the {setting_link} setting.
overview.measures.security_hotspots_reviewed=Reviewed
# New periods (MMF-1579)
overview.period.number_of_days=From last {0} days
overview.period.specific_analysis=Since {0}
-overview.period.reference_branch=Compared to {0}
+overview.period.reference_branch=Compared to {0}
overview.gate.ERROR=Failed
overview.gate.WARN=Warning
organization.url=Url
organization.url.description=Url of the homepage of the organization.
organization.binding_with_x_easy_sync=Binding an organization from SonarCloud with {0} is an easy way to keep them synchronized.
-organization.app_will_be_installed_on_x=To bind this organization to {0}, the SonarCloud application will be installed.
+organization.app_will_be_installed_on_x=To bind this organization to {0}, the SonarCloud application will be installed.
organization.members.page=Members
organization.members.page.description=Add users to the organization and grant them permissions to work on the projects. See {link} documentation.
organization.members.add=Add a member
# INDEXATION
#
#------------------------------------------------------------------------------
-indexation.in_progress=SonarQube is reloading project data. Some projects will be unavailable until this process is complete.
+indexation.in_progress=SonarQube is reloading project data. Some projects will be unavailable until this process is complete.
indexation.progression={0}% complete.
indexation.progression_with_error={0}% complete with some {link}.
indexation.progression_with_error.link=tasks failing