@@ -34,7 +34,6 @@ import accountRoutes from '../../apps/account/routes'; | |||
import backgroundTasksRoutes from '../../apps/background-tasks/routes'; | |||
import codeRoutes from '../../apps/code/routes'; | |||
import codingRulesRoutes from '../../apps/coding-rules/routes'; | |||
import componentRoutes from '../../apps/component/routes'; | |||
import componentMeasuresRoutes from '../../apps/component-measures/routes'; | |||
import customMeasuresRoutes from '../../apps/custom-measures/routes'; | |||
import groupsRoutes from '../../apps/groups/routes'; | |||
@@ -172,7 +171,6 @@ export default function startReactApp( | |||
{!isSonarCloud() && ( | |||
<RouteWithChildRoutes path="coding_rules" childRoutes={codingRulesRoutes} /> | |||
)} | |||
<RouteWithChildRoutes path="component" childRoutes={componentRoutes} /> | |||
<RouteWithChildRoutes path="documentation" childRoutes={documentationRoutes} /> | |||
<Route path="explore" component={Explore}> | |||
<Route path="issues" component={ExploreIssues} /> |
@@ -21,13 +21,14 @@ import * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import { connect } from 'react-redux'; | |||
import Helmet from 'react-helmet'; | |||
import { Location } from 'history'; | |||
import Components from './Components'; | |||
import Breadcrumbs from './Breadcrumbs'; | |||
import Search from './Search'; | |||
import SourceViewerWrapper from './SourceViewerWrapper'; | |||
import { addComponent, addComponentBreadcrumbs, clearBucket } from '../bucket'; | |||
import { retrieveComponentChildren, retrieveComponent, loadMoreChildren } from '../utils'; | |||
import ListFooter from '../../../components/controls/ListFooter'; | |||
import SourceViewer from '../../../components/SourceViewer/SourceViewer'; | |||
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; | |||
import { fetchMetrics } from '../../../store/rootActions'; | |||
import { getMetrics } from '../../../store/rootReducer'; | |||
@@ -46,7 +47,7 @@ interface DispatchToProps { | |||
interface OwnProps { | |||
branchLike?: T.BranchLike; | |||
component: T.Component; | |||
location: { query: { [x: string]: string } }; | |||
location: Pick<Location, 'query'>; | |||
} | |||
type Props = StateToProps & DispatchToProps & OwnProps; | |||
@@ -175,7 +176,7 @@ export class App extends React.PureComponent<Props, State> { | |||
}; | |||
render() { | |||
const { branchLike, component } = this.props; | |||
const { branchLike, component, location } = this.props; | |||
const { loading, baseComponent, components, breadcrumbs, total, sourceViewer } = this.state; | |||
const shouldShowBreadcrumbs = breadcrumbs.length > 1; | |||
@@ -224,7 +225,11 @@ export class App extends React.PureComponent<Props, State> { | |||
{sourceViewer !== undefined && ( | |||
<div className="spacer-top"> | |||
<SourceViewer branchLike={branchLike} component={sourceViewer.key} /> | |||
<SourceViewerWrapper | |||
branchLike={branchLike} | |||
component={sourceViewer.key} | |||
location={location} | |||
/> | |||
</div> | |||
)} | |||
</div> |
@@ -0,0 +1,54 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 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 { Location } from 'history'; | |||
import SourceViewer from '../../../components/SourceViewer/SourceViewer'; | |||
import { scrollToElement } from '../../../helpers/scrolling'; | |||
interface Props { | |||
branchLike?: T.BranchLike; | |||
component: string; | |||
location: Pick<Location, 'query'>; | |||
} | |||
export default function SourceViewerWrapper({ branchLike, component, location }: Props) { | |||
const { line } = location.query; | |||
const scrollToLine = () => { | |||
if (line) { | |||
const row = document.querySelector(`.source-line[data-line-number="${line}"]`); | |||
if (row) { | |||
scrollToElement(row, { smooth: false, bottomOffset: window.innerHeight / 2 - 60 }); | |||
} | |||
} | |||
}; | |||
const finalLine = line ? Number(line) : undefined; | |||
return ( | |||
<SourceViewer | |||
aroundLine={finalLine} | |||
branchLike={branchLike} | |||
component={component} | |||
highlightedLine={finalLine} | |||
onLoaded={scrollToLine} | |||
/> | |||
); | |||
} |
@@ -1,71 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 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 SourceViewer from '../../../components/SourceViewer/SourceViewer'; | |||
import { fillBranchLike } from '../../../helpers/branches'; | |||
interface Props { | |||
location: { | |||
query: { | |||
branch?: string; | |||
id: string; | |||
line?: string; | |||
pullRequest?: string; | |||
}; | |||
}; | |||
} | |||
export default class App extends React.PureComponent<Props> { | |||
scrollToLine = () => { | |||
const { line } = this.props.location.query; | |||
if (line) { | |||
const row = document.querySelector(`.source-line[data-line-number="${line}"]`); | |||
if (row) { | |||
const rect = row.getBoundingClientRect(); | |||
const topOffset = window.innerHeight / 2 - 60; | |||
const goal = rect.top - topOffset; | |||
window.scrollTo(0, goal); | |||
} | |||
} | |||
}; | |||
render() { | |||
const { branch, id, line, pullRequest } = this.props.location.query; | |||
const finalLine = line ? Number(line) : undefined; | |||
// TODO find a way to avoid creating this fakeBranchLike | |||
// probably the best way would be to drop this page completely | |||
// and redirect to the Code page | |||
const fakeBranchLike = fillBranchLike(branch, pullRequest); | |||
return ( | |||
<div className="page page-limited"> | |||
<SourceViewer | |||
aroundLine={finalLine} | |||
branchLike={fakeBranchLike} | |||
component={id} | |||
highlightedLine={finalLine} | |||
onLoaded={this.scrollToLine} | |||
/> | |||
</div> | |||
); | |||
} | |||
} |
@@ -1,22 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders 1`] = ` | |||
<div | |||
className="page page-limited" | |||
> | |||
<LazyLoader | |||
aroundLine={7} | |||
branchLike={ | |||
Object { | |||
"isMain": false, | |||
"mergeBranch": "", | |||
"name": "b", | |||
"type": "SHORT", | |||
} | |||
} | |||
component="foo" | |||
highlightedLine={7} | |||
onLoaded={[Function]} | |||
/> | |||
</div> | |||
`; |
@@ -1,28 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 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 { lazyLoad } from '../../components/lazyLoad'; | |||
const routes = [ | |||
{ | |||
indexRoute: { component: lazyLoad(() => import('./components/App')) } | |||
} | |||
]; | |||
export default routes; |
@@ -41,6 +41,7 @@ exports[`renders 1`] = ` | |||
"pathname": "/code", | |||
"query": Object { | |||
"id": "proj", | |||
"line": undefined, | |||
"selected": "comp", | |||
}, | |||
} | |||
@@ -122,6 +123,7 @@ exports[`renders with branch 1`] = ` | |||
"query": Object { | |||
"branch": "feature", | |||
"id": "proj", | |||
"line": undefined, | |||
"selected": "comp", | |||
}, | |||
} | |||
@@ -200,6 +202,7 @@ exports[`renders with sub-project 1`] = ` | |||
"pathname": "/code", | |||
"query": Object { | |||
"id": "proj", | |||
"line": undefined, | |||
"selected": "comp", | |||
}, | |||
} |
@@ -22,6 +22,7 @@ import * as classNames from 'classnames'; | |||
import { intersection, uniqBy } from 'lodash'; | |||
import SourceViewerHeader from './SourceViewerHeader'; | |||
import SourceViewerCode from './SourceViewerCode'; | |||
import { SourceViewerContext } from './SourceViewerContext'; | |||
import DuplicationPopup from './components/DuplicationPopup'; | |||
import defaultLoadIssues from './helpers/loadIssues'; | |||
import getCoverageStatus from './helpers/getCoverageStatus'; | |||
@@ -703,23 +704,25 @@ export default class SourceViewerBase extends React.PureComponent<Props, State> | |||
}); | |||
return ( | |||
<div className={className} ref={node => (this.node = node)}> | |||
<WorkspaceContext.Consumer> | |||
{({ openComponent }) => ( | |||
<SourceViewerHeader | |||
branchLike={this.props.branchLike} | |||
openComponent={openComponent} | |||
sourceViewerFile={component} | |||
/> | |||
<SourceViewerContext.Provider value={{ branchLike: this.props.branchLike, file: component }}> | |||
<div className={className} ref={node => (this.node = node)}> | |||
<WorkspaceContext.Consumer> | |||
{({ openComponent }) => ( | |||
<SourceViewerHeader | |||
branchLike={this.props.branchLike} | |||
openComponent={openComponent} | |||
sourceViewerFile={component} | |||
/> | |||
)} | |||
</WorkspaceContext.Consumer> | |||
{sourceRemoved && ( | |||
<Alert className="spacer-top" variant="warning"> | |||
{translate('code_viewer.no_source_code_displayed_due_to_source_removed')} | |||
</Alert> | |||
)} | |||
</WorkspaceContext.Consumer> | |||
{sourceRemoved && ( | |||
<Alert className="spacer-top" variant="warning"> | |||
{translate('code_viewer.no_source_code_displayed_due_to_source_removed')} | |||
</Alert> | |||
)} | |||
{!sourceRemoved && sources !== undefined && this.renderCode(sources)} | |||
</div> | |||
{!sourceRemoved && sources !== undefined && this.renderCode(sources)} | |||
</div> | |||
</SourceViewerContext.Provider> | |||
); | |||
} | |||
} |
@@ -156,7 +156,6 @@ export default class SourceViewerCode extends React.PureComponent<Props> { | |||
return ( | |||
<Line | |||
branchLike={this.props.branchLike} | |||
componentKey={this.props.componentKey} | |||
displayAllIssues={this.props.displayAllIssues} | |||
displayCoverage={displayCoverage} | |||
displayDuplications={displayDuplications} |
@@ -18,11 +18,13 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import App from '../App'; | |||
it('renders', () => { | |||
expect( | |||
shallow(<App location={{ query: { branch: 'b', id: 'foo', line: '7' } }} />) | |||
).toMatchSnapshot(); | |||
}); | |||
interface SourceViewerContextShape { | |||
branchLike?: T.BranchLike; | |||
file: T.SourceViewerFile; | |||
} | |||
export const SourceViewerContext = React.createContext({ | |||
branchLike: {}, | |||
file: {} | |||
}) as React.Context<SourceViewerContextShape>; |
@@ -32,7 +32,8 @@ import { | |||
getPathUrlAsString, | |||
getBranchLikeUrl, | |||
getComponentIssuesUrl, | |||
getBaseUrl | |||
getBaseUrl, | |||
getCodeUrl | |||
} from '../../helpers/urls'; | |||
import { collapsedDirFromPath, fileFromPath } from '../../helpers/path'; | |||
import { translate } from '../../helpers/l10n'; | |||
@@ -136,15 +137,13 @@ export default class SourceViewerHeader extends React.PureComponent<Props, State | |||
</a> | |||
</li> | |||
<li> | |||
<a | |||
<Link | |||
className="js-new-window" | |||
href={getPathUrlAsString({ | |||
pathname: '/component', | |||
query: { id: key, ...getBranchLikeQuery(this.props.branchLike) } | |||
})} | |||
target="_blank"> | |||
rel="noopener noreferrer" | |||
target="_blank" | |||
to={getCodeUrl(this.props.sourceViewerFile.project, this.props.branchLike, key)}> | |||
{translate('component_viewer.new_window')} | |||
</a> | |||
</Link> | |||
</li> | |||
{!workspace && ( | |||
<li> | |||
@@ -154,7 +153,11 @@ export default class SourceViewerHeader extends React.PureComponent<Props, State | |||
</li> | |||
)} | |||
<li> | |||
<a className="js-raw-source" href={rawSourcesLink} target="_blank"> | |||
<a | |||
className="js-raw-source" | |||
href={rawSourcesLink} | |||
rel="noopener noreferrer" | |||
target="_blank"> | |||
{translate('component_viewer.show_raw_source')} | |||
</a> | |||
</li> |
@@ -30,7 +30,6 @@ import LineCode from './LineCode'; | |||
interface Props { | |||
branchLike: T.BranchLike | undefined; | |||
componentKey: string; | |||
displayAllIssues?: boolean; | |||
displayCoverage: boolean; | |||
displayDuplications: boolean; | |||
@@ -112,8 +111,6 @@ export default class Line extends React.PureComponent<Props> { | |||
return ( | |||
<tr className={className} data-line-number={line.line}> | |||
<LineNumber | |||
branchLike={this.props.branchLike} | |||
componentKey={this.props.componentKey} | |||
line={line} | |||
onPopupToggle={this.props.onLinePopupToggle} | |||
popupOpen={this.isPopupOpen('line-number')} |
@@ -22,8 +22,6 @@ import LineOptionsPopup from './LineOptionsPopup'; | |||
import Toggler from '../../controls/Toggler'; | |||
interface Props { | |||
branchLike: T.BranchLike | undefined; | |||
componentKey: string; | |||
line: T.SourceLine; | |||
onPopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void; | |||
popupOpen: boolean; | |||
@@ -46,7 +44,7 @@ export default class LineNumber extends React.PureComponent<Props> { | |||
}; | |||
render() { | |||
const { branchLike, componentKey, line, popupOpen } = this.props; | |||
const { line, popupOpen } = this.props; | |||
const { line: lineNumber } = line; | |||
const hasLineNumber = !!lineNumber; | |||
return hasLineNumber ? ( | |||
@@ -60,9 +58,7 @@ export default class LineNumber extends React.PureComponent<Props> { | |||
<Toggler | |||
onRequestClose={this.closePopup} | |||
open={popupOpen} | |||
overlay={ | |||
<LineOptionsPopup branchLike={branchLike} componentKey={componentKey} line={line} /> | |||
} | |||
overlay={<LineOptionsPopup line={line} />} | |||
/> | |||
</td> | |||
) : ( |
@@ -22,26 +22,32 @@ import { Link } from 'react-router'; | |||
import { DropdownOverlay } from '../../controls/Dropdown'; | |||
import { PopupPlacement } from '../../ui/popups'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getBranchLikeQuery } from '../../../helpers/branches'; | |||
import { getCodeUrl } from '../../../helpers/urls'; | |||
import { SourceViewerContext } from '../SourceViewerContext'; | |||
interface Props { | |||
branchLike: T.BranchLike | undefined; | |||
componentKey: string; | |||
line: T.SourceLine; | |||
} | |||
export default function LineOptionsPopup({ branchLike, componentKey, line }: Props) { | |||
const permalink = { | |||
pathname: '/component', | |||
query: { id: componentKey, line: line.line, ...getBranchLikeQuery(branchLike) } | |||
}; | |||
export default function LineOptionsPopup({ line }: Props) { | |||
return ( | |||
<DropdownOverlay placement={PopupPlacement.RightTop}> | |||
<div className="source-viewer-bubble-popup nowrap"> | |||
<Link className="js-get-permalink" to={permalink}> | |||
{translate('component_viewer.get_permalink')} | |||
</Link> | |||
</div> | |||
</DropdownOverlay> | |||
<SourceViewerContext.Consumer> | |||
{({ branchLike, file }) => ( | |||
<DropdownOverlay placement={PopupPlacement.RightTop}> | |||
<div className="source-viewer-bubble-popup nowrap"> | |||
<Link | |||
className="js-get-permalink" | |||
onClick={event => { | |||
event.stopPropagation(); | |||
}} | |||
rel="noopener noreferrer" | |||
target="_blank" | |||
to={getCodeUrl(file.project, branchLike, file.key, line.line)}> | |||
{translate('component_viewer.get_permalink')} | |||
</Link> | |||
</div> | |||
</DropdownOverlay> | |||
)} | |||
</SourceViewerContext.Consumer> | |||
); | |||
} |
@@ -24,29 +24,13 @@ import LineNumber from '../LineNumber'; | |||
it('render line 3', () => { | |||
const line = { line: 3 }; | |||
const wrapper = shallow( | |||
<LineNumber | |||
branchLike={undefined} | |||
componentKey="foo" | |||
line={line} | |||
onPopupToggle={jest.fn()} | |||
popupOpen={false} | |||
/> | |||
); | |||
const wrapper = shallow(<LineNumber line={line} onPopupToggle={jest.fn()} popupOpen={false} />); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper); | |||
}); | |||
it('render line 0', () => { | |||
const line = { line: 0 }; | |||
const wrapper = shallow( | |||
<LineNumber | |||
branchLike={undefined} | |||
componentKey="foo" | |||
line={line} | |||
onPopupToggle={jest.fn()} | |||
popupOpen={false} | |||
/> | |||
); | |||
const wrapper = shallow(<LineNumber line={line} onPopupToggle={jest.fn()} popupOpen={false} />); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -21,14 +21,18 @@ import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import LineOptionsPopup from '../LineOptionsPopup'; | |||
jest.mock('../../SourceViewerContext', () => ({ | |||
SourceViewerContext: { | |||
Consumer: (props: any) => | |||
props.children({ | |||
branchLike: { isMain: false, name: 'feature', type: 'SHORT' }, | |||
file: { project: 'prj', key: 'foo' } | |||
}) | |||
} | |||
})); | |||
it('should render', () => { | |||
const line = { line: 3 }; | |||
const branch: T.ShortLivingBranch = { | |||
isMain: false, | |||
mergeBranch: 'master', | |||
name: 'feature', | |||
type: 'SHORT' | |||
}; | |||
const wrapper = shallow(<LineOptionsPopup branchLike={branch} componentKey="foo" line={line} />); | |||
const wrapper = shallow(<LineOptionsPopup line={line} />).dive(); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -19,7 +19,6 @@ exports[`render line 3 1`] = ` | |||
open={false} | |||
overlay={ | |||
<LineOptionsPopup | |||
componentKey="foo" | |||
line={ | |||
Object { | |||
"line": 3, |
@@ -9,15 +9,19 @@ exports[`should render 1`] = ` | |||
> | |||
<Link | |||
className="js-get-permalink" | |||
onClick={[Function]} | |||
onlyActiveOnIndex={false} | |||
rel="noopener noreferrer" | |||
style={Object {}} | |||
target="_blank" | |||
to={ | |||
Object { | |||
"pathname": "/component", | |||
"pathname": "/code", | |||
"query": Object { | |||
"branch": "feature", | |||
"id": "foo", | |||
"id": "prj", | |||
"line": 3, | |||
"selected": "foo", | |||
}, | |||
} | |||
} |
@@ -219,8 +219,16 @@ export function getMarkdownHelpUrl(): string { | |||
return getBaseUrl() + '/markdown/help'; | |||
} | |||
export function getCodeUrl(project: string, branchLike?: T.BranchLike, selected?: string) { | |||
return { pathname: '/code', query: { id: project, ...getBranchLikeQuery(branchLike), selected } }; | |||
export function getCodeUrl( | |||
project: string, | |||
branchLike?: T.BranchLike, | |||
selected?: string, | |||
line?: number | |||
) { | |||
return { | |||
pathname: '/code', | |||
query: { id: project, ...getBranchLikeQuery(branchLike), selected, line } | |||
}; | |||
} | |||
export function getOrganizationUrl(organization: string) { |