Pārlūkot izejas kodu

SONAR-10489 Support cross file issue locations in web app

tags/7.5
Stas Vilchik pirms 6 gadiem
vecāks
revīzija
16ae386379
21 mainītis faili ar 976 papildinājumiem un 139 dzēšanām
  1. 2
    0
      server/sonar-web/src/main/js/app/types.ts
  2. 13
    5
      server/sonar-web/src/main/js/apps/issues/components/App.tsx
  3. 25
    15
      server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx
  4. 32
    26
      server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.tsx
  5. 3
    1
      server/sonar-web/src/main/js/apps/issues/components/__tests__/ComponentBreadcrumbs-test.tsx
  6. 1
    1
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocations.tsx
  7. 37
    22
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationsNavigator.tsx
  8. 1
    1
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationsNavigatorLocation.tsx
  9. 190
    0
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/CrossFileLocationsNavigator.tsx
  10. 7
    32
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueLocations-test.tsx
  11. 138
    0
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueLocationsNavigator-test.tsx
  12. 108
    0
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/CrossFileLocationsNavigator-test.tsx
  13. 136
    0
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocationsNavigator-test.tsx.snap
  14. 76
    0
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/CrossFileLocationsNavigator-test.tsx.snap
  15. 85
    2
      server/sonar-web/src/main/js/apps/issues/styles.css
  16. 29
    0
      server/sonar-web/src/main/js/apps/issues/utils.ts
  17. 11
    1
      server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx
  18. 8
    4
      server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx
  19. 54
    9
      server/sonar-web/src/main/js/helpers/__tests__/issues-test.ts
  20. 19
    20
      server/sonar-web/src/main/js/helpers/issues.ts
  21. 1
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 2
- 0
server/sonar-web/src/main/js/app/types.ts Parādīt failu

@@ -150,6 +150,8 @@ export interface FacetValue {
}

export interface FlowLocation {
component: string;
componentName?: string;
msg: string;
textRange: TextRange;
}

+ 13
- 5
server/sonar-web/src/main/js/apps/issues/components/App.tsx Parādīt failu

@@ -347,7 +347,14 @@ export default class App extends React.PureComponent<Props, State> {
};
if (this.state.openIssue) {
if (path.query.open && path.query.open === this.state.openIssue.key) {
this.scrollToSelectedIssue();
this.setState(
{
locationsNavigator: false,
selectedFlowIndex: undefined,
selectedLocationIndex: undefined
},
this.scrollToSelectedIssue
);
} else {
this.context.router.replace(path);
}
@@ -384,7 +391,7 @@ export default class App extends React.PureComponent<Props, State> {
if (selected) {
const element = document.querySelector(`[data-issue="${selected}"]`);
if (element) {
scrollToElement(element, { topOffset: 150, bottomOffset: 100, smooth });
scrollToElement(element, { topOffset: 250, bottomOffset: 100, smooth });
}
}
};
@@ -993,6 +1000,8 @@ export default class App extends React.PureComponent<Props, State> {
component={component}
issue={openIssue}
organization={this.props.organization}
selectedFlowIndex={this.state.selectedFlowIndex}
selectedLocationIndex={this.state.selectedLocationIndex}
/>
</div>
) : (
@@ -1020,14 +1029,13 @@ export default class App extends React.PureComponent<Props, State> {
<IssuesSourceViewer
branchLike={this.props.branchLike}
loadIssues={this.fetchIssuesForComponent}
locationsNavigator={this.state.locationsNavigator}
onIssueChange={this.handleIssueChange}
onIssueSelect={this.openIssue}
onLocationSelect={this.selectLocation}
openIssue={openIssue}
selectedFlowIndex={this.state.selectedFlowIndex}
selectedLocationIndex={
this.state.locationsNavigator ? this.state.selectedLocationIndex : undefined
}
selectedLocationIndex={this.state.selectedLocationIndex}
/>
) : (
this.renderList()

+ 25
- 15
server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx Parādīt failu

@@ -19,7 +19,8 @@
*/
import * as React from 'react';
import { Link } from 'react-router';
import { BranchLike, Component } from '../../../app/types';
import { getSelectedLocation } from '../utils';
import { BranchLike, Component, Issue } from '../../../app/types';
import Organization from '../../../components/shared/Organization';
import { collapsePath, limitComponentName } from '../../../helpers/path';
import { getBranchLikeUrl, getCodeUrl } from '../../../helpers/urls';
@@ -27,29 +28,40 @@ import { getBranchLikeUrl, getCodeUrl } from '../../../helpers/urls';
interface Props {
branchLike?: BranchLike;
component?: Component;
issue: {
component: string;
componentLongName: string;
organization: string;
project: string;
projectName: string;
subProject?: string;
subProjectName?: string;
};
issue: Pick<
Issue,
| 'component'
| 'componentLongName'
| 'flows'
| 'organization'
| 'project'
| 'projectName'
| 'secondaryLocations'
| 'subProject'
| 'subProjectName'
>;
organization: { key: string } | undefined;
selectedFlowIndex?: number;
selectedLocationIndex?: number;
}

export default function ComponentBreadcrumbs({
branchLike,
component,
issue,
organization
organization,
selectedFlowIndex,
selectedLocationIndex
}: Props) {
const displayOrganization =
!organization && (!component || ['VW', 'SVW'].includes(component.qualifier));
const displayProject = !component || !['TRK', 'BRC', 'DIR'].includes(component.qualifier);
const displaySubProject = !component || !['BRC', 'DIR'].includes(component.qualifier);

const selectedLocation = getSelectedLocation(issue, selectedFlowIndex, selectedLocationIndex);
const componentKey = selectedLocation ? selectedLocation.component : issue.component;
const componentName = selectedLocation ? selectedLocation.componentName : issue.componentLongName;

return (
<div className="component-name text-ellipsis">
{displayOrganization && (
@@ -76,10 +88,8 @@ export default function ComponentBreadcrumbs({
</span>
)}

<Link
className="link-no-underline"
to={getCodeUrl(issue.project, branchLike, issue.component)}>
<span title={issue.componentLongName}>{collapsePath(issue.componentLongName)}</span>
<Link className="link-no-underline" to={getCodeUrl(issue.project, branchLike, componentKey)}>
<span title={componentName}>{collapsePath(componentName || '')}</span>
</Link>
</div>
);

+ 32
- 26
server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.tsx Parādīt failu

@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { getLocations, getSelectedLocation } from '../utils';
import { BranchLike, Issue } from '../../../app/types';
import SourceViewer from '../../../components/SourceViewer/SourceViewer';
import { scrollToElement } from '../../../helpers/scrolling';
@@ -25,6 +26,7 @@ import { scrollToElement } from '../../../helpers/scrolling';
interface Props {
branchLike: BranchLike | undefined;
loadIssues: (component: string, from: number, to: number) => Promise<Issue[]>;
locationsNavigator: boolean;
onIssueChange: (issue: Issue) => void;
onIssueSelect: (issueKey: string) => void;
onLocationSelect: (index: number) => void;
@@ -71,53 +73,57 @@ export default class IssuesSourceViewer extends React.PureComponent<Props> {
render() {
const { openIssue, selectedFlowIndex, selectedLocationIndex } = this.props;

const locations =
selectedFlowIndex !== undefined
? openIssue.flows[selectedFlowIndex]
: openIssue.flows.length > 0 ? openIssue.flows[0] : openIssue.secondaryLocations;

let locationMessage = undefined;
let locationLine = undefined;
const locations = getLocations(openIssue, selectedFlowIndex);
const selectedLocation = getSelectedLocation(
openIssue,
selectedFlowIndex,
selectedLocationIndex
);

// We don't want to display a location message when selected location is -1
if (
locations !== undefined &&
selectedLocationIndex !== undefined &&
selectedLocationIndex >= 0 &&
locations.length >= selectedLocationIndex
) {
locationMessage = {
index: selectedLocationIndex,
text: locations[selectedLocationIndex].msg
};
locationLine = locations[selectedLocationIndex].textRange.startLine;
}
const component = selectedLocation ? selectedLocation.component : openIssue.component;

// if location is selected, show (and load) code around it
// otherwise show code around the open issue
const aroundLine = locationLine || (openIssue.textRange && openIssue.textRange.endLine);
const aroundLine = selectedLocation
? selectedLocation.textRange.startLine
: openIssue.textRange && openIssue.textRange.endLine;

// replace locations in another file with `undefined` to keep the same location indexes
const highlightedLocations = locations.map(
location => (location.component === component ? location : undefined)
);

const highlightedLocationMessage =
this.props.locationsNavigator && selectedLocationIndex !== undefined
? selectedLocation && { index: selectedLocationIndex, text: selectedLocation.msg }
: undefined;

const allMessagesEmpty = locations !== undefined && locations.every(location => !location.msg);

// do not load issues when open another file for a location
const loadIssues =
component === openIssue.component ? this.props.loadIssues : () => Promise.resolve([]);
const selectedIssue = component === openIssue.component ? openIssue.key : undefined;

return (
<div ref={node => (this.node = node)}>
<SourceViewer
aroundLine={aroundLine}
branchLike={this.props.branchLike}
component={openIssue.component}
component={component}
displayAllIssues={true}
displayIssueLocationsCount={false}
displayIssueLocationsLink={false}
displayLocationMarkers={!allMessagesEmpty}
highlightedLocationMessage={locationMessage}
highlightedLocations={locations}
loadIssues={this.props.loadIssues}
highlightedLocationMessage={highlightedLocationMessage}
highlightedLocations={highlightedLocations}
loadIssues={loadIssues}
onIssueChange={this.props.onIssueChange}
onIssueSelect={this.props.onIssueSelect}
onLoaded={this.handleLoaded}
onLocationSelect={this.props.onLocationSelect}
scroll={this.handleScroll}
selectedIssue={openIssue.key}
selectedIssue={selectedIssue}
/>
</div>
);

+ 3
- 1
server/sonar-web/src/main/js/apps/issues/components/__tests__/ComponentBreadcrumbs-test.tsx Parādīt failu

@@ -25,9 +25,11 @@ import { ShortLivingBranch, BranchType } from '../../../../app/types';
const baseIssue = {
component: 'comp',
componentLongName: 'comp-name',
flows: [],
organization: 'org',
project: 'proj',
projectName: 'proj-name'
projectName: 'proj-name',
secondaryLocations: []
};

it('renders', () => {

+ 1
- 1
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocations.tsx Parādīt failu

@@ -22,7 +22,7 @@ import ConciseIssueLocationBadge from './ConciseIssueLocationBadge';
import { Issue } from '../../../app/types';

interface Props {
issue: Issue;
issue: Pick<Issue, 'flows' | 'secondaryLocations'>;
onFlowSelect: (index: number) => void;
selectedFlowIndex: number | undefined;
}

+ 37
- 22
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationsNavigator.tsx Parādīt failu

@@ -18,11 +18,14 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { uniq } from 'lodash';
import ConciseIssueLocationsNavigatorLocation from './ConciseIssueLocationsNavigatorLocation';
import CrossFileLocationsNavigator from './CrossFileLocationsNavigator';
import { getLocations } from '../utils';
import { Issue } from '../../../app/types';

interface Props {
issue: Issue;
issue: Pick<Issue, 'component' | 'key' | 'flows' | 'secondaryLocations'>;
onLocationSelect: (index: number) => void;
scroll: (element: Element) => void;
selectedFlowIndex: number | undefined;
@@ -31,31 +34,43 @@ interface Props {

export default class ConciseIssueLocationsNavigator extends React.PureComponent<Props> {
render() {
const { selectedFlowIndex, selectedLocationIndex } = this.props;
const { flows, secondaryLocations } = this.props.issue;

const locations =
selectedFlowIndex !== undefined
? flows[selectedFlowIndex]
: flows.length > 0 ? flows[0] : secondaryLocations;
const locations = getLocations(this.props.issue, this.props.selectedFlowIndex);

if (!locations || locations.length === 0 || locations.every(location => !location.msg)) {
return null;
}

return (
<div className="spacer-top">
{locations.map((location, index) => (
<ConciseIssueLocationsNavigatorLocation
index={index}
key={index}
message={location.msg}
onClick={this.props.onLocationSelect}
scroll={this.props.scroll}
selected={index === selectedLocationIndex}
/>
))}
</div>
);
const locationComponents = [
this.props.issue.component,
...locations.map(location => location.component)
];
const isCrossFile = uniq(locationComponents).length > 1;

if (isCrossFile) {
return (
<CrossFileLocationsNavigator
issue={this.props.issue}
locations={locations}
onLocationSelect={this.props.onLocationSelect}
scroll={this.props.scroll}
selectedLocationIndex={this.props.selectedLocationIndex}
/>
);
} else {
return (
<div className="concise-issue-locations-navigator spacer-top">
{locations.map((location, index) => (
<ConciseIssueLocationsNavigatorLocation
index={index}
key={index}
message={location.msg}
onClick={this.props.onLocationSelect}
scroll={this.props.scroll}
selected={index === this.props.selectedLocationIndex}
/>
))}
</div>
);
}
}
}

+ 1
- 1
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueLocationsNavigatorLocation.tsx Parādīt failu

@@ -53,7 +53,7 @@ export default class ConciseIssueLocationsNavigatorLocation extends React.PureCo
return (
<div className="little-spacer-top" ref={node => (this.node = node)}>
<a
className="consice-issue-locations-navigator-location"
className="concise-issue-locations-navigator-location"
href="#"
onClick={this.handleClick}>
<LocationIndex selected={this.props.selected}>{this.props.index + 1}</LocationIndex>

+ 190
- 0
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/CrossFileLocationsNavigator.tsx Parādīt failu

@@ -0,0 +1,190 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 ConciseIssueLocationsNavigatorLocation from './ConciseIssueLocationsNavigatorLocation';
import { Issue, FlowLocation } from '../../../app/types';
import { translateWithParameters } from '../../../helpers/l10n';
import { collapsePath } from '../../../helpers/path';

interface Props {
issue: Pick<Issue, 'key'>;
locations: FlowLocation[];
onLocationSelect: (index: number) => void;
scroll: (element: Element) => void;
selectedLocationIndex: number | undefined;
}

interface State {
collapsed: boolean;
}

interface LocationGroup {
component: string | undefined;
componentName: string | undefined;
firstLocationIndex: number;
locations: FlowLocation[];
}

export default class CrossFileLocationsNavigator extends React.PureComponent<Props, State> {
state: State = { collapsed: true };

componentWillReceiveProps(nextProps: Props) {
if (nextProps.issue.key !== this.props.issue.key) {
this.setState({ collapsed: true });
}

// expand locations list as soon as a location in the middle of the list is selected
const { locations: nextLocations } = nextProps;
if (
nextProps.selectedLocationIndex &&
nextProps.selectedLocationIndex > 0 &&
nextLocations !== undefined &&
nextProps.selectedLocationIndex < nextLocations.length - 1
) {
this.setState({ collapsed: false });
}
}

handleMoreLocationsClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.setState({ collapsed: false });
};

groupByFile = (locations: FlowLocation[]) => {
const groups: LocationGroup[] = [];

let currentLocations: FlowLocation[] = [];
let currentComponent: string | undefined;
let currentComponentName: string | undefined;
let currentFirstLocationIndex = 0;

for (let index = 0; index < locations.length; index++) {
const location = locations[index];
if (location.component === currentComponent) {
currentLocations.push(location);
} else {
if (currentLocations.length > 0) {
groups.push({
component: currentComponent,
componentName: currentComponentName,
firstLocationIndex: currentFirstLocationIndex,
locations: currentLocations
});
}
currentLocations = [location];
currentComponent = location.component;
currentComponentName = location.componentName;
currentFirstLocationIndex = index;
}
}

if (currentLocations.length > 0) {
groups.push({
component: currentComponent,
componentName: currentComponentName,
firstLocationIndex: currentFirstLocationIndex,
locations: currentLocations
});
}

return groups;
};

renderLocation = (index: number, message: string) => {
return (
<ConciseIssueLocationsNavigatorLocation
index={index}
key={index}
message={message}
onClick={this.props.onLocationSelect}
scroll={this.props.scroll}
selected={index === this.props.selectedLocationIndex}
/>
);
};

renderGroup = (
group: LocationGroup,
groupIndex: number,
{ onlyFirst = false, onlyLast = false } = {}
) => {
const { firstLocationIndex } = group;
const lastLocationIndex = group.locations.length - 1;
return (
<div className="concise-issue-locations-navigator-file" key={groupIndex}>
<div className="concise-issue-location-file">
<i className="concise-issue-location-file-circle little-spacer-right" />
{collapsePath(group.componentName || '', 15)}
</div>
{group.locations.length > 0 && (
<div className="concise-issue-location-file-locations">
{onlyFirst && this.renderLocation(firstLocationIndex, group.locations[0].msg)}

{onlyLast &&
this.renderLocation(
firstLocationIndex + lastLocationIndex,
group.locations[lastLocationIndex].msg
)}

{!onlyFirst &&
!onlyLast &&
group.locations.map((location, index) =>
this.renderLocation(firstLocationIndex + index, location.msg)
)}
</div>
)}
</div>
);
};

render() {
const { locations } = this.props;
const groups = this.groupByFile(locations);

if (locations.length > 2 && groups.length > 1 && this.state.collapsed) {
const firstGroup = groups[0];
const lastGroup = groups[groups.length - 1];
return (
<div className="concise-issue-locations-navigator spacer-top">
{this.renderGroup(firstGroup, 0, { onlyFirst: true })}
<div className="concise-issue-locations-navigator-file">
<div className="concise-issue-location-file">
<i className="concise-issue-location-file-circle-multiple little-spacer-right" />
<a
className="concise-issue-location-file-more"
href="#"
onClick={this.handleMoreLocationsClick}>
{translateWithParameters('issues.x_more_locations', locations.length - 2)}
</a>
</div>
</div>
{this.renderGroup(lastGroup, groups.length - 1, { onlyLast: true })}
</div>
);
} else {
return (
<div className="concise-issue-locations-navigator spacer-top">
{groups.map((group, groupIndex) => this.renderGroup(group, groupIndex))}
</div>
);
}
}
}

+ 7
- 32
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueLocations-test.tsx Parādīt failu

@@ -23,32 +23,12 @@ import ConciseIssueLocations from '../ConciseIssueLocations';

const textRange = { startLine: 1, startOffset: 1, endLine: 1, endOffset: 1 };

const baseIssue = {
component: '',
componentLongName: '',
componentQualifier: '',
componentUuid: '',
creationDate: '',
key: '',
message: '',
organization: '',
project: '',
projectName: '',
projectOrganization: '',
projectUuid: '',
rule: '',
ruleName: '',
severity: '',
status: '',
type: '',
secondaryLocations: [],
flows: []
};
const loc = { component: '', msg: '', textRange };

it('should render secondary locations', () => {
const issue = {
...baseIssue,
secondaryLocations: [{ msg: '', textRange }, { msg: '', textRange }, { msg: '', textRange }]
flows: [],
secondaryLocations: [loc, loc, loc]
};
expect(
shallow(
@@ -59,9 +39,8 @@ it('should render secondary locations', () => {

it('should render one flow', () => {
const issue = {
...baseIssue,
secondaryLocations: [],
flows: [[{ msg: '', textRange }, { msg: '', textRange }, { msg: '', textRange }]]
flows: [[loc, loc, loc]],
secondaryLocations: []
};
expect(
shallow(
@@ -72,12 +51,8 @@ it('should render one flow', () => {

it('should render several flows', () => {
const issue = {
...baseIssue,
flows: [
[{ msg: '', textRange }, { msg: '', textRange }, { msg: '', textRange }],
[{ msg: '', textRange }, { msg: '', textRange }],
[{ msg: '', textRange }, { msg: '', textRange }, { msg: '', textRange }]
]
flows: [[loc, loc, loc], [loc, loc], [loc, loc, loc]],
secondaryLocations: []
};
expect(
shallow(

+ 138
- 0
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/ConciseIssueLocationsNavigator-test.tsx Parādīt failu

@@ -0,0 +1,138 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
import ConciseIssueLocationsNavigator from '../ConciseIssueLocationsNavigator';
import { FlowLocation } from '../../../../app/types';

const location1: FlowLocation = {
component: 'foo',
componentName: 'src/foo.js',
msg: 'Do not use foo',
textRange: { startLine: 7, endLine: 7, startOffset: 5, endOffset: 8 }
};

const location2: FlowLocation = {
component: 'foo',
componentName: 'src/foo.js',
msg: 'Do not use foo',
textRange: { startLine: 8, endLine: 8, startOffset: 0, endOffset: 5 }
};

const location3: FlowLocation = {
component: 'bar',
componentName: 'src/bar.js',
msg: 'Do not use bar',
textRange: { startLine: 15, endLine: 16, startOffset: 4, endOffset: 6 }
};

it('should render secondary locations in the same file', () => {
const issue = {
component: 'foo',
key: '',
flows: [],
secondaryLocations: [location1, location2]
};
expect(
shallow(
<ConciseIssueLocationsNavigator
issue={issue}
onLocationSelect={jest.fn()}
scroll={jest.fn()}
selectedFlowIndex={undefined}
selectedLocationIndex={undefined}
/>
)
).toMatchSnapshot();
});

it('should render flow locations in the same file', () => {
const issue = {
component: 'foo',
key: '',
flows: [[location1, location2]],
secondaryLocations: []
};
expect(
shallow(
<ConciseIssueLocationsNavigator
issue={issue}
onLocationSelect={jest.fn()}
scroll={jest.fn()}
selectedFlowIndex={undefined}
selectedLocationIndex={undefined}
/>
)
).toMatchSnapshot();
});

it('should render selected flow locations in the same file', () => {
const issue = {
component: 'foo',
key: '',
flows: [[location1, location2]],
secondaryLocations: [location1]
};
expect(
shallow(
<ConciseIssueLocationsNavigator
issue={issue}
onLocationSelect={jest.fn()}
scroll={jest.fn()}
selectedFlowIndex={0}
selectedLocationIndex={undefined}
/>
)
).toMatchSnapshot();
});

it('should render flow locations in different file', () => {
const issue = {
component: 'foo',
key: '',
flows: [[location1, location3]],
secondaryLocations: []
};
expect(
shallow(
<ConciseIssueLocationsNavigator
issue={issue}
onLocationSelect={jest.fn()}
scroll={jest.fn()}
selectedFlowIndex={undefined}
selectedLocationIndex={undefined}
/>
)
).toMatchSnapshot();
});

it('should not render locations', () => {
const issue = { component: 'foo', key: '', flows: [], secondaryLocations: [] };
const wrapper = shallow(
<ConciseIssueLocationsNavigator
issue={issue}
onLocationSelect={jest.fn()}
scroll={jest.fn()}
selectedFlowIndex={undefined}
selectedLocationIndex={undefined}
/>
);
expect(wrapper.type()).toBeNull();
});

+ 108
- 0
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/CrossFileLocationsNavigator-test.tsx Parādīt failu

@@ -0,0 +1,108 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
import CrossFileLocationsNavigator from '../CrossFileLocationsNavigator';
import { FlowLocation } from '../../../../app/types';
import { click } from '../../../../helpers/testUtils';

const location1: FlowLocation = {
component: 'foo',
componentName: 'src/foo.js',
msg: 'Do not use foo',
textRange: { startLine: 7, endLine: 7, startOffset: 5, endOffset: 8 }
};

const location2: FlowLocation = {
component: 'foo',
componentName: 'src/foo.js',
msg: 'Do not use foo',
textRange: { startLine: 8, endLine: 8, startOffset: 0, endOffset: 5 }
};

const location3: FlowLocation = {
component: 'bar',
componentName: 'src/bar.js',
msg: 'Do not use bar',
textRange: { startLine: 15, endLine: 16, startOffset: 4, endOffset: 6 }
};

it('should render', () => {
const wrapper = shallow(
<CrossFileLocationsNavigator
issue={{ key: 'abcd' }}
locations={[location1, location2, location3]}
onLocationSelect={jest.fn()}
scroll={jest.fn()}
selectedLocationIndex={undefined}
/>
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('ConciseIssueLocationsNavigatorLocation').length).toBe(2);

click(wrapper.find('.concise-issue-location-file-more'));
expect(wrapper.find('ConciseIssueLocationsNavigatorLocation').length).toBe(3);
});

it('should render all locations', () => {
const wrapper = shallow(
<CrossFileLocationsNavigator
issue={{ key: 'abcd' }}
locations={[location1, location2]}
onLocationSelect={jest.fn()}
scroll={jest.fn()}
selectedLocationIndex={undefined}
/>
);
expect(wrapper.find('ConciseIssueLocationsNavigatorLocation').length).toBe(2);
});

it('should expand all locations', () => {
const wrapper = shallow(
<CrossFileLocationsNavigator
issue={{ key: 'abcd' }}
locations={[location1, location2, location3]}
onLocationSelect={jest.fn()}
scroll={jest.fn()}
selectedLocationIndex={undefined}
/>
);
expect(wrapper.find('ConciseIssueLocationsNavigatorLocation').length).toBe(2);

wrapper.setProps({ selectedLocationIndex: 1 });
expect(wrapper.find('ConciseIssueLocationsNavigatorLocation').length).toBe(3);
});

it('should collapse locations when issue changes', () => {
const wrapper = shallow(
<CrossFileLocationsNavigator
issue={{ key: 'abcd' }}
locations={[location1, location2, location3]}
onLocationSelect={jest.fn()}
scroll={jest.fn()}
selectedLocationIndex={undefined}
/>
);
wrapper.setProps({ selectedLocationIndex: 1 });
expect(wrapper.find('ConciseIssueLocationsNavigatorLocation').length).toBe(3);

wrapper.setProps({ issue: { key: 'def' }, selectedLocationIndex: undefined });
expect(wrapper.find('ConciseIssueLocationsNavigatorLocation').length).toBe(2);
});

+ 136
- 0
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/ConciseIssueLocationsNavigator-test.tsx.snap Parādīt failu

@@ -0,0 +1,136 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render flow locations in different file 1`] = `
<CrossFileLocationsNavigator
issue={
Object {
"component": "foo",
"flows": Array [
Array [
Object {
"component": "foo",
"componentName": "src/foo.js",
"msg": "Do not use foo",
"textRange": Object {
"endLine": 7,
"endOffset": 8,
"startLine": 7,
"startOffset": 5,
},
},
Object {
"component": "bar",
"componentName": "src/bar.js",
"msg": "Do not use bar",
"textRange": Object {
"endLine": 16,
"endOffset": 6,
"startLine": 15,
"startOffset": 4,
},
},
],
],
"key": "",
"secondaryLocations": Array [],
}
}
locations={
Array [
Object {
"component": "foo",
"componentName": "src/foo.js",
"msg": "Do not use foo",
"textRange": Object {
"endLine": 7,
"endOffset": 8,
"startLine": 7,
"startOffset": 5,
},
},
Object {
"component": "bar",
"componentName": "src/bar.js",
"msg": "Do not use bar",
"textRange": Object {
"endLine": 16,
"endOffset": 6,
"startLine": 15,
"startOffset": 4,
},
},
]
}
onLocationSelect={[MockFunction]}
scroll={[MockFunction]}
/>
`;

exports[`should render flow locations in the same file 1`] = `
<div
className="concise-issue-locations-navigator spacer-top"
>
<ConciseIssueLocationsNavigatorLocation
index={0}
key="0"
message="Do not use foo"
onClick={[MockFunction]}
scroll={[MockFunction]}
selected={false}
/>
<ConciseIssueLocationsNavigatorLocation
index={1}
key="1"
message="Do not use foo"
onClick={[MockFunction]}
scroll={[MockFunction]}
selected={false}
/>
</div>
`;

exports[`should render secondary locations in the same file 1`] = `
<div
className="concise-issue-locations-navigator spacer-top"
>
<ConciseIssueLocationsNavigatorLocation
index={0}
key="0"
message="Do not use foo"
onClick={[MockFunction]}
scroll={[MockFunction]}
selected={false}
/>
<ConciseIssueLocationsNavigatorLocation
index={1}
key="1"
message="Do not use foo"
onClick={[MockFunction]}
scroll={[MockFunction]}
selected={false}
/>
</div>
`;

exports[`should render selected flow locations in the same file 1`] = `
<div
className="concise-issue-locations-navigator spacer-top"
>
<ConciseIssueLocationsNavigatorLocation
index={0}
key="0"
message="Do not use foo"
onClick={[MockFunction]}
scroll={[MockFunction]}
selected={false}
/>
<ConciseIssueLocationsNavigatorLocation
index={1}
key="1"
message="Do not use foo"
onClick={[MockFunction]}
scroll={[MockFunction]}
selected={false}
/>
</div>
`;

+ 76
- 0
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/__tests__/__snapshots__/CrossFileLocationsNavigator-test.tsx.snap Parādīt failu

@@ -0,0 +1,76 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render 1`] = `
<div
className="concise-issue-locations-navigator spacer-top"
>
<div
className="concise-issue-locations-navigator-file"
key="0"
>
<div
className="concise-issue-location-file"
>
<i
className="concise-issue-location-file-circle little-spacer-right"
/>
src/foo.js
</div>
<div
className="concise-issue-location-file-locations"
>
<ConciseIssueLocationsNavigatorLocation
index={0}
key="0"
message="Do not use foo"
onClick={[MockFunction]}
scroll={[MockFunction]}
selected={false}
/>
</div>
</div>
<div
className="concise-issue-locations-navigator-file"
>
<div
className="concise-issue-location-file"
>
<i
className="concise-issue-location-file-circle-multiple little-spacer-right"
/>
<a
className="concise-issue-location-file-more"
href="#"
onClick={[Function]}
>
issues.x_more_locations.1
</a>
</div>
</div>
<div
className="concise-issue-locations-navigator-file"
key="1"
>
<div
className="concise-issue-location-file"
>
<i
className="concise-issue-location-file-circle little-spacer-right"
/>
src/bar.js
</div>
<div
className="concise-issue-location-file-locations"
>
<ConciseIssueLocationsNavigatorLocation
index={2}
key="2"
message="Do not use bar"
onClick={[MockFunction]}
scroll={[MockFunction]}
selected={false}
/>
</div>
</div>
</div>
`;

+ 85
- 2
server/sonar-web/src/main/js/apps/issues/styles.css Parādīt failu

@@ -123,12 +123,95 @@
margin-bottom: 4px;
}

.consice-issue-locations-navigator-location {
display: flex;
.concise-issue-locations-navigator-location {
position: relative;
z-index: var(--aboveNormalZIndex);
display: inline-flex;
align-items: flex-start;
max-width: 100%;
border: none;
}

.concise-issue-locations-navigator-file {
position: relative;
}

.concise-issue-locations-navigator-file + .concise-issue-locations-navigator-file {
margin-top: calc(1.5 * var(--gridSize));
}

.concise-issue-locations-navigator-file:not(:last-child)::before {
position: absolute;
display: block;
width: 0;
top: 13px;
bottom: calc(-2 * var(--gridSize));
left: 4px;
border-left: 1px dotted #d18582;
content: '';
}

.concise-issue-location-file {
height: calc(2 * var(--gridSize));
padding-bottom: calc(0.5 * var(--gridSize));
font-size: var(--smallFontSize);
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.concise-issue-location-file-circle,
.concise-issue-location-file-circle-multiple,
.concise-issue-location-file-circle-multiple::before,
.concise-issue-location-file-circle-multiple::after {
position: relative;
top: 1px;
display: inline-block;
width: calc(1px + var(--gridSize));
height: calc(1px + var(--gridSize));
border: 1px solid #d18582;
border-radius: 100%;
box-sizing: border-box;
background-color: #ffeaea;
}

.concise-issue-location-file-circle-multiple {
top: -2px;
}

.concise-issue-location-file-circle-multiple::before {
position: absolute;
z-index: calc(5 + var(--normalZIndex));
top: 2px;
left: -1px;
content: '';
}

.concise-issue-location-file-circle-multiple::after {
position: absolute;
z-index: calc(5 + var(--aboveNormalZIndex));
top: 5px;
left: -1px;
content: '';
}

.concise-issue-location-file-locations {
padding-left: calc(2 * var(--gridSize));
}

.concise-issue-location-file-more {
border-color: rgba(209, 133, 130, 0.2);
color: rgb(209, 133, 130) !important;
font-style: italic;
font-weight: normal;
}

.concise-issue-location-file-more:hover,
.concise-issue-location-file-more:focus {
border-color: rgba(209, 133, 130, 0.6);
}

.issues-my-issues-filter {
margin-bottom: 24px;
text-align: center;

+ 29
- 0
server/sonar-web/src/main/js/apps/issues/utils.ts Parādīt failu

@@ -19,6 +19,7 @@
*/
import { searchMembers } from '../../api/organizations';
import { searchUsers } from '../../api/users';
import { Issue } from '../../app/types';
import { formatMeasure } from '../../helpers/measures';
import {
queriesEqual,
@@ -227,3 +228,31 @@ const save = (value: string) => {

export const saveMyIssues = (myIssues: boolean) =>
save(myIssues ? LOCALSTORAGE_MY : LOCALSTORAGE_ALL);

export function getLocations(
{ flows, secondaryLocations }: Pick<Issue, 'flows' | 'secondaryLocations'>,
selectedFlowIndex: number | undefined
) {
if (selectedFlowIndex !== undefined) {
return flows[selectedFlowIndex] || [];
} else {
return flows.length > 0 ? flows[0] : secondaryLocations;
}
}

export function getSelectedLocation(
issue: Pick<Issue, 'flows' | 'secondaryLocations'>,
selectedFlowIndex: number | undefined,
selectedLocationIndex: number | undefined
) {
const locations = getLocations(issue, selectedFlowIndex);
if (
selectedLocationIndex !== undefined &&
selectedLocationIndex >= 0 &&
locations.length >= selectedLocationIndex
) {
return locations[selectedLocationIndex];
} else {
return undefined;
}
}

+ 11
- 1
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx Parādīt failu

@@ -63,7 +63,9 @@ interface Props {
displayIssueLocationsLink?: boolean;
displayLocationMarkers?: boolean;
highlightedLine?: number;
highlightedLocations?: FlowLocation[];
// `undefined` elements mean they are located in a different file,
// but kept to maintaint the location indexes
highlightedLocations?: (FlowLocation | undefined)[];
highlightedLocationMessage?: { index: number; text: string };
loadComponent?: (
component: string,
@@ -158,6 +160,14 @@ export default class SourceViewerBase extends React.PureComponent<Props, State>
}

componentWillReceiveProps(nextProps: Props) {
// if a component or a branch has changed,
// set `loading: true` immediately to avoid unwanted scrolling in `LineCode`
if (
nextProps.component !== this.props.component ||
!isSameBranchLike(nextProps.branchLike, this.props.branchLike)
) {
this.setState({ loading: true });
}
if (
nextProps.onIssueSelect !== undefined &&
nextProps.selectedIssue !== this.props.selectedIssue

+ 8
- 4
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx Parādīt failu

@@ -54,7 +54,9 @@ interface Props {
hasSourcesBefore: boolean;
highlightedLine: number | undefined;
highlightedLocationMessage: { index: number; text: string } | undefined;
highlightedLocations: FlowLocation[] | undefined;
// `undefined` elements mean they are located in a different file,
// but kept to maintain the location indexes
highlightedLocations: (FlowLocation | undefined)[] | undefined;
highlightedSymbols: string[];
issueLocationsByLine: { [line: number]: LinearIssueLocation[] };
issuePopup: { issue: string; name: string } | undefined;
@@ -102,9 +104,11 @@ export default class SourceViewerCode extends React.PureComponent<Props> {
return EMPTY_ARRAY;
}
return highlightedLocations.reduce((locations, location, index) => {
const linearLocations: LinearIssueLocation[] = getLinearLocations(location.textRange)
.filter(l => l.line === line.line)
.map(l => ({ ...l, startLine: location.textRange.startLine, index }));
const linearLocations: LinearIssueLocation[] = location
? getLinearLocations(location.textRange)
.filter(l => l.line === line.line)
.map(l => ({ ...l, startLine: location.textRange.startLine, index }))
: [];
return [...locations, ...linearLocations];
}, []);
};

+ 54
- 9
server/sonar-web/src/main/js/helpers/__tests__/issues-test.ts Parādīt failu

@@ -60,16 +60,61 @@ it('should populate comments data', () => {
it('orders secondary locations', () => {
const issue = {
flows: [
{ locations: [{ textRange: { startLine: 68, startOffset: 5, endLine: 68, endOffset: 7 } }] },
{ locations: [{ textRange: { startLine: 43, startOffset: 8, endLine: 43, endOffset: 12 } }] },
{ locations: [{ textRange: { startLine: 43, startOffset: 6, endLine: 43, endOffset: 8 } }] },
{ locations: [{ textRange: { startLine: 70, startOffset: 12, endLine: 70, endOffset: 16 } }] }
{
locations: [
{
component: 'foo',
textRange: { startLine: 68, startOffset: 5, endLine: 68, endOffset: 7 }
}
]
},
{
locations: [
{
component: 'unknown',
textRange: { startLine: 43, startOffset: 8, endLine: 43, endOffset: 12 }
}
]
},
{
locations: [
{
component: 'bar',
textRange: { startLine: 43, startOffset: 6, endLine: 43, endOffset: 8 }
}
]
},
{
locations: [
{
component: 'foo',
textRange: { startLine: 70, startOffset: 12, endLine: 70, endOffset: 16 }
}
]
}
]
} as any;
expect(parseIssueFromResponse(issue).secondaryLocations).toEqual([
{ textRange: { startLine: 43, startOffset: 6, endLine: 43, endOffset: 8 } },
{ textRange: { startLine: 43, startOffset: 8, endLine: 43, endOffset: 12 } },
{ textRange: { startLine: 68, startOffset: 5, endLine: 68, endOffset: 7 } },
{ textRange: { startLine: 70, startOffset: 12, endLine: 70, endOffset: 16 } }
const components = [{ key: 'foo', name: 'src/foo.js' }, { key: 'bar', name: 'src/bar.js' }];
expect(parseIssueFromResponse(issue, components).secondaryLocations).toEqual([
{
component: 'bar',
componentName: 'src/bar.js',
textRange: { endLine: 43, endOffset: 8, startLine: 43, startOffset: 6 }
},
{
component: 'unknown',
componentName: undefined,
textRange: { endLine: 43, endOffset: 12, startLine: 43, startOffset: 8 }
},
{
component: 'foo',
componentName: 'src/foo.js',
textRange: { endLine: 68, endOffset: 7, startLine: 68, startOffset: 5 }
},
{
component: 'foo',
componentName: 'src/foo.js',
textRange: { endLine: 70, endOffset: 16, startLine: 70, startOffset: 12 }
}
]);
});

+ 19
- 20
server/sonar-web/src/main/js/helpers/issues.ts Parādīt failu

@@ -19,19 +19,7 @@
*/
import { flatten, sortBy } from 'lodash';
import { SEVERITIES } from './constants';
import { Issue } from '../app/types';

interface TextRange {
startLine: number;
endLine: number;
startOffset: number;
endOffset: number;
}

interface FlowLocation {
msg: string;
textRange?: TextRange;
}
import { Issue, FlowLocation, TextRange, Omit } from '../app/types';

interface Comment {
login: string;
@@ -44,7 +32,10 @@ interface User {

interface Rule {}

interface Component {}
interface Component {
key: string;
name: string;
}

interface IssueBase {
severity: string;
@@ -57,7 +48,8 @@ export interface RawIssue extends IssueBase {
comments?: Array<Comment>;
component: string;
flows?: Array<{
locations?: FlowLocation[];
// `componentName` is not available in RawIssue
locations?: Array<Omit<FlowLocation, 'componentName'>>;
}>;
key: string;
line?: number;
@@ -132,11 +124,18 @@ function reverseLocations(locations: FlowLocation[]): FlowLocation[] {
}

function splitFlows(
issue: RawIssue
issue: RawIssue,
components: Component[] = []
): { secondaryLocations: FlowLocation[]; flows: FlowLocation[][] } {
const parsedFlows = (issue.flows || [])
.filter(flow => flow.locations != null)
.map(flow => flow.locations!.filter(location => location.textRange != null));
const parsedFlows: FlowLocation[][] = (issue.flows || [])
.filter(flow => flow.locations !== undefined)
.map(flow => flow.locations!.filter(location => location.textRange != null))
.map(flow =>
flow.map(location => {
const component = components.find(component => component.key === location.component);
return { ...location, componentName: component && component.name };
})
);

const onlySecondaryLocations = parsedFlows.every(flow => flow.length === 1);

@@ -159,7 +158,7 @@ export function parseIssueFromResponse(
users?: User[],
rules?: Rule[]
): Issue {
const { secondaryLocations, flows } = splitFlows(issue);
const { secondaryLocations, flows } = splitFlows(issue, components);
return {
...issue,
...injectRelational(issue, components, 'component', 'key'),

+ 1
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Parādīt failu

@@ -620,6 +620,7 @@ issues.to_switch_flows=to switch flows
issues.leak_period=Leak Period
issues.my_issues=My Issues
issues.no_my_issues=There are no issues assigned to you.
issues.x_more_locations=+ {0} more location(s)


#------------------------------------------------------------------------------

Notiek ielāde…
Atcelt
Saglabāt