@@ -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)); | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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" /> |
@@ -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); | |||
}); |
@@ -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> | |||
`; |
@@ -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" |
@@ -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); | |||
}); | |||
}); |
@@ -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}`; | |||
} |
@@ -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; | |||
} |
@@ -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 |