Browse Source

SONAR-14110 Add "Open in IDE" button to Security Hotspots page

tags/8.6.0.39681
Jean-Baptiste Lievremont 3 years ago
parent
commit
d570089a93

+ 26
- 0
server/sonar-web/src/main/js/app/utils/addGlobalErrorMessage.ts View File

@@ -0,0 +1,26 @@
/*
* 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));
}

+ 74
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeButton.tsx View File

@@ -0,0 +1,74 @@
/*
* 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>
);
}
}

+ 14
- 5
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx View File

@@ -32,6 +32,7 @@ import { isLoggedIn } from '../../../helpers/users';
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';
@@ -80,11 +81,19 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
<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" />

+ 45
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotOpenInIdeButton-test.tsx View File

@@ -0,0 +1,45 @@
/*
* 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);
});

+ 13
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotOpenInIdeButton-test.tsx.snap View File

@@ -0,0 +1,13 @@
// 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>
`;

+ 40
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap View File

@@ -27,6 +27,14 @@ exports[`should render correctly 1`] = `
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"
@@ -674,6 +682,14 @@ exports[`should render correctly: anonymous user 1`] = `
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"
@@ -1321,6 +1337,14 @@ exports[`should render correctly: assignee without name 1`] = `
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"
@@ -1968,6 +1992,14 @@ exports[`should render correctly: deleted assignee 1`] = `
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"
@@ -2621,6 +2653,14 @@ exports[`should render correctly: unassigned 1`] = `
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"

+ 67
- 0
server/sonar-web/src/main/js/helpers/__tests__/sonarlint-test.ts View File

@@ -0,0 +1,67 @@
/*
* 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);
});
});

+ 59
- 0
server/sonar-web/src/main/js/helpers/sonarlint.ts View File

@@ -0,0 +1,59 @@
/*
* 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}`;
}

+ 25
- 0
server/sonar-web/src/main/js/types/sonarlint.ts View File

@@ -0,0 +1,25 @@
/*
* 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;
}

+ 10
- 7
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -716,6 +716,9 @@ hotspots.review_history.comment_added=added a comment
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.
@@ -736,8 +739,8 @@ hotspot.filters.assignee.assigned_to_me=Assigned to me
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:
@@ -2529,7 +2532,7 @@ keyboard_shortcuts.title=Keyboard Shortcuts
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
@@ -2881,7 +2884,7 @@ overview.measures=Measures
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

@@ -2913,7 +2916,7 @@ overview.period.manual_baseline=Since {0}
# 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
@@ -3129,7 +3132,7 @@ organization.updated=Organization details have been updated.
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
@@ -3781,7 +3784,7 @@ maintenance.sonarqube_is_offline.text=The connection to SonarQube is lost. Pleas
# 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

Loading…
Cancel
Save