* 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 classNames from 'classnames';
import * as React from 'react';
import { getSources } from '../../../api/components';
import getCoverageStatus from '../../../components/SourceViewer/helpers/getCoverageStatus';
issuePopup?: { issue: string; name: string };
issuesByLine: T.IssuesByLine;
lastSnippetGroup: boolean;
- linePopup?: T.LinePopup;
loadDuplications: (component: string, line: T.SourceLine) => void;
locations: T.FlowLocation[];
onIssueChange: (issue: T.Issue) => void;
onIssuePopupToggle: (issue: string, popupName: string, open?: boolean) => void;
- onLinePopupToggle: (linePopup: T.LinePopup & { component: string }) => void;
onLocationSelect: (index: number) => void;
renderDuplicationPopup: (
component: T.SourceViewerFile,
);
};
- handleLinePopupToggle = (linePopup: T.LinePopup) => {
- this.props.onLinePopupToggle({
- ...linePopup,
- component: this.props.snippetGroup.component.key
- });
- };
-
handleOpenIssues = (line: T.SourceLine) => {
this.setState(state => ({
openIssuesByLine: { ...state.openIssuesByLine, [line.line]: true }
<SnippetViewer
branchLike={this.props.branchLike}
component={this.props.snippetGroup.component}
+ duplications={this.props.duplications}
+ duplicationsByLine={this.props.duplicationsByLine}
expandBlock={this.expandBlock}
handleCloseIssues={this.handleCloseIssues}
- handleLinePopupToggle={this.handleLinePopupToggle}
handleOpenIssues={this.handleOpenIssues}
handleSymbolClick={this.handleSymbolClick}
highlightedLocationMessage={this.props.highlightedLocationMessage}
issuePopup={this.props.issuePopup}
issuesByLine={issuesByLine}
lastSnippetOfLastGroup={lastSnippetOfLastGroup}
- linePopup={this.props.linePopup}
loadDuplications={this.loadDuplications}
locations={this.props.locations}
locationsByLine={locationsByLine}
}
render() {
- const {
- branchLike,
- duplications,
- issue,
- issuesByLine,
- lastSnippetGroup,
- snippetGroup
- } = this.props;
+ const { branchLike, issue, issuesByLine, lastSnippetGroup, snippetGroup } = this.props;
const { additionalLines, loading, snippets } = this.state;
const locations =
issue.component === snippetGroup.component.key ? locationsByLine([issue]) : {};
isFlow ? lastSnippetGroup && snippetIndex === snippets.length - 1 : snippetIndex === 0;
return (
- <div
- className={classNames('component-source-container', {
- 'source-duplications-expanded': duplications && duplications.length > 0
- })}
- ref={this.rootNodeRef}>
+ <div className="component-source-container" ref={this.rootNodeRef}>
<SourceViewerHeaderSlim
branchLike={branchLike}
expandable={!fullyShown}
duplications?: T.Duplication[];
duplicationsByLine: { [line: number]: number[] };
issuePopup?: { issue: string; name: string };
- linePopup?: T.LinePopup & { component: string };
loading: boolean;
notAccessible: boolean;
}
this.mounted = false;
}
- fetchDuplications = (component: string, line: T.SourceLine) => {
+ fetchDuplications = (component: string) => {
getDuplications({
key: component,
...getBranchLikeQuery(this.props.branchLike)
}).then(
r => {
if (this.mounted) {
- this.setState(state => ({
+ this.setState({
duplicatedFiles: r.files,
duplications: r.duplications,
- duplicationsByLine: getDuplicationsByLine(r.duplications),
- linePopup:
- r.duplications.length === 1
- ? { component, index: 0, line: line.line, name: 'duplications' }
- : state.linePopup
- }));
+ duplicationsByLine: getDuplicationsByLine(r.duplications)
+ });
}
},
() => {}
this.setState({
components,
issuePopup: undefined,
- linePopup: undefined,
loading: false
});
if (this.props.onLoaded) {
});
};
- handleLinePopupToggle = ({
- component,
- index,
- line,
- name,
- open
- }: T.LinePopup & { component: string }) => {
- this.setState((state: State) => {
- const samePopup =
- state.linePopup !== undefined &&
- state.linePopup.line === line &&
- state.linePopup.name === name &&
- state.linePopup.component === component &&
- state.linePopup.index === index;
- if (open !== false && !samePopup) {
- return { linePopup: { component, index, line, name } };
- } else if (open !== true && samePopup) {
- return { linePopup: undefined };
- }
- return null;
- });
- };
-
- handleCloseLinePopup = () => {
- this.setState({ linePopup: undefined });
- };
-
renderDuplicationPopup = (component: T.SourceViewerFile, index: number, line: number) => {
const { duplicatedFiles, duplications } = this.state;
branchLike={this.props.branchLike}
duplicatedFiles={duplicatedFiles}
inRemovedComponent={isDuplicationBlockInRemovedComponent(blocks)}
- onClose={this.handleCloseLinePopup}
openComponent={openComponent}
sourceViewerFile={component}
/>
}
const { issue, locations } = this.props;
- const { components, duplications, duplicationsByLine, linePopup } = this.state;
+ const { components, duplications, duplicationsByLine } = this.state;
const issuesByComponent = issuesByComponentAndLine(this.props.issues);
const locationsByComponent = groupLocationsByComponent(issue, locations, components);
return (
<div>
{locationsByComponent.map((snippetGroup, i) => {
- let componentProps = {};
- if (linePopup && snippetGroup.component.key === linePopup.component) {
- componentProps = {
- duplications,
- duplicationsByLine,
- linePopup: { index: linePopup.index, line: linePopup.line, name: linePopup.name }
- };
- }
return (
<SourceViewerContext.Provider
// eslint-disable-next-line react/no-array-index-key
value={{ branchLike: this.props.branchLike, file: snippetGroup.component }}>
<ComponentSourceSnippetGroupViewer
branchLike={this.props.branchLike}
+ duplications={duplications}
+ duplicationsByLine={duplicationsByLine}
highlightedLocationMessage={this.props.highlightedLocationMessage}
issue={issue}
issuePopup={this.state.issuePopup}
locations={snippetGroup.locations || []}
onIssueChange={this.props.onIssueChange}
onIssuePopupToggle={this.handleIssuePopupToggle}
- onLinePopupToggle={this.handleLinePopupToggle}
onLocationSelect={this.props.onLocationSelect}
renderDuplicationPopup={this.renderDuplicationPopup}
scroll={this.props.scroll}
snippetGroup={snippetGroup}
- {...componentProps}
/>
</SourceViewerContext.Provider>
);
duplicationsByLine?: { [line: number]: number[] };
expandBlock: (snippetIndex: number, direction: T.ExpandDirection) => Promise<void>;
handleCloseIssues: (line: T.SourceLine) => void;
- handleLinePopupToggle: (line: T.SourceLine) => void;
handleOpenIssues: (line: T.SourceLine) => void;
handleSymbolClick: (symbols: string[]) => void;
highlightedLocationMessage: { index: number; text: string | undefined } | undefined;
issuePopup?: { issue: string; name: string };
issuesByLine: T.IssuesByLine;
lastSnippetOfLastGroup: boolean;
- linePopup?: T.LinePopup;
- loadDuplications: (line: T.SourceLine) => void;
+ loadDuplications?: (line: T.SourceLine) => void;
locations: T.FlowLocation[];
locationsByLine: { [line: number]: T.LinearIssueLocation[] };
onIssueChange: (issue: T.Issue) => void;
(duplicationsCount && duplicationsByLine && duplicationsByLine[line.line]) || [];
const isSinkLine = issuesForLine.some(i => i.key === this.props.issue.key);
+ const noop = () => {};
return (
<Line
key={line.line}
last={false}
line={line}
- linePopup={this.props.linePopup}
- loadDuplications={this.props.loadDuplications}
+ loadDuplications={this.props.loadDuplications || noop}
onIssueChange={this.props.onIssueChange}
onIssuePopupToggle={this.props.onIssuePopupToggle}
- onIssueSelect={() => {}}
- onIssueUnselect={() => {}}
+ onIssueSelect={noop}
+ onIssueUnselect={noop}
onIssuesClose={this.props.handleCloseIssues}
onIssuesOpen={this.props.handleOpenIssues}
- onLinePopupToggle={this.props.handleLinePopupToggle}
onLocationSelect={this.props.onLocationSelect}
onSymbolClick={this.props.handleSymbolClick}
openIssues={this.props.openIssuesByLine[line.line]}
? Math.max(0, LINES_BELOW_ISSUE - (bottomLine - lowestVisibleIssue))
: 0;
- const displayDuplications = snippet.some(s => !!s.duplicated);
+ const displayDuplications =
+ Boolean(this.props.loadDuplications) && snippet.some(s => !!s.duplicated);
return (
<div className="source-viewer-code snippet" ref={this.snippetNodeRef}>
...mockSnippetsByComponent('a', [32, 33, 34, 35, 36, 52, 53, 54, 55, 56])
};
const loadDuplications = jest.fn();
- const onLinePopupToggle = jest.fn();
const renderDuplicationPopup = jest.fn();
const wrapper = shallowRender({
loadDuplications,
- onLinePopupToggle,
renderDuplicationPopup,
snippetGroup
});
.prop<Function>('loadDuplications')(line);
expect(loadDuplications).toHaveBeenCalledWith('a', line);
- wrapper
- .find('SnippetViewer')
- .first()
- .prop<Function>('handleLinePopupToggle')({ line: 13, name: 'foo' });
- expect(onLinePopupToggle).toHaveBeenCalledWith({ component: 'a', line: 13, name: 'foo' });
-
wrapper
.find('SnippetViewer')
.first()
const wrapper = mount<ComponentSourceSnippetGroupViewer>(
<ComponentSourceSnippetGroupViewer
branchLike={mockMainBranch()}
- duplications={undefined}
- duplicationsByLine={undefined}
highlightedLocationMessage={{ index: 0, text: '' }}
issue={mockIssue()}
issuesByLine={{}}
lastSnippetGroup={false}
- linePopup={undefined}
loadDuplications={jest.fn()}
locations={[]}
onIssueChange={jest.fn()}
onIssuePopupToggle={jest.fn()}
- onLinePopupToggle={jest.fn()}
onLocationSelect={jest.fn()}
renderDuplicationPopup={jest.fn()}
scroll={jest.fn()}
const wrapper = mount<ComponentSourceSnippetGroupViewer>(
<ComponentSourceSnippetGroupViewer
branchLike={mockMainBranch()}
- duplications={undefined}
- duplicationsByLine={undefined}
highlightedLocationMessage={{ index: 0, text: '' }}
issue={mockIssue()}
issuesByLine={{}}
lastSnippetGroup={false}
- linePopup={undefined}
loadDuplications={jest.fn()}
locations={[]}
onIssueChange={jest.fn()}
onIssuePopupToggle={jest.fn()}
- onLinePopupToggle={jest.fn()}
onLocationSelect={jest.fn()}
renderDuplicationPopup={jest.fn()}
scroll={jest.fn()}
return shallow<ComponentSourceSnippetGroupViewer>(
<ComponentSourceSnippetGroupViewer
branchLike={mockMainBranch()}
- duplications={undefined}
- duplicationsByLine={undefined}
highlightedLocationMessage={{ index: 0, text: '' }}
issue={mockIssue()}
issuesByLine={{}}
lastSnippetGroup={false}
- linePopup={undefined}
loadDuplications={jest.fn()}
locations={[]}
onIssueChange={jest.fn()}
onIssuePopupToggle={jest.fn()}
- onLinePopupToggle={jest.fn()}
onLocationSelect={jest.fn()}
renderDuplicationPopup={jest.fn()}
scroll={jest.fn()}
expect(wrapper.state('issuePopup')).toBeUndefined();
});
-it('should handle line popup', async () => {
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
-
- const linePopup = { component: 'foo', index: 0, line: 16, name: 'b.tsx' };
- wrapper.find('ComponentSourceSnippetGroupViewer').prop<Function>('onLinePopupToggle')(linePopup);
- expect(wrapper.state('linePopup')).toEqual(linePopup);
-
- wrapper.find('ComponentSourceSnippetGroupViewer').prop<Function>('onLinePopupToggle')(linePopup);
- expect(wrapper.state('linePopup')).toBeUndefined();
-
- const openLinePopup = { ...linePopup, open: true };
- wrapper.find('ComponentSourceSnippetGroupViewer').prop<Function>('onLinePopupToggle')(
- openLinePopup
- );
- wrapper.find('ComponentSourceSnippetGroupViewer').prop<Function>('onLinePopupToggle')(
- openLinePopup
- );
- expect(wrapper.state('linePopup')).toEqual(linePopup);
-});
-
it('should handle duplication popup', async () => {
const files = { b: { key: 'b', name: 'B.tsx', project: 'foo', projectName: 'Foo' } };
const duplications = [{ blocks: [{ _ref: '1', from: 1, size: 2 }] }];
expect(wrapper.state('duplicatedFiles')).toEqual(files);
expect(wrapper.state('duplications')).toEqual(duplications);
expect(wrapper.state('duplicationsByLine')).toEqual({ '1': [0], '2': [0] });
- expect(wrapper.state('linePopup')).toEqual({
- component: 'foo',
- index: 0,
- line: 16,
- name: 'duplications'
- });
expect(
wrapper.find('ComponentSourceSnippetGroupViewer').prop<Function>('renderDuplicationPopup')(
duplicationsByLine={undefined}
expandBlock={jest.fn()}
handleCloseIssues={jest.fn()}
- handleLinePopupToggle={jest.fn()}
handleOpenIssues={jest.fn()}
handleSymbolClick={jest.fn()}
highlightedLocationMessage={{ index: 0, text: '' }}
issue={mockIssue()}
issuesByLine={{}}
lastSnippetOfLastGroup={false}
- linePopup={undefined}
loadDuplications={jest.fn()}
locations={[]}
locationsByLine={{}}
duplicationsByLine={undefined}
expandBlock={jest.fn()}
handleCloseIssues={jest.fn()}
- handleLinePopupToggle={jest.fn()}
handleOpenIssues={jest.fn()}
handleSymbolClick={jest.fn()}
highlightedLocationMessage={{ index: 0, text: '' }}
issue={mockIssue()}
issuesByLine={{}}
lastSnippetOfLastGroup={false}
- linePopup={undefined}
loadDuplications={jest.fn()}
locations={[]}
locationsByLine={{}}
}
>
<ComponentSourceSnippetGroupViewer
+ duplicationsByLine={Object {}}
issue={
Object {
"actions": Array [],
}
onIssueChange={[MockFunction]}
onIssuePopupToggle={[Function]}
- onLinePopupToggle={[Function]}
onLocationSelect={[MockFunction]}
renderDuplicationPopup={[Function]}
scroll={[MockFunction]}
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
- onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
renderDuplicationPopup={[MockFunction]}
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
- onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
previousLine={
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
- onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
previousLine={
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
- onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
renderDuplicationPopup={[MockFunction]}
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
- onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
previousLine={
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
- onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
previousLine={
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
- onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
previousLine={
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
- onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
renderDuplicationPopup={[MockFunction]}
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
- onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
previousLine={
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
- onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
previousLine={
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
- onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
previousLine={
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
- onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
previousLine={
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
- onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
previousLine={
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
- onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
previousLine={
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
- onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
renderDuplicationPopup={[MockFunction]}
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
- onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
previousLine={
onIssueUnselect={[Function]}
onIssuesClose={[MockFunction]}
onIssuesOpen={[MockFunction]}
- onLinePopupToggle={[MockFunction]}
onLocationSelect={[MockFunction]}
onSymbolClick={[MockFunction]}
previousLine={
highlightedSymbols: string[];
lastLine?: number;
loading: boolean;
- linePopup?: T.LinePopup & { component: string };
sourceLines: T.SourceLine[];
}
});
};
- handleLinePopupToggle = (params: T.LinePopup & { component: string }) => {
- const { component, index, line, name, open } = params;
- this.setState((state: State) => {
- const samePopup =
- state.linePopup !== undefined &&
- state.linePopup.line === line &&
- state.linePopup.name === name &&
- state.linePopup.component === component &&
- state.linePopup.index === index;
- if (open !== false && !samePopup) {
- return { linePopup: params };
- } else if (open !== true && samePopup) {
- return { linePopup: undefined };
- }
- return null;
- });
- };
-
handleSymbolClick = (highlightedSymbols: string[]) => {
this.setState({ highlightedSymbols });
};
render() {
const { branchLike, component, hotspot } = this.props;
- const { highlightedSymbols, lastLine, linePopup, loading, sourceLines } = this.state;
+ const { highlightedSymbols, lastLine, loading, sourceLines } = this.state;
const locations = locationsByLine([hotspot]);
highlightedSymbols={highlightedSymbols}
hotspot={hotspot}
lastLine={lastLine}
- linePopup={linePopup}
loading={loading}
locations={locations}
onExpandBlock={this.handleExpansion}
- onLinePopupToggle={this.handleLinePopupToggle}
onSymbolClick={this.handleSymbolClick}
sourceLines={sourceLines}
sourceViewerFile={sourceViewerFile}
lastLine?: number;
loading: boolean;
locations: { [line: number]: T.LinearIssueLocation[] };
- linePopup?: T.LinePopup & { component: string };
onExpandBlock: (direction: T.ExpandDirection) => Promise<void>;
- onLinePopupToggle: (line: T.SourceLine) => void;
onSymbolClick: (symbols: string[]) => void;
sourceLines: T.SourceLine[];
sourceViewerFile: T.SourceViewerFile;
displayProjectName,
highlightedSymbols,
hotspot,
- linePopup,
loading,
locations,
sourceLines,
displaySCM={false}
expandBlock={(_i, direction) => props.onExpandBlock(direction)}
handleCloseIssues={noop}
- handleLinePopupToggle={props.onLinePopupToggle}
handleOpenIssues={noop}
handleSymbolClick={props.onSymbolClick}
highlightedLocationMessage={undefined}
issue={hotspot}
issuesByLine={{}}
lastSnippetOfLastGroup={false}
- linePopup={linePopup}
- loadDuplications={noop}
locations={[]}
locationsByLine={locations}
onIssueChange={noop}
});
});
-it('should handle line popups', async () => {
- (getSources as jest.Mock).mockResolvedValueOnce(
- range(5, 18).map(line => mockSourceLine({ line }))
- );
-
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
-
- const params = wrapper.state().sourceLines[0];
-
- wrapper
- .find(HotspotSnippetContainerRenderer)
- .props()
- .onLinePopupToggle(params);
-
- expect(wrapper.state().linePopup).toEqual(params);
-
- // Close it
- wrapper
- .find(HotspotSnippetContainerRenderer)
- .props()
- .onLinePopupToggle(params);
-
- expect(wrapper.state().linePopup).toBeUndefined();
-});
-
it('should handle symbol click', () => {
const wrapper = shallowRender();
const symbols = ['symbol'];
highlightedSymbols={[]}
hotspot={mockHotspot()}
lastLine={undefined}
- linePopup={undefined}
loading={false}
locations={{}}
onExpandBlock={jest.fn()}
- onLinePopupToggle={jest.fn()}
onSymbolClick={jest.fn()}
sourceLines={[]}
sourceViewerFile={mockSourceViewerFile()}
}
}
onExpandBlock={[Function]}
- onLinePopupToggle={[Function]}
onSymbolClick={[Function]}
sourceLines={Array []}
sourceViewerFile={
displaySCM={false}
expandBlock={[Function]}
handleCloseIssues={[Function]}
- handleLinePopupToggle={[MockFunction]}
handleOpenIssues={[Function]}
handleSymbolClick={[MockFunction]}
highlightedSymbols={Array []}
}
issuesByLine={Object {}}
lastSnippetOfLastGroup={false}
- loadDuplications={[Function]}
locations={Array []}
locationsByLine={Object {}}
onIssueChange={[Function]}
* 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 classNames from 'classnames';
import { intersection, uniqBy } from 'lodash';
import * as React from 'react';
import { Alert } from 'sonar-ui-common/components/ui/Alert';
interface State {
component?: T.SourceViewerFile;
- displayDuplications: boolean;
duplicatedFiles?: T.Dict<T.DuplicatedFile>;
duplications?: T.Duplication[];
duplicationsByLine: { [line: number]: number[] };
issuePopup?: { issue: string; name: string };
issues?: T.Issue[];
issuesByLine: { [line: number]: T.Issue[] };
- linePopup?: T.LinePopup;
loading: boolean;
loadingSourcesAfter: boolean;
loadingSourcesBefore: boolean;
super(props);
this.state = {
- displayDuplications: false,
duplicationsByLine: {},
hasSourcesAfter: false,
highlightedSymbols: [],
this.setState(
{
component,
- displayDuplications: false,
duplicatedFiles: undefined,
duplications: undefined,
duplicationsByLine: {},
issueLocationsByLine: locationsByLine(issues),
issues,
issuesByLine: issuesByLine(issues),
- linePopup: undefined,
loading: false,
notAccessible: false,
notExist: false,
);
};
- loadDuplications = (line: T.SourceLine) => {
+ loadDuplications = () => {
getDuplications({
key: this.props.component,
...getBranchLikeQuery(this.props.branchLike)
}).then(
r => {
if (this.mounted) {
- this.setState(state => ({
- displayDuplications: true,
+ this.setState({
duplications: r.duplications,
duplicationsByLine: duplicationsByLine(r.duplications),
- duplicatedFiles: r.files,
- linePopup:
- r.duplications.length === 1
- ? { index: 0, line: line.line, name: 'duplications' }
- : state.linePopup
- }));
+ duplicatedFiles: r.files
+ });
}
},
() => {
);
};
- handleLinePopupToggle = ({ index, line, name, open }: T.LinePopup) => {
- this.setState((state: State) => {
- const samePopup =
- state.linePopup !== undefined &&
- state.linePopup.name === name &&
- state.linePopup.line === line &&
- state.linePopup.index === index;
- if (open !== false && !samePopup) {
- return { linePopup: { index, line, name } };
- } else if (open !== true && samePopup) {
- return { linePopup: undefined };
- }
- return null;
- });
- };
-
- closeLinePopup = () => {
- this.setState({ linePopup: undefined });
- };
-
handleIssuePopupToggle = (issue: string, popupName: string, open?: boolean) => {
this.setState((state: State) => {
const samePopup =
branchLike={this.props.branchLike}
duplicatedFiles={duplicatedFiles}
inRemovedComponent={isDuplicationBlockInRemovedComponent(blocks)}
- onClose={this.closeLinePopup}
openComponent={openComponent}
sourceViewerFile={component}
/>
issuePopup={this.state.issuePopup}
issues={this.state.issues}
issuesByLine={this.state.issuesByLine}
- linePopup={this.state.linePopup}
loadDuplications={this.loadDuplications}
loadSourcesAfter={this.loadSourcesAfter}
loadSourcesBefore={this.loadSourcesBefore}
onIssueUnselect={this.handleIssueUnselect}
onIssuesClose={this.handleCloseIssues}
onIssuesOpen={this.handleOpenIssues}
- onLinePopupToggle={this.handleLinePopupToggle}
onLocationSelect={this.props.onLocationSelect}
onSymbolClick={this.handleSymbolClick}
openIssuesByLine={this.state.openIssuesByLine}
return null;
}
- const className = classNames('source-viewer', {
- 'source-duplications-expanded': this.state.displayDuplications
- });
-
return (
<SourceViewerContext.Provider value={{ branchLike: this.props.branchLike, file: component }}>
- <div className={className} ref={node => (this.node = node)}>
+ <div className="source-viewer" ref={node => (this.node = node)}>
{this.renderHeader(this.props.branchLike, component)}
{sourceRemoved && (
<Alert className="spacer-top" variant="warning">
issuePopup: { issue: string; name: string } | undefined;
issues: T.Issue[] | undefined;
issuesByLine: { [line: number]: T.Issue[] };
- linePopup: T.LinePopup | undefined;
loadDuplications: (line: T.SourceLine) => void;
loadingSourcesAfter: boolean;
loadingSourcesBefore: boolean;
onIssueSelect: (issueKey: string) => void;
onIssuesOpen: (line: T.SourceLine) => void;
onIssueUnselect: () => void;
- onLinePopupToggle: (linePopup: T.LinePopup) => void;
onLocationSelect: ((index: number) => void) | undefined;
onSymbolClick: (symbols: string[]) => void;
openIssuesByLine: { [line: number]: boolean };
key={line.line}
last={index === this.props.sources.length - 1 && !this.props.hasSourcesAfter}
line={line}
- linePopup={this.props.linePopup}
loadDuplications={this.props.loadDuplications}
onIssueChange={this.props.onIssueChange}
onIssuePopupToggle={this.props.onIssuePopupToggle}
onIssueUnselect={this.props.onIssueUnselect}
onIssuesClose={this.props.onIssuesClose}
onIssuesOpen={this.props.onIssuesOpen}
- onLinePopupToggle={this.props.onLinePopupToggle}
onLocationSelect={this.props.onLocationSelect}
onSymbolClick={this.props.onSymbolClick}
openIssues={this.props.openIssuesByLine[line.line] || false}
onIssueUnselect={[Function]}
onIssuesClose={[Function]}
onIssuesOpen={[Function]}
- onLinePopupToggle={[Function]}
onSymbolClick={[Function]}
openIssuesByLine={Object {}}
renderDuplicationPopup={[Function]}
import { groupBy, sortBy } from 'lodash';
import * as React from 'react';
import { Link } from 'react-router';
-import { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown';
import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon';
import { Alert } from 'sonar-ui-common/components/ui/Alert';
-import { PopupPlacement } from 'sonar-ui-common/components/ui/popups';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { collapsedDirFromPath, fileFromPath } from 'sonar-ui-common/helpers/path';
import { isPullRequest } from '../../../helpers/branch-like';
branchLike: BranchLike | undefined;
duplicatedFiles?: T.Dict<T.DuplicatedFile>;
inRemovedComponent: boolean;
- onClose: () => void;
openComponent: WorkspaceContextShape['openComponent'];
sourceViewerFile: T.SourceViewerFile;
}
line: line ? Number(line) : undefined
});
}
- this.props.onClose();
};
renderDuplication(file: T.DuplicatedFile, children: React.ReactNode, line?: number) {
);
return (
- <DropdownOverlay placement={PopupPlacement.RightTop}>
- <div className="source-viewer-bubble-popup abs-width-400">
- {this.props.inRemovedComponent && (
- <Alert variant="warning">
- {translate('duplications.dups_found_on_deleted_resource')}
- </Alert>
- )}
- {duplications.length > 0 && (
- <>
- <h6 className="spacer-bottom">
- {translate('component_viewer.transition.duplication')}
- </h6>
- {duplications.map(duplication => (
- <div className="spacer-top text-ellipsis" key={duplication.file.key}>
- <div className="component-name">
- {this.isDifferentComponent(duplication.file, this.props.sourceViewerFile) && (
- <>
+ <div className="source-viewer-bubble-popup abs-width-400">
+ {this.props.inRemovedComponent && (
+ <Alert variant="warning">
+ {translate('duplications.dups_found_on_deleted_resource')}
+ </Alert>
+ )}
+ {duplications.length > 0 && (
+ <>
+ <h6 className="spacer-bottom">
+ {translate('component_viewer.transition.duplication')}
+ </h6>
+ {duplications.map(duplication => (
+ <div className="spacer-top text-ellipsis" key={duplication.file.key}>
+ <div className="component-name">
+ {this.isDifferentComponent(duplication.file, this.props.sourceViewerFile) && (
+ <>
+ <div className="component-name-parent">
+ <QualifierIcon className="little-spacer-right" qualifier="TRK" />
+ <Link to={getProjectUrl(duplication.file.project)}>
+ {duplication.file.projectName}
+ </Link>
+ </div>
+ {duplication.file.subProject && duplication.file.subProjectName && (
<div className="component-name-parent">
- <QualifierIcon className="little-spacer-right" qualifier="TRK" />
- <Link to={getProjectUrl(duplication.file.project)}>
- {duplication.file.projectName}
- </Link>
+ <QualifierIcon className="little-spacer-right" qualifier="BRC" />
+ {duplication.file.subProjectName}
</div>
- {duplication.file.subProject && duplication.file.subProjectName && (
- <div className="component-name-parent">
- <QualifierIcon className="little-spacer-right" qualifier="BRC" />
- {duplication.file.subProjectName}
- </div>
- )}
- </>
- )}
+ )}
+ </>
+ )}
+
+ {duplication.file.key !== this.props.sourceViewerFile.key && (
+ <div className="component-name-path">
+ {this.renderDuplication(
+ duplication.file,
+ <>
+ <span>{collapsedDirFromPath(duplication.file.name)}</span>
+ <span className="component-name-file">
+ {fileFromPath(duplication.file.name)}
+ </span>
+ </>
+ )}
+ </div>
+ )}
- {duplication.file.key !== this.props.sourceViewerFile.key && (
- <div className="component-name-path">
+ <div className="component-name-path">
+ {'Lines: '}
+ {duplication.blocks.map((block, index) => (
+ <React.Fragment key={index}>
{this.renderDuplication(
duplication.file,
<>
- <span>{collapsedDirFromPath(duplication.file.name)}</span>
- <span className="component-name-file">
- {fileFromPath(duplication.file.name)}
- </span>
- </>
+ {block.from}
+ {' – '}
+ {block.from + block.size - 1}
+ </>,
+ block.from
)}
- </div>
- )}
-
- <div className="component-name-path">
- {'Lines: '}
- {duplication.blocks.map((block, index) => (
- <React.Fragment key={index}>
- {this.renderDuplication(
- duplication.file,
- <>
- {block.from}
- {' – '}
- {block.from + block.size - 1}
- </>,
- block.from
- )}
- {index < duplication.blocks.length - 1 && ', '}
- </React.Fragment>
- ))}
- </div>
+ {index < duplication.blocks.length - 1 && ', '}
+ </React.Fragment>
+ ))}
</div>
</div>
- ))}
- </>
- )}
- </div>
- </DropdownOverlay>
+ </div>
+ ))}
+ </>
+ )}
+ </div>
);
}
}
background-color: #f5f5f5;
}
+.source-line [role='button'] {
+ cursor: pointer;
+}
+
.source-line-highlighted .source-line-number,
.source-line-highlighted:hover .source-line-number,
.source-line-highlighted .source-line-issues,
outline: none;
}
-.source-meta[role='button'] {
- cursor: pointer;
-}
-
.source-line-number {
min-width: 18px;
padding: 0 10px;
text-align: right;
}
-.source-line-number:before {
- content: attr(data-line-number);
-}
-
.source-line-issues {
position: relative;
padding: 0 2px;
display: none;
}
-.source-duplications-expanded .source-line-duplications {
- display: none;
+.source-line-scm {
+ padding: 0 5px;
+ background-color: var(--barBackgroundColor);
}
-.source-duplications-expanded .source-line-duplications-extra {
- display: table-cell;
+.source-line-scm .dropdown {
+ display: block;
}
-.source-line-scm {
- padding: 0 5px;
- background-color: var(--barBackgroundColor);
+.source-line-scm [role='button'] {
+ height: 18px;
}
.source-line-scm-inner {
white-space: nowrap;
}
-.source-line-scm-inner:before {
- content: attr(data-author);
-}
-
.source-line-bar {
width: 5px;
height: 18px;
}
-.source-line-bar[role='button'] {
- cursor: pointer;
-}
-
.source-line-bar:focus {
outline: none;
}
import LineCode from './LineCode';
import LineCoverage from './LineCoverage';
import LineDuplicationBlock from './LineDuplicationBlock';
-import LineDuplications from './LineDuplications';
import LineIssuesIndicator from './LineIssuesIndicator';
import LineNumber from './LineNumber';
import LineSCM from './LineSCM';
issues: T.Issue[];
last: boolean;
line: T.SourceLine;
- linePopup: T.LinePopup | undefined;
loadDuplications: (line: T.SourceLine) => void;
- onLinePopupToggle: (linePopup: T.LinePopup) => void;
onIssueChange: (issue: T.Issue) => void;
onIssuePopupToggle: (issueKey: string, popupName: string, open?: boolean) => void;
onIssuesClose: (line: T.SourceLine) => void;
const LINE_HEIGHT = 18;
export default class Line extends React.PureComponent<Props> {
- isPopupOpen = (name: string, index?: number) => {
- const { line, linePopup } = this.props;
- return (
- linePopup !== undefined &&
- linePopup.index === index &&
- linePopup.line === line.line &&
- linePopup.name === name
- );
- };
-
handleIssuesIndicatorClick = () => {
if (this.props.openIssues) {
this.props.onIssuesClose(this.props.line);
render() {
const {
+ branchLike,
+ displayAllIssues,
displayCoverage,
+ displayDuplications,
+ displayIssueLocationsCount,
+ displayIssueLocationsLink,
+ displayLocationMarkers,
+ highlightedLocationMessage,
+ displayIssues,
displaySCM = true,
duplications,
duplicationsCount,
+ highlighted,
+ highlightedSymbols,
+ issueLocations,
issuePopup,
- line
+ issues,
+ last,
+ line,
+ openIssues,
+ previousLine,
+ secondaryIssueLocations,
+ selectedIssue,
+ verticalBuffer
} = this.props;
+
const className = classNames('source-line', {
- 'source-line-highlighted': this.props.highlighted,
+ 'source-line-highlighted': highlighted,
'source-line-filtered': line.isNew,
'source-line-filtered-dark':
displayCoverage &&
(line.coverageStatus === 'uncovered' || line.coverageStatus === 'partially-covered'),
- 'source-line-last': this.props.last === true
+ 'source-line-last': last === true
});
- const bottomPadding = this.props.verticalBuffer
- ? this.props.verticalBuffer * LINE_HEIGHT
- : undefined;
+ const bottomPadding = verticalBuffer ? verticalBuffer * LINE_HEIGHT : undefined;
return (
<tr className={className} data-line-number={line.line}>
- <LineNumber
- line={line}
- onPopupToggle={this.props.onLinePopupToggle}
- popupOpen={this.isPopupOpen('line-number')}
- />
+ <LineNumber line={line} />
- {displaySCM && (
- <LineSCM
- line={line}
- onPopupToggle={this.props.onLinePopupToggle}
- popupOpen={this.isPopupOpen('scm')}
- previousLine={this.props.previousLine}
- />
- )}
- {this.props.displayIssues && !this.props.displayAllIssues ? (
+ {displaySCM && <LineSCM line={line} previousLine={previousLine} />}
+ {displayIssues && !displayAllIssues ? (
<LineIssuesIndicator
- issues={this.props.issues}
+ issues={issues}
+ issuesOpen={openIssues}
line={line}
onClick={this.handleIssuesIndicatorClick}
/>
<td className="source-meta source-line-issues" />
)}
- {this.props.displayDuplications && (
- <LineDuplications line={line} onClick={this.props.loadDuplications} />
- )}
-
- {times(duplicationsCount, index => (
+ {displayDuplications && (
<LineDuplicationBlock
- duplicated={duplications.includes(index)}
- index={index}
- key={index}
+ blocksLoaded={duplicationsCount > 0}
+ duplicated={Boolean(line.duplicated)}
+ index={0}
+ key={0}
line={this.props.line}
- onPopupToggle={this.props.onLinePopupToggle}
- popupOpen={this.isPopupOpen('duplications', index)}
+ onClick={this.props.loadDuplications}
renderDuplicationPopup={this.props.renderDuplicationPopup}
/>
- ))}
+ )}
+
+ {duplicationsCount > 1 &&
+ times(duplicationsCount - 1, index => (
+ <LineDuplicationBlock
+ blocksLoaded={true}
+ duplicated={duplications.includes(index + 1)}
+ index={index + 1}
+ key={index + 1}
+ line={this.props.line}
+ renderDuplicationPopup={this.props.renderDuplicationPopup}
+ />
+ ))}
- {this.props.displayCoverage && <LineCoverage line={line} />}
+ {displayCoverage && <LineCoverage line={line} />}
<LineCode
- branchLike={this.props.branchLike}
- displayIssueLocationsCount={this.props.displayIssueLocationsCount}
- displayIssueLocationsLink={this.props.displayIssueLocationsLink}
- displayLocationMarkers={this.props.displayLocationMarkers}
- highlightedLocationMessage={this.props.highlightedLocationMessage}
- highlightedSymbols={this.props.highlightedSymbols}
- issueLocations={this.props.issueLocations}
+ branchLike={branchLike}
+ displayIssueLocationsCount={displayIssueLocationsCount}
+ displayIssueLocationsLink={displayIssueLocationsLink}
+ displayLocationMarkers={displayLocationMarkers}
+ highlightedLocationMessage={highlightedLocationMessage}
+ highlightedSymbols={highlightedSymbols}
+ issueLocations={issueLocations}
issuePopup={issuePopup}
- issues={this.props.issues}
+ issues={issues}
line={line}
onIssueChange={this.props.onIssueChange}
onIssuePopupToggle={this.props.onIssuePopupToggle}
onSymbolClick={this.props.onSymbolClick}
padding={bottomPadding}
scroll={this.props.scroll}
- secondaryIssueLocations={this.props.secondaryIssueLocations}
- selectedIssue={this.props.selectedIssue}
- showIssues={this.props.openIssues || this.props.displayAllIssues}
+ secondaryIssueLocations={secondaryIssueLocations}
+ selectedIssue={selectedIssue}
+ showIssues={openIssues || displayAllIssues}
/>
</tr>
);
import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
-interface Props {
+export interface LineCoverageProps {
line: T.SourceLine;
}
-export default function LineCoverage({ line }: Props) {
+export function LineCoverage({ line }: LineCoverageProps) {
const className =
'source-meta source-line-coverage' +
(line.coverageStatus != null ? ` source-line-${line.coverageStatus}` : '');
+ const status = getStatusTooltip(line);
+
return (
<td className={className} data-line-number={line.line}>
- <Tooltip overlay={getStatusTooltip(line)} placement="right">
- <div className="source-line-bar" />
+ <Tooltip overlay={status} placement="right">
+ <div aria-label={status} className="source-line-bar" />
</Tooltip>
</td>
);
}
return undefined;
}
+
+export default React.memo(LineCoverage);
*/
import * as classNames from 'classnames';
import * as React from 'react';
+import { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown';
import Toggler from 'sonar-ui-common/components/controls/Toggler';
import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
+import { PopupPlacement } from 'sonar-ui-common/components/ui/popups';
import { translate } from 'sonar-ui-common/helpers/l10n';
-interface Props {
+export interface LineDuplicationBlockProps {
+ blocksLoaded: boolean;
duplicated: boolean;
index: number;
line: T.SourceLine;
- onPopupToggle: (linePopup: T.LinePopup) => void;
- popupOpen: boolean;
+ onClick?: (line: T.SourceLine) => void;
renderDuplicationPopup: (index: number, line: number) => React.ReactNode;
}
-export default class LineDuplicationBlock extends React.PureComponent<Props> {
- handleClick = (event: React.MouseEvent<HTMLElement>) => {
- event.preventDefault();
- event.stopPropagation();
- event.currentTarget.blur();
- this.props.onPopupToggle({
- index: this.props.index,
- line: this.props.line.line,
- name: 'duplications'
- });
- };
+export function LineDuplicationBlock(props: LineDuplicationBlockProps) {
+ const { blocksLoaded, duplicated, index, line } = props;
+ const [dropdownOpen, setDropdownOpen] = React.useState(false);
- handleTogglePopup = (open: boolean) => {
- this.props.onPopupToggle({
- index: this.props.index,
- line: this.props.line.line,
- name: 'duplications',
- open
- });
- };
+ const className = classNames('source-meta', 'source-line-duplications', {
+ 'source-line-duplicated': duplicated
+ });
- closePopup = () => {
- this.handleTogglePopup(false);
- };
-
- render() {
- const { duplicated, index, line, popupOpen } = this.props;
- const className = classNames('source-meta', 'source-line-duplications-extra', {
- 'source-line-duplicated': duplicated
- });
-
- return duplicated ? (
- <td className={className} data-index={index} data-line-number={line.line}>
- <Toggler
- onRequestClose={this.closePopup}
- open={popupOpen}
- overlay={this.props.renderDuplicationPopup(index, line.line)}>
- <Tooltip
- overlay={popupOpen ? undefined : translate('source_viewer.tooltip.duplicated_block')}
- placement="right">
+ return duplicated ? (
+ <td className={className} data-index={index} data-line-number={line.line}>
+ <Tooltip
+ overlay={dropdownOpen ? undefined : translate('source_viewer.tooltip.duplicated_block')}
+ placement="right">
+ <div>
+ <Toggler
+ onRequestClose={() => setDropdownOpen(false)}
+ open={dropdownOpen}
+ overlay={
+ <DropdownOverlay placement={PopupPlacement.RightTop}>
+ {props.renderDuplicationPopup(index, line.line)}
+ </DropdownOverlay>
+ }>
<div
+ aria-label={translate('source_viewer.tooltip.duplicated_block')}
className="source-line-bar"
- onClick={this.handleClick}
+ onClick={() => {
+ setDropdownOpen(true);
+ if (!blocksLoaded && line.duplicated && props.onClick) {
+ props.onClick(line);
+ }
+ }}
role="button"
tabIndex={0}
/>
- </Tooltip>
- </Toggler>
- </td>
- ) : (
- <td className={className} data-index={index} data-line-number={line.line}>
- <div className="source-line-bar" />
- </td>
- );
- }
+ </Toggler>
+ </div>
+ </Tooltip>
+ </td>
+ ) : (
+ <td className={className} data-index={index} data-line-number={line.line}>
+ <div className="source-line-bar" />
+ </td>
+ );
}
+
+export default React.memo(LineDuplicationBlock);
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2020 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as classNames from 'classnames';
-import * as React from 'react';
-import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
-import { translate } from 'sonar-ui-common/helpers/l10n';
-
-interface Props {
- line: T.SourceLine;
- onClick: (line: T.SourceLine) => void;
-}
-
-export default class LineDuplications extends React.PureComponent<Props> {
- handleClick = (event: React.MouseEvent<HTMLElement>) => {
- event.preventDefault();
- this.props.onClick(this.props.line);
- };
-
- render() {
- const { line } = this.props;
- const className = classNames('source-meta', 'source-line-duplications', {
- 'source-line-duplicated': line.duplicated
- });
-
- const cell = (
- <td
- className={className}
- onClick={line.duplicated ? this.handleClick : undefined}
- role={line.duplicated ? 'button' : undefined}
- tabIndex={line.duplicated ? 0 : undefined}>
- <div className="source-line-bar" />
- </td>
- );
-
- return line.duplicated ? (
- <Tooltip overlay={translate('source_viewer.tooltip.duplicated_line')} placement="right">
- {cell}
- </Tooltip>
- ) : (
- cell
- );
- }
-}
import * as classNames from 'classnames';
import * as React from 'react';
import IssueIcon from 'sonar-ui-common/components/icons/IssueIcon';
+import { translate } from 'sonar-ui-common/helpers/l10n';
import { sortByType } from '../../../helpers/issues';
-interface Props {
+export interface LineIssuesIndicatorProps {
issues: T.Issue[];
+ issuesOpen?: boolean;
line: T.SourceLine;
onClick: () => void;
}
-export default class LineIssuesIndicator extends React.PureComponent<Props> {
- handleClick = (event: React.MouseEvent<HTMLElement>) => {
- event.preventDefault();
- this.props.onClick();
- };
+export function LineIssuesIndicator(props: LineIssuesIndicatorProps) {
+ const { issues, issuesOpen, line } = props;
+ const hasIssues = issues.length > 0;
+ const className = classNames('source-meta', 'source-line-issues', {
+ 'source-line-with-issues': hasIssues
+ });
+ const mostImportantIssue = hasIssues ? sortByType(issues)[0] : null;
- render() {
- const { issues, line } = this.props;
- const hasIssues = issues.length > 0;
- const className = classNames('source-meta', 'source-line-issues', {
- 'source-line-with-issues': hasIssues
- });
- const mostImportantIssue = hasIssues ? sortByType(issues)[0] : null;
+ const handleClick = (e: React.MouseEvent<HTMLElement>) => {
+ e.preventDefault();
+ e.currentTarget.blur();
+ props.onClick();
+ };
- return (
- <td
- className={className}
- data-line-number={line.line}
- onClick={hasIssues ? this.handleClick : undefined}
- role={hasIssues ? 'button' : undefined}
- tabIndex={hasIssues ? 0 : undefined}>
- {mostImportantIssue != null && <IssueIcon type={mostImportantIssue.type} />}
- {issues.length > 1 && <span className="source-line-issues-counter">{issues.length}</span>}
- </td>
- );
- }
+ return (
+ <td className={className} data-line-number={line.line}>
+ {hasIssues && (
+ <span
+ aria-label={translate('source_viewer.issues_on_line', issuesOpen ? 'hide' : 'show')}
+ onClick={handleClick}
+ role="button"
+ tabIndex={0}>
+ {mostImportantIssue != null && <IssueIcon type={mostImportantIssue.type} />}
+ {issues.length > 1 && <span className="source-line-issues-counter">{issues.length}</span>}
+ </span>
+ )}
+ </td>
+ );
}
+
+export default React.memo(LineIssuesIndicator);
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import Toggler from 'sonar-ui-common/components/controls/Toggler';
+import Dropdown from 'sonar-ui-common/components/controls/Dropdown';
+import { PopupPlacement } from 'sonar-ui-common/components/ui/popups';
+import { translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import LineOptionsPopup from './LineOptionsPopup';
-interface Props {
+export interface LineNumberProps {
line: T.SourceLine;
- onPopupToggle: (linePopup: T.LinePopup) => void;
- popupOpen: boolean;
}
-export default class LineNumber extends React.PureComponent<Props> {
- handleClick = (event: React.MouseEvent<HTMLElement>) => {
- event.preventDefault();
- event.stopPropagation();
- event.currentTarget.blur();
- this.props.onPopupToggle({ line: this.props.line.line, name: 'line-number' });
- };
-
- handleTogglePopup = (open: boolean) => {
- this.props.onPopupToggle({ line: this.props.line.line, name: 'line-number', open });
- };
-
- closePopup = () => {
- this.handleTogglePopup(false);
- };
-
- render() {
- const { line, popupOpen } = this.props;
- const { line: lineNumber } = line;
- const hasLineNumber = !!lineNumber;
- return hasLineNumber ? (
- <td
- className="source-meta source-line-number"
- data-line-number={lineNumber}
- onClick={this.handleClick}
- // eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
- role="button"
- tabIndex={0}>
- <Toggler
- onRequestClose={this.closePopup}
- open={popupOpen}
- overlay={<LineOptionsPopup line={line} />}
- />
- </td>
- ) : (
- <td className="source-meta source-line-number" />
- );
- }
+export function LineNumber({ line }: LineNumberProps) {
+ const { line: lineNumber } = line;
+ const hasLineNumber = !!lineNumber;
+ return hasLineNumber ? (
+ <td className="source-meta source-line-number" data-line-number={lineNumber}>
+ <Dropdown
+ overlay={<LineOptionsPopup line={line} />}
+ overlayPlacement={PopupPlacement.RightTop}>
+ <span
+ aria-label={translateWithParameters('source_viewer.line_X', lineNumber)}
+ role="button">
+ {lineNumber}
+ </span>
+ </Dropdown>
+ </td>
+ ) : (
+ <td className="source-meta source-line-number" />
+ );
}
+
+export default React.memo(LineNumber);
*/
import * as React from 'react';
import { Link } from 'react-router';
-import { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown';
-import { PopupPlacement } from 'sonar-ui-common/components/ui/popups';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { getCodeUrl } from '../../../helpers/urls';
import { SourceViewerContext } from '../SourceViewerContext';
-interface Props {
+interface LineOptionsPopupProps {
line: T.SourceLine;
}
-export default function LineOptionsPopup({ line }: Props) {
+export function LineOptionsPopup({ line }: LineOptionsPopupProps) {
return (
<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>
+ <div className="source-viewer-bubble-popup nowrap">
+ <Link
+ className="js-get-permalink"
+ rel="noopener noreferrer"
+ target="_blank"
+ to={getCodeUrl(file.project, branchLike, file.key, line.line)}>
+ {translate('component_viewer.get_permalink')}
+ </Link>
+ </div>
)}
</SourceViewerContext.Consumer>
);
}
+
+export default React.memo(LineOptionsPopup);
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import Toggler from 'sonar-ui-common/components/controls/Toggler';
+import Dropdown from 'sonar-ui-common/components/controls/Dropdown';
+import { PopupPlacement } from 'sonar-ui-common/components/ui/popups';
+import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import SCMPopup from './SCMPopup';
-interface Props {
+export interface LineSCMProps {
line: T.SourceLine;
- onPopupToggle: (linePopup: T.LinePopup) => void;
- popupOpen: boolean;
previousLine: T.SourceLine | undefined;
}
-export default class LineSCM extends React.PureComponent<Props> {
- handleClick = (event: React.MouseEvent<HTMLElement>) => {
- event.preventDefault();
- event.stopPropagation();
- event.currentTarget.blur();
- this.props.onPopupToggle({ line: this.props.line.line, name: 'scm' });
- };
+export function LineSCM({ line, previousLine }: LineSCMProps) {
+ const hasPopup = !!line.line;
+ const cell = (
+ <div className="source-line-scm-inner">
+ {isSCMChanged(line, previousLine) ? line.scmAuthor || '…' : ' '}
+ </div>
+ );
- handleTogglePopup = (open: boolean) => {
- this.props.onPopupToggle({ line: this.props.line.line, name: 'scm', open });
- };
+ if (hasPopup) {
+ let ariaLabel = translate('source_viewer.click_for_scm_info');
+ if (line.scmAuthor) {
+ ariaLabel = `${translateWithParameters(
+ 'source_viewer.author_X',
+ line.scmAuthor
+ )}, ${ariaLabel}`;
+ }
- closePopup = () => {
- this.handleTogglePopup(false);
- };
-
- render() {
- const { line, popupOpen, previousLine } = this.props;
- const hasPopup = !!line.line;
- const cell = isSCMChanged(line, previousLine) && (
- <div className="source-line-scm-inner" data-author={line.scmAuthor || '…'} />
- );
- return hasPopup ? (
- <td
- className="source-meta source-line-scm"
- data-line-number={line.line}
- onClick={this.handleClick}
- // eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
- role="button"
- tabIndex={0}>
- <Toggler
- onRequestClose={this.closePopup}
- open={popupOpen}
- overlay={<SCMPopup line={line} />}>
- {cell}
- </Toggler>
+ return (
+ <td className="source-meta source-line-scm" data-line-number={line.line}>
+ <Dropdown overlay={<SCMPopup line={line} />} overlayPlacement={PopupPlacement.RightTop}>
+ <div aria-label={ariaLabel} role="button">
+ {cell}
+ </div>
+ </Dropdown>
</td>
- ) : (
+ );
+ } else {
+ return (
<td className="source-meta source-line-scm" data-line-number={line.line}>
{cell}
</td>
}
return changed;
}
+
+export default React.memo(LineSCM);
*/
import * as classNames from 'classnames';
import * as React from 'react';
-import { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown';
import DateFormatter from 'sonar-ui-common/components/intl/DateFormatter';
-import { PopupPlacement } from 'sonar-ui-common/components/ui/popups';
+import { translate } from 'sonar-ui-common/helpers/l10n';
-interface Props {
+export interface SCMPopupProps {
line: T.SourceLine;
}
-export default function SCMPopup({ line }: Props) {
- const hasAuthor = line.scmAuthor !== '';
+export function SCMPopup({ line }: SCMPopupProps) {
+ const hasAuthor = line.scmAuthor !== undefined && line.scmAuthor !== '';
+ const hasDate = line.scmDate !== undefined;
return (
- <DropdownOverlay placement={PopupPlacement.RightTop}>
- <div className="source-viewer-bubble-popup abs-width-400">
- {hasAuthor && <div>{line.scmAuthor}</div>}
- {line.scmDate && (
- <div className={classNames({ 'spacer-top': hasAuthor })}>
- <DateFormatter date={line.scmDate} />
- </div>
- )}
- {line.scmRevision && <div className="spacer-top">{line.scmRevision}</div>}
- </div>
- </DropdownOverlay>
+ <div className="source-viewer-bubble-popup abs-width-400">
+ {hasAuthor && (
+ <div>
+ <h4>{translate('author')}</h4>
+ {line.scmAuthor}
+ </div>
+ )}
+ {hasDate && (
+ <div className={classNames({ 'spacer-top': hasAuthor })}>
+ <h4>{translate('source_viewer.tooltip.scm.commited_on')}</h4>
+ <DateFormatter date={line.scmDate!} />
+ </div>
+ )}
+ {line.scmRevision && (
+ <div className={classNames({ 'spacer-top': hasAuthor || hasDate })}>
+ <h4>{translate('source_viewer.tooltip.scm.revision')}</h4>
+ {line.scmRevision}
+ </div>
+ )}
+ </div>
);
}
+
+export default React.memo(SCMPopup);
issues={[mockIssue(), mockIssue(false, { type: 'VULNERABILITY' })]}
last={false}
line={mockSourceLine()}
- linePopup={undefined}
loadDuplications={jest.fn()}
- onLinePopupToggle={jest.fn()}
onIssueChange={jest.fn()}
onIssuePopupToggle={jest.fn()}
onIssuesClose={jest.fn()}
*/
import { shallow } from 'enzyme';
import * as React from 'react';
-import LineCoverage from '../LineCoverage';
+import { LineCoverage, LineCoverageProps } from '../LineCoverage';
-it('render covered line', () => {
- const line: T.SourceLine = { line: 3, coverageStatus: 'covered' };
- const wrapper = shallow(<LineCoverage line={line} />);
- expect(wrapper).toMatchSnapshot();
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot('covered');
+ expect(shallowRender({ line: { line: 3, coverageStatus: 'uncovered' } })).toMatchSnapshot(
+ 'uncovered'
+ );
+ expect(shallowRender({ line: { line: 3, coverageStatus: 'partially-covered' } })).toMatchSnapshot(
+ 'partially covered, 0 conditions'
+ );
+ expect(
+ shallowRender({ line: { line: 3, coverageStatus: 'partially-covered', coveredConditions: 10 } })
+ ).toMatchSnapshot('partially covered, 10 conditions');
+ expect(shallowRender({ line: { line: 3, coverageStatus: undefined } })).toMatchSnapshot(
+ 'no data'
+ );
});
-it('render uncovered line', () => {
- const line: T.SourceLine = { line: 3, coverageStatus: 'uncovered' };
- const wrapper = shallow(<LineCoverage line={line} />);
- expect(wrapper).toMatchSnapshot();
-});
-
-it('render line with unknown coverage', () => {
- const line: T.SourceLine = { line: 3 };
- const wrapper = shallow(<LineCoverage line={line} />);
- expect(wrapper).toMatchSnapshot();
-});
+function shallowRender(props: Partial<LineCoverageProps> = {}) {
+ return shallow(<LineCoverage line={{ line: 3, coverageStatus: 'covered' }} {...props} />);
+}
*/
import { shallow } from 'enzyme';
import * as React from 'react';
+import Toggler from 'sonar-ui-common/components/controls/Toggler';
import { click } from 'sonar-ui-common/helpers/testUtils';
-import LineDuplicationBlock from '../LineDuplicationBlock';
+import { LineDuplicationBlock, LineDuplicationBlockProps } from '../LineDuplicationBlock';
-it('render duplicated line', () => {
- const line = { line: 3, duplicated: true };
- const onPopupToggle = jest.fn();
- const wrapper = shallow(
- <LineDuplicationBlock
- duplicated={true}
- index={1}
- line={line}
- onPopupToggle={onPopupToggle}
- popupOpen={false}
- renderDuplicationPopup={jest.fn()}
- />
- );
- expect(wrapper).toMatchSnapshot();
- click(wrapper.find('[tabIndex]'));
- expect(onPopupToggle).toHaveBeenCalled();
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot('default');
+ expect(
+ shallowRender({ line: { line: 3, duplicated: false }, duplicated: false })
+ ).toMatchSnapshot('not duplicated');
+});
+
+it('should correctly open/close the dropdown', () => {
+ const wrapper = shallowRender();
+ click(wrapper.find('div[role="button"]'));
+ expect(wrapper.find(Toggler).prop('open')).toBe(true);
+ wrapper.find(Toggler).prop('onRequestClose')();
+ expect(wrapper.find(Toggler).prop('open')).toBe(false);
});
-it('render not duplicated line', () => {
- const line = { line: 3, duplicated: false };
- const wrapper = shallow(
+it('should correctly call the onCick prop', () => {
+ const line = { line: 1, duplicated: true };
+ const onClick = jest.fn();
+ const wrapper = shallowRender({ line, onClick });
+
+ // Propagate if blocks aren't loaded.
+ click(wrapper.find('div[role="button"]'));
+ expect(onClick).toBeCalledWith(line);
+
+ // Don't propagate if blocks were loaded.
+ onClick.mockClear();
+ wrapper.setProps({ blocksLoaded: true });
+ click(wrapper.find('div[role="button"]'));
+ expect(onClick).not.toBeCalled();
+});
+
+function shallowRender(props: Partial<LineDuplicationBlockProps> = {}) {
+ return shallow<LineDuplicationBlockProps>(
<LineDuplicationBlock
- duplicated={false}
+ blocksLoaded={false}
+ duplicated={true}
index={1}
- line={line}
- onPopupToggle={jest.fn()}
- popupOpen={false}
+ line={{ line: 3, duplicated: true }}
renderDuplicationPopup={jest.fn()}
+ {...props}
/>
);
- expect(wrapper).toMatchSnapshot();
-});
+}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2020 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { shallow } from 'enzyme';
-import * as React from 'react';
-import { click } from 'sonar-ui-common/helpers/testUtils';
-import LineDuplications from '../LineDuplications';
-
-it('render duplicated line', () => {
- const line = { line: 3, duplicated: true };
- const onClick = jest.fn();
- const wrapper = shallow(<LineDuplications line={line} onClick={onClick} />);
- expect(wrapper).toMatchSnapshot();
- click(wrapper.find('[tabIndex]'));
- expect(onClick).toHaveBeenCalled();
-});
-
-it('render not duplicated line', () => {
- const line = { line: 3, duplicated: false };
- const onClick = jest.fn();
- const wrapper = shallow(<LineDuplications line={line} onClick={onClick} />);
- expect(wrapper).toMatchSnapshot();
-});
import * as React from 'react';
import { click } from 'sonar-ui-common/helpers/testUtils';
import { mockIssue } from '../../../../helpers/testMocks';
-import LineIssuesIndicator from '../LineIssuesIndicator';
+import { LineIssuesIndicator, LineIssuesIndicatorProps } from '../LineIssuesIndicator';
it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot('default');
+ expect(
+ shallowRender({
+ issues: [
+ mockIssue(false, { key: 'foo', type: 'VULNERABILITY' }),
+ mockIssue(false, { key: 'bar', type: 'SECURITY_HOTSPOT' })
+ ]
+ })
+ ).toMatchSnapshot('diff issue types');
+ expect(shallowRender({ issues: [] })).toMatchSnapshot('no issues');
+});
+
+it('should correctly handle click', () => {
const onClick = jest.fn();
const wrapper = shallowRender({ onClick });
- expect(wrapper).toMatchSnapshot();
- click(wrapper);
+ click(wrapper.find('span[role="button"]'));
expect(onClick).toHaveBeenCalled();
-
- const nextIssues = [
- mockIssue(false, { key: 'foo', type: 'VULNERABILITY' }),
- mockIssue(false, { key: 'bar', type: 'SECURITY_HOTSPOT' })
- ];
- wrapper.setProps({ issues: nextIssues });
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should render correctly for no issues', () => {
- expect(shallowRender({ issues: [] })).toMatchSnapshot();
});
-function shallowRender(props: Partial<LineIssuesIndicator['props']> = {}) {
+function shallowRender(props: Partial<LineIssuesIndicatorProps> = {}) {
return shallow(
<LineIssuesIndicator
issues={[
*/
import { shallow } from 'enzyme';
import * as React from 'react';
-import { click } from 'sonar-ui-common/helpers/testUtils';
-import LineNumber from '../LineNumber';
+import { LineNumber, LineNumberProps } from '../LineNumber';
-it('render line 3', () => {
- const line = { line: 3 };
- const wrapper = shallow(<LineNumber line={line} onPopupToggle={jest.fn()} popupOpen={false} />);
- expect(wrapper).toMatchSnapshot();
- click(wrapper);
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot('default');
+ expect(shallowRender({ line: { line: 0 } })).toMatchSnapshot('no line number');
});
-it('render line 0', () => {
- const line = { line: 0 };
- const wrapper = shallow(<LineNumber line={line} onPopupToggle={jest.fn()} popupOpen={false} />);
- expect(wrapper).toMatchSnapshot();
-});
+function shallowRender(props: Partial<LineNumberProps> = {}) {
+ return shallow(<LineNumber line={{ line: 3 }} {...props} />);
+}
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockBranch } from '../../../../helpers/mocks/branch-like';
-import LineOptionsPopup from '../LineOptionsPopup';
+import { LineOptionsPopup } from '../LineOptionsPopup';
jest.mock('../../SourceViewerContext', () => ({
SourceViewerContext: {
}
}));
-it('should render', () => {
+it('should render correctly', () => {
const line = { line: 3 };
const wrapper = shallow(<LineOptionsPopup line={line} />).dive();
expect(wrapper).toMatchSnapshot();
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+/* eslint-disable sonarjs/no-duplicate-string */
import { shallow } from 'enzyme';
import * as React from 'react';
-import { click } from 'sonar-ui-common/helpers/testUtils';
-import LineSCM from '../LineSCM';
+import { LineSCM, LineSCMProps } from '../LineSCM';
-it('render scm details', () => {
- const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' };
- const previousLine = { line: 2, scmAuthor: 'bar', scmDate: '2017-01-02' };
- const wrapper = shallow(
- <LineSCM line={line} onPopupToggle={jest.fn()} popupOpen={false} previousLine={previousLine} />
- );
- expect(wrapper).toMatchSnapshot();
-});
-
-it('render scm details for the first line', () => {
- const line = { line: 3, scmRevision: 'foo', scmAuthor: 'foo', scmDate: '2017-01-01' };
- const wrapper = shallow(
- <LineSCM line={line} onPopupToggle={jest.fn()} popupOpen={false} previousLine={undefined} />
- );
- expect(wrapper).toMatchSnapshot();
-});
+it('should render correctly', () => {
+ const scmInfo = { scmRevision: 'foo', scmAuthor: 'foo', scmDate: '2017-01-01' };
-it('does not render scm details', () => {
- const line = { line: 3, scmRevision: 'foo', scmAuthor: 'foo', scmDate: '2017-01-01' };
- const previousLine = { line: 2, scmRevision: 'foo', scmAuthor: 'foo', scmDate: '2017-01-01' };
- const wrapper = shallow(
- <LineSCM line={line} onPopupToggle={jest.fn()} popupOpen={false} previousLine={previousLine} />
- );
- expect(wrapper).toMatchSnapshot();
+ expect(shallowRender()).toMatchSnapshot('default');
+ expect(
+ shallowRender({ line: { line: 3, ...scmInfo }, previousLine: { line: 2, ...scmInfo } })
+ ).toMatchSnapshot('same commit');
+ expect(shallowRender({ line: { line: 3, scmDate: '2017-01-01' } })).toMatchSnapshot('no author');
});
-it('renders ellipsis when no author info', () => {
- const line = { line: 3, scmRevision: 'foo', scmDate: '2017-01-01' };
- const previousLine = { line: 2, scmRevision: 'bar', scmDate: '2017-01-01' };
- const wrapper = shallow(
- <LineSCM line={line} onPopupToggle={jest.fn()} popupOpen={false} previousLine={previousLine} />
+function shallowRender(props: Partial<LineSCMProps> = {}) {
+ return shallow(
+ <LineSCM
+ line={{ line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' }}
+ previousLine={{ line: 2, scmAuthor: 'bar', scmDate: '2017-01-02' }}
+ {...props}
+ />
);
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should open popup', () => {
- const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' };
- const onPopupToggle = jest.fn();
- const wrapper = shallow(
- <LineSCM line={line} onPopupToggle={onPopupToggle} popupOpen={false} previousLine={undefined} />
- );
- click(wrapper.find('[role="button"]'));
- expect(onPopupToggle).toBeCalledWith({ line: 3, name: 'scm' });
-});
+}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+/* eslint-disable sonarjs/no-duplicate-string */
import { shallow } from 'enzyme';
import * as React from 'react';
-import SCMPopup from '../SCMPopup';
+import { SCMPopup, SCMPopupProps } from '../SCMPopup';
-it('should render', () => {
- const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' };
- expect(shallow(<SCMPopup line={line} />)).toMatchSnapshot();
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot('default');
+ expect(
+ shallowRender({ line: { line: 3, scmDate: '2017-01-01', scmRevision: 'bar' } })
+ ).toMatchSnapshot('no author');
+ expect(
+ shallowRender({ line: { line: 3, scmAuthor: 'foo', scmRevision: 'bar' } })
+ ).toMatchSnapshot('no date');
+ expect(
+ shallowRender({ line: { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' } })
+ ).toMatchSnapshot('no revision');
});
+
+function shallowRender(props: Partial<SCMPopupProps> = {}) {
+ return shallow(
+ <SCMPopup
+ line={{ line: 3, scmAuthor: 'foo', scmDate: '2017-01-01', scmRevision: 'bar' }}
+ {...props}
+ />
+ );
+}
className="source-line source-line-filtered"
data-line-number={16}
>
- <LineNumber
+ <Memo(LineNumber)
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
- onPopupToggle={[MockFunction]}
- popupOpen={false}
/>
- <LineSCM
+ <Memo(LineSCM)
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
- onPopupToggle={[MockFunction]}
- popupOpen={false}
/>
<td
className="source-meta source-line-issues"
className="source-line source-line-highlighted source-line-filtered source-line-last"
data-line-number={16}
>
- <LineNumber
+ <Memo(LineNumber)
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
- onPopupToggle={[MockFunction]}
- popupOpen={false}
/>
- <LineSCM
+ <Memo(LineSCM)
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
- onPopupToggle={[MockFunction]}
- popupOpen={false}
/>
<td
className="source-meta source-line-issues"
className="source-line source-line-filtered"
data-line-number={16}
>
- <LineNumber
+ <Memo(LineNumber)
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
- onPopupToggle={[MockFunction]}
- popupOpen={false}
/>
- <LineSCM
+ <Memo(LineSCM)
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
- onPopupToggle={[MockFunction]}
- popupOpen={false}
/>
<td
className="source-meta source-line-issues"
/>
- <LineCoverage
+ <Memo(LineCoverage)
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
className="source-line source-line-filtered"
data-line-number={16}
>
- <LineNumber
+ <Memo(LineNumber)
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
- onPopupToggle={[MockFunction]}
- popupOpen={false}
/>
- <LineSCM
+ <Memo(LineSCM)
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
- onPopupToggle={[MockFunction]}
- popupOpen={false}
/>
<td
className="source-meta source-line-issues"
/>
- <LineDuplications
- line={
- Object {
- "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
- "coverageStatus": "covered",
- "coveredConditions": 2,
- "duplicated": false,
- "isNew": true,
- "line": 16,
- "scmAuthor": "simon.brandhof@sonarsource.com",
- "scmDate": "2018-12-11T10:48:39+0100",
- "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
- }
- }
- onClick={[MockFunction]}
- />
- <LineDuplicationBlock
+ <Memo(LineDuplicationBlock)
+ blocksLoaded={true}
duplicated={false}
index={0}
key="0"
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
- onPopupToggle={[MockFunction]}
- popupOpen={false}
+ onClick={[MockFunction]}
renderDuplicationPopup={[MockFunction]}
/>
- <LineDuplicationBlock
+ <Memo(LineDuplicationBlock)
+ blocksLoaded={true}
duplicated={false}
index={1}
key="1"
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
- onPopupToggle={[MockFunction]}
- popupOpen={false}
renderDuplicationPopup={[MockFunction]}
/>
- <LineDuplicationBlock
+ <Memo(LineDuplicationBlock)
+ blocksLoaded={true}
duplicated={false}
index={2}
key="2"
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
- onPopupToggle={[MockFunction]}
- popupOpen={false}
renderDuplicationPopup={[MockFunction]}
/>
<LineCode
className="source-line source-line-filtered"
data-line-number={16}
>
- <LineNumber
+ <Memo(LineNumber)
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
- onPopupToggle={[MockFunction]}
- popupOpen={false}
/>
- <LineSCM
+ <Memo(LineSCM)
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
- onPopupToggle={[MockFunction]}
- popupOpen={false}
/>
- <LineIssuesIndicator
+ <Memo(LineIssuesIndicator)
issues={
Array [
Object {
},
]
}
+ issuesOpen={false}
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
className="source-line source-line-filtered"
data-line-number={16}
>
- <LineNumber
+ <Memo(LineNumber)
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
- onPopupToggle={[MockFunction]}
- popupOpen={false}
/>
<td
className="source-meta source-line-issues"
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`render covered line 1`] = `
+exports[`should render correctly: covered 1`] = `
<td
className="source-meta source-line-coverage source-line-covered"
data-line-number={3}
placement="right"
>
<div
+ aria-label="source_viewer.tooltip.covered"
className="source-line-bar"
/>
</Tooltip>
</td>
`;
-exports[`render line with unknown coverage 1`] = `
+exports[`should render correctly: no data 1`] = `
<td
className="source-meta source-line-coverage"
data-line-number={3}
</td>
`;
-exports[`render uncovered line 1`] = `
+exports[`should render correctly: partially covered, 0 conditions 1`] = `
+<td
+ className="source-meta source-line-coverage source-line-partially-covered"
+ data-line-number={3}
+>
+ <Tooltip
+ overlay="source_viewer.tooltip.partially-covered"
+ placement="right"
+ >
+ <div
+ aria-label="source_viewer.tooltip.partially-covered"
+ className="source-line-bar"
+ />
+ </Tooltip>
+</td>
+`;
+
+exports[`should render correctly: partially covered, 10 conditions 1`] = `
+<td
+ className="source-meta source-line-coverage source-line-partially-covered"
+ data-line-number={3}
+>
+ <Tooltip
+ overlay="source_viewer.tooltip.partially-covered"
+ placement="right"
+ >
+ <div
+ aria-label="source_viewer.tooltip.partially-covered"
+ className="source-line-bar"
+ />
+ </Tooltip>
+</td>
+`;
+
+exports[`should render correctly: uncovered 1`] = `
<td
className="source-meta source-line-coverage source-line-uncovered"
data-line-number={3}
placement="right"
>
<div
+ aria-label="source_viewer.tooltip.uncovered"
className="source-line-bar"
/>
</Tooltip>
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`render duplicated line 1`] = `
+exports[`should render correctly: default 1`] = `
<td
- className="source-meta source-line-duplications-extra source-line-duplicated"
+ className="source-meta source-line-duplications source-line-duplicated"
data-index={1}
data-line-number={3}
>
- <Toggler
- onRequestClose={[Function]}
- open={false}
+ <Tooltip
+ overlay="source_viewer.tooltip.duplicated_block"
+ placement="right"
>
- <Tooltip
- overlay="source_viewer.tooltip.duplicated_block"
- placement="right"
- >
- <div
- className="source-line-bar"
- onClick={[Function]}
- role="button"
- tabIndex={0}
- />
- </Tooltip>
- </Toggler>
+ <div>
+ <Toggler
+ onRequestClose={[Function]}
+ open={false}
+ overlay={
+ <DropdownOverlay
+ placement="right-top"
+ />
+ }
+ >
+ <div
+ aria-label="source_viewer.tooltip.duplicated_block"
+ className="source-line-bar"
+ onClick={[Function]}
+ role="button"
+ tabIndex={0}
+ />
+ </Toggler>
+ </div>
+ </Tooltip>
</td>
`;
-exports[`render not duplicated line 1`] = `
+exports[`should render correctly: not duplicated 1`] = `
<td
- className="source-meta source-line-duplications-extra"
+ className="source-meta source-line-duplications"
data-index={1}
data-line-number={3}
>
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`render duplicated line 1`] = `
-<Tooltip
- overlay="source_viewer.tooltip.duplicated_line"
- placement="right"
->
- <td
- className="source-meta source-line-duplications source-line-duplicated"
- onClick={[Function]}
- role="button"
- tabIndex={0}
- >
- <div
- className="source-line-bar"
- />
- </td>
-</Tooltip>
-`;
-
-exports[`render not duplicated line 1`] = `
-<td
- className="source-meta source-line-duplications"
->
- <div
- className="source-line-bar"
- />
-</td>
-`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should render correctly 1`] = `
+exports[`should render correctly: default 1`] = `
<td
className="source-meta source-line-issues source-line-with-issues"
data-line-number={3}
- onClick={[Function]}
- role="button"
- tabIndex={0}
>
- <IssueIcon
- type="BUG"
- />
<span
- className="source-line-issues-counter"
+ aria-label="source_viewer.issues_on_line.show"
+ onClick={[Function]}
+ role="button"
+ tabIndex={0}
>
- 2
+ <IssueIcon
+ type="BUG"
+ />
+ <span
+ className="source-line-issues-counter"
+ >
+ 2
+ </span>
</span>
</td>
`;
-exports[`should render correctly 2`] = `
+exports[`should render correctly: diff issue types 1`] = `
<td
className="source-meta source-line-issues source-line-with-issues"
data-line-number={3}
- onClick={[Function]}
- role="button"
- tabIndex={0}
>
- <IssueIcon
- type="VULNERABILITY"
- />
<span
- className="source-line-issues-counter"
+ aria-label="source_viewer.issues_on_line.show"
+ onClick={[Function]}
+ role="button"
+ tabIndex={0}
>
- 2
+ <IssueIcon
+ type="VULNERABILITY"
+ />
+ <span
+ className="source-line-issues-counter"
+ >
+ 2
+ </span>
</span>
</td>
`;
-exports[`should render correctly for no issues 1`] = `
+exports[`should render correctly: no issues 1`] = `
<td
className="source-meta source-line-issues"
data-line-number={3}
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`render line 0 1`] = `
-<td
- className="source-meta source-line-number"
-/>
-`;
-
-exports[`render line 3 1`] = `
+exports[`should render correctly: default 1`] = `
<td
className="source-meta source-line-number"
data-line-number={3}
- onClick={[Function]}
- role="button"
- tabIndex={0}
>
- <Toggler
- onRequestClose={[Function]}
- open={false}
+ <Dropdown
overlay={
- <LineOptionsPopup
+ <Memo(LineOptionsPopup)
line={
Object {
"line": 3,
}
/>
}
- />
+ overlayPlacement="right-top"
+ >
+ <span
+ aria-label="source_viewer.line_X.3"
+ role="button"
+ >
+ 3
+ </span>
+ </Dropdown>
</td>
`;
+
+exports[`should render correctly: no line number 1`] = `
+<td
+ className="source-meta source-line-number"
+/>
+`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should render 1`] = `
-<DropdownOverlay
- placement="right-top"
+exports[`should render correctly 1`] = `
+<div
+ className="source-viewer-bubble-popup nowrap"
>
- <div
- className="source-viewer-bubble-popup nowrap"
- >
- <Link
- className="js-get-permalink"
- onClick={[Function]}
- onlyActiveOnIndex={false}
- rel="noopener noreferrer"
- style={Object {}}
- target="_blank"
- to={
- Object {
- "pathname": "/code",
- "query": Object {
- "branch": "feature",
- "id": "prj",
- "line": 3,
- "selected": "foo",
- },
- }
+ <Link
+ className="js-get-permalink"
+ onlyActiveOnIndex={false}
+ rel="noopener noreferrer"
+ style={Object {}}
+ target="_blank"
+ to={
+ Object {
+ "pathname": "/code",
+ "query": Object {
+ "branch": "feature",
+ "id": "prj",
+ "line": 3,
+ "selected": "foo",
+ },
}
- >
- component_viewer.get_permalink
- </Link>
- </div>
-</DropdownOverlay>
+ }
+ >
+ component_viewer.get_permalink
+ </Link>
+</div>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`does not render scm details 1`] = `
+exports[`should render correctly: default 1`] = `
<td
className="source-meta source-line-scm"
data-line-number={3}
- onClick={[Function]}
- role="button"
- tabIndex={0}
>
- <Toggler
- onRequestClose={[Function]}
- open={false}
+ <Dropdown
overlay={
- <SCMPopup
- line={
- Object {
- "line": 3,
- "scmAuthor": "foo",
- "scmDate": "2017-01-01",
- "scmRevision": "foo",
- }
- }
- />
- }
- />
-</td>
-`;
-
-exports[`render scm details 1`] = `
-<td
- className="source-meta source-line-scm"
- data-line-number={3}
- onClick={[Function]}
- role="button"
- tabIndex={0}
->
- <Toggler
- onRequestClose={[Function]}
- open={false}
- overlay={
- <SCMPopup
+ <Memo(SCMPopup)
line={
Object {
"line": 3,
}
/>
}
+ overlayPlacement="right-top"
>
<div
- className="source-line-scm-inner"
- data-author="foo"
- />
- </Toggler>
+ aria-label="source_viewer.author_X.foo, source_viewer.click_for_scm_info"
+ role="button"
+ >
+ <div
+ className="source-line-scm-inner"
+ >
+ foo
+ </div>
+ </div>
+ </Dropdown>
</td>
`;
-exports[`render scm details for the first line 1`] = `
+exports[`should render correctly: no author 1`] = `
<td
className="source-meta source-line-scm"
data-line-number={3}
- onClick={[Function]}
- role="button"
- tabIndex={0}
>
- <Toggler
- onRequestClose={[Function]}
- open={false}
+ <Dropdown
overlay={
- <SCMPopup
+ <Memo(SCMPopup)
line={
Object {
"line": 3,
- "scmAuthor": "foo",
"scmDate": "2017-01-01",
- "scmRevision": "foo",
}
}
/>
}
+ overlayPlacement="right-top"
>
<div
- className="source-line-scm-inner"
- data-author="foo"
- />
- </Toggler>
+ aria-label="source_viewer.click_for_scm_info"
+ role="button"
+ >
+ <div
+ className="source-line-scm-inner"
+ >
+ …
+ </div>
+ </div>
+ </Dropdown>
</td>
`;
-exports[`renders ellipsis when no author info 1`] = `
+exports[`should render correctly: same commit 1`] = `
<td
className="source-meta source-line-scm"
data-line-number={3}
- onClick={[Function]}
- role="button"
- tabIndex={0}
>
- <Toggler
- onRequestClose={[Function]}
- open={false}
+ <Dropdown
overlay={
- <SCMPopup
+ <Memo(SCMPopup)
line={
Object {
"line": 3,
+ "scmAuthor": "foo",
"scmDate": "2017-01-01",
"scmRevision": "foo",
}
}
/>
}
+ overlayPlacement="right-top"
>
<div
- className="source-line-scm-inner"
- data-author="…"
- />
- </Toggler>
+ aria-label="source_viewer.author_X.foo, source_viewer.click_for_scm_info"
+ role="button"
+ >
+ <div
+ className="source-line-scm-inner"
+ >
+
+ </div>
+ </div>
+ </Dropdown>
</td>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should render 1`] = `
-<DropdownOverlay
- placement="right-top"
+exports[`should render correctly: default 1`] = `
+<div
+ className="source-viewer-bubble-popup abs-width-400"
>
+ <div>
+ <h4>
+ author
+ </h4>
+ foo
+ </div>
+ <div
+ className="spacer-top"
+ >
+ <h4>
+ source_viewer.tooltip.scm.commited_on
+ </h4>
+ <DateFormatter
+ date="2017-01-01"
+ />
+ </div>
+ <div
+ className="spacer-top"
+ >
+ <h4>
+ source_viewer.tooltip.scm.revision
+ </h4>
+ bar
+ </div>
+</div>
+`;
+
+exports[`should render correctly: no author 1`] = `
+<div
+ className="source-viewer-bubble-popup abs-width-400"
+>
+ <div
+ className=""
+ >
+ <h4>
+ source_viewer.tooltip.scm.commited_on
+ </h4>
+ <DateFormatter
+ date="2017-01-01"
+ />
+ </div>
+ <div
+ className="spacer-top"
+ >
+ <h4>
+ source_viewer.tooltip.scm.revision
+ </h4>
+ bar
+ </div>
+</div>
+`;
+
+exports[`should render correctly: no date 1`] = `
+<div
+ className="source-viewer-bubble-popup abs-width-400"
+>
+ <div>
+ <h4>
+ author
+ </h4>
+ foo
+ </div>
+ <div
+ className="spacer-top"
+ >
+ <h4>
+ source_viewer.tooltip.scm.revision
+ </h4>
+ bar
+ </div>
+</div>
+`;
+
+exports[`should render correctly: no revision 1`] = `
+<div
+ className="source-viewer-bubble-popup abs-width-400"
+>
+ <div>
+ <h4>
+ author
+ </h4>
+ foo
+ </div>
<div
- className="source-viewer-bubble-popup abs-width-400"
+ className="spacer-top"
>
- <div>
- foo
- </div>
- <div
- className="spacer-top"
- >
- <DateFormatter
- date="2017-01-01"
- />
- </div>
- </div>
-</DropdownOverlay>
+ <h4>
+ source_viewer.tooltip.scm.commited_on
+ </h4>
+ <DateFormatter
+ date="2017-01-01"
+ />
+ </div>
+</div>
`;
source_viewer.covered=Covered by the following tests
source_viewer.not_covered=Not covered by tests
source_viewer.conditions=conditions
+source_viewer.line_X=Line: {0}
+source_viewer.click_for_scm_info=Click to see SCM information
+source_viewer.author_X=Author: {0}
source_viewer.tooltip.duplicated_line=This line is duplicated. Click to see duplicated blocks.
source_viewer.tooltip.duplicated_block=Duplicated block. Click for details.
source_viewer.tooltip.uncovered=Not covered by tests.
source_viewer.tooltip.uncovered.conditions=Not covered by tests ({0} conditions).
source_viewer.tooltip.no_information_about_tests=There is no extra information about test files.
+source_viewer.tooltip.scm.commited_on=Committed on
+source_viewer.tooltip.scm.revision=Revision
+
+source_viewer.issues_on_line.show=Click to show all issues on this line
+source_viewer.issues_on_line.hide=Click to hide all issues on this line
source_viewer.load_more_code=Load More Code
source_viewer.loading_more_code=Loading More Code...