* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { intersection, uniqBy } from 'lodash';
+import { intersection } from 'lodash';
import * as React from 'react';
import {
getComponentData,
symbolsByLine
} from './helpers/indexing';
import { LINES_TO_LOAD } from './helpers/lines';
-import defaultLoadIssues from './helpers/loadIssues';
+import loadIssues from './helpers/loadIssues';
import SourceViewerCode from './SourceViewerCode';
import { SourceViewerContext } from './SourceViewerContext';
import SourceViewerHeader from './SourceViewerHeader';
// but kept to maintaint the location indexes
highlightedLocations?: (FlowLocation | undefined)[];
highlightedLocationMessage?: { index: number; text: string | undefined };
- loadIssues?: (
- component: string,
- from: number,
- to: number,
- branchLike: BranchLike | undefined
- ) => Promise<Issue[]>;
onLoaded?: (component: SourceViewerFile, sources: SourceLine[], issues: Issue[]) => void;
onLocationSelect?: (index: number) => void;
onIssueChange?: (issue: Issue) => void;
}
export default class SourceViewer extends React.PureComponent<Props, State> {
- node?: HTMLElement | null;
mounted = false;
static defaultProps = {
}
);
}
- } else {
- this.checkSelectedIssueChange();
}
}
}));
}
- checkSelectedIssueChange() {
- const { selectedIssue } = this.props;
- const { issues } = this.state;
- if (
- selectedIssue !== undefined &&
- issues !== undefined &&
- issues.find(issue => issue.key === selectedIssue) === undefined
- ) {
- this.reloadIssues();
- }
- }
-
loadSources(
key: string,
from: number | undefined,
return getSources({ key, from, to, ...getBranchLikeQuery(branchLike) });
}
- get loadIssues() {
- return this.props.loadIssues || defaultLoadIssues;
- }
-
computeCoverageStatus(lines: SourceLine[]) {
return lines.map(line => ({ ...line, coverageStatus: getCoverageStatus(line) }));
}
fetchComponent() {
this.setState({ loading: true });
- const to = (this.props.aroundLine || 0) + LINES_TO_LOAD;
- const loadIssues = (component: SourceViewerFile, sources: SourceLine[]) => {
- this.loadIssues(this.props.component, 1, to, this.props.branchLike).then(
+ const loadIssuesCallback = (component: SourceViewerFile, sources: SourceLine[]) => {
+ loadIssues(this.props.component, this.props.branchLike).then(
issues => {
if (this.mounted) {
const finalSources = sources.slice(0, LINES_TO_LOAD);
const sourcesRequest =
component.q === 'FIL' || component.q === 'UTS' ? this.fetchSources() : Promise.resolve([]);
sourcesRequest.then(
- sources => loadIssues(component, sources),
+ sources => loadIssuesCallback(component, sources),
response => onFailLoadSources(response, component)
);
};
);
}
- reloadIssues() {
- if (!this.state.sources) {
- return;
- }
- const firstSourceLine = this.state.sources[0];
- const lastSourceLine = this.state.sources[this.state.sources.length - 1];
- this.loadIssues(
- this.props.component,
- firstSourceLine && firstSourceLine.line,
- lastSourceLine && lastSourceLine.line,
- this.props.branchLike
- ).then(
- issues => {
- if (this.mounted) {
- this.setState({
- issues,
- issuesByLine: issuesByLine(issues),
- issueLocationsByLine: locationsByLine(issues)
- });
- }
- },
- () => {
- /* no op */
- }
- );
- }
-
fetchSources = (): Promise<SourceLine[]> => {
return new Promise((resolve, reject) => {
const onFailLoadSources = (response: Response) => {
const firstSourceLine = this.state.sources[0];
this.setState({ loadingSourcesBefore: true });
const from = Math.max(1, firstSourceLine.line - LINES_TO_LOAD);
- Promise.all([
- this.loadSources(this.props.component, from, firstSourceLine.line - 1, this.props.branchLike),
- this.loadIssues(this.props.component, from, firstSourceLine.line - 1, this.props.branchLike)
- ]).then(
- ([sources, issues]) => {
+ this.loadSources(
+ this.props.component,
+ from,
+ firstSourceLine.line - 1,
+ this.props.branchLike
+ ).then(
+ sources => {
if (this.mounted) {
this.setState(prevState => {
- const nextIssues = uniqBy([...issues, ...(prevState.issues || [])], issue => issue.key);
return {
- issues: nextIssues,
- issuesByLine: issuesByLine(nextIssues),
- issueLocationsByLine: locationsByLine(nextIssues),
loadingSourcesBefore: false,
sources: [...this.computeCoverageStatus(sources), ...(prevState.sources || [])],
symbolsByLine: { ...prevState.symbolsByLine, ...symbolsByLine(sources) }
const lastSourceLine = this.state.sources[this.state.sources.length - 1];
this.setState({ loadingSourcesAfter: true });
const fromLine = lastSourceLine.line + 1;
- // request one additional line to define `hasSourcesAfter`
const toLine = lastSourceLine.line + LINES_TO_LOAD + 1;
- Promise.all([
- this.loadSources(this.props.component, fromLine, toLine, this.props.branchLike),
- this.loadIssues(this.props.component, fromLine, toLine, this.props.branchLike)
- ]).then(
- ([sources, issues]) => {
+ this.loadSources(this.props.component, fromLine, toLine, this.props.branchLike).then(
+ sources => {
if (this.mounted) {
+ const hasSourcesAfter = LINES_TO_LOAD < sources.length;
+ if (hasSourcesAfter) {
+ sources.pop();
+ }
this.setState(prevState => {
- const nextIssues = uniqBy([...(prevState.issues || []), ...issues], issue => issue.key);
return {
- issues: nextIssues,
- issuesByLine: issuesByLine(nextIssues),
- issueLocationsByLine: locationsByLine(nextIssues),
- hasSourcesAfter: sources.length > LINES_TO_LOAD,
+ hasSourcesAfter,
loadingSourcesAfter: false,
- sources: [
- ...(prevState.sources || []),
- ...this.computeCoverageStatus(sources.slice(0, LINES_TO_LOAD))
- ],
+ sources: [...(prevState.sources || []), ...this.computeCoverageStatus(sources)],
symbolsByLine: {
...prevState.symbolsByLine,
- ...symbolsByLine(sources.slice(0, LINES_TO_LOAD))
+ ...symbolsByLine(sources)
}
};
});
return (
<SourceViewerContext.Provider value={{ branchLike: this.props.branchLike, file: component }}>
- <div className="source-viewer" ref={node => (this.node = node)}>
+ <div className="source-viewer">
{this.renderHeader(component)}
{sourceRemoved && (
<Alert className="spacer-top" variant="warning">
import { HttpStatus } from '../../../helpers/request';
import { mockIssue } from '../../../helpers/testMocks';
import { renderComponent } from '../../../helpers/testReactTestingUtils';
+import loadIssues from '../helpers/loadIssues';
import SourceViewer from '../SourceViewer';
jest.mock('../../../api/components');
jest.mock('../../../api/issues');
+jest.mock('../helpers/loadIssues', () => ({
+ __esModule: true,
+ default: jest.fn().mockResolvedValue([])
+}));
jest.mock('../helpers/lines', () => {
const lines = jest.requireActual('../helpers/lines');
return {
});
it('should show issue on empty file', async () => {
+ (loadIssues as jest.Mock).mockResolvedValueOnce([
+ mockIssue(false, {
+ key: 'first-issue',
+ message: 'First Issue',
+ line: undefined,
+ textRange: undefined
+ })
+ ]);
renderSourceViewer({
- component: handler.getEmptyFile(),
- loadIssues: jest.fn().mockResolvedValue([
- mockIssue(false, {
- key: 'first-issue',
- message: 'First Issue',
- line: undefined,
- textRange: undefined
- })
- ])
+ component: handler.getEmptyFile()
});
expect(await screen.findByRole('table')).toBeInTheDocument();
expect(await screen.findByRole('row', { name: 'First Issue' })).toBeInTheDocument();
});
it('should be able to interact with issue action', async () => {
+ (loadIssues as jest.Mock).mockResolvedValueOnce([
+ mockIssue(false, {
+ actions: ['set_type', 'set_tags', 'comment', 'set_severity', 'assign'],
+ key: 'first-issue',
+ message: 'First Issue',
+ line: 1,
+ textRange: { startLine: 1, endLine: 1, startOffset: 0, endOffset: 1 }
+ })
+ ]);
const user = userEvent.setup();
- renderSourceViewer({
- loadIssues: jest.fn().mockResolvedValue([
- mockIssue(false, {
- actions: ['set_type', 'set_tags', 'comment', 'set_severity', 'assign'],
- key: 'first-issue',
- message: 'First Issue',
- line: 1,
- textRange: { startLine: 1, endLine: 1, startOffset: 0, endOffset: 1 }
- })
- ])
- });
+ renderSourceViewer();
//Open Issue type
await user.click(
});
it('should show issue indicator', async () => {
+ (loadIssues as jest.Mock).mockResolvedValueOnce([
+ mockIssue(false, {
+ key: 'first-issue',
+ message: 'First Issue',
+ line: 1,
+ textRange: { startLine: 1, endLine: 1, startOffset: 0, endOffset: 1 }
+ }),
+ mockIssue(false, {
+ key: 'second-issue',
+ message: 'Second Issue',
+ line: 1,
+ textRange: { startLine: 1, endLine: 1, startOffset: 1, endOffset: 2 }
+ })
+ ]);
const user = userEvent.setup();
const onIssueSelect = jest.fn();
renderSourceViewer({
onIssueSelect,
- displayAllIssues: false,
- loadIssues: jest.fn().mockResolvedValue([
- mockIssue(false, {
- key: 'first-issue',
- message: 'First Issue',
- line: 1,
- textRange: { startLine: 1, endLine: 1, startOffset: 0, endOffset: 1 }
- }),
- mockIssue(false, {
- key: 'second-issue',
- message: 'Second Issue',
- line: 1,
- textRange: { startLine: 1, endLine: 1, startOffset: 1, endOffset: 2 }
- })
- ])
+ displayAllIssues: false
});
const row = await screen.findByRole('row', { name: /.*\/ \*$/ });
const issueRow = within(row);
displayIssueLocationsCount={true}
displayIssueLocationsLink={false}
displayLocationMarkers={true}
- loadIssues={jest.fn().mockResolvedValue([])}
onIssueChange={jest.fn()}
onIssueSelect={jest.fn()}
onLoaded={jest.fn()}
expect(wrapper).toMatchSnapshot();
});
-it('should use load props if provided', () => {
- const loadIssues = jest.fn().mockResolvedValue([]);
- const wrapper = shallowRender({
- loadIssues
- });
-
- expect(wrapper.instance().loadIssues).toBe(loadIssues);
-});
-
-it('should reload', async () => {
- (defaultLoadIssues as jest.Mock)
- .mockResolvedValueOnce([mockIssue()])
- .mockResolvedValueOnce([mockIssue()]);
- (getComponentForSourceViewer as jest.Mock).mockResolvedValueOnce(mockSourceViewerFile());
- (getComponentData as jest.Mock).mockResolvedValueOnce({
- component: { leakPeriodDate: '2018-06-20T17:12:19+0200' }
- });
- (getSources as jest.Mock).mockResolvedValueOnce([mockSourceLine()]);
-
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
-
- wrapper.instance().reloadIssues();
-
- expect(defaultLoadIssues).toBeCalledTimes(2);
-
- await waitAndUpdate(wrapper);
-
- expect(wrapper.state().issues).toHaveLength(1);
-});
-
it('should load sources before', async () => {
- (defaultLoadIssues as jest.Mock)
- .mockResolvedValueOnce([mockIssue(false, { key: 'issue1' })])
- .mockResolvedValueOnce([mockIssue(false, { key: 'issue2' })]);
+ (defaultLoadIssues as jest.Mock).mockResolvedValueOnce([
+ mockIssue(false, { key: 'issue1' }),
+ mockIssue(false, { key: 'issue2' })
+ ]);
(getComponentForSourceViewer as jest.Mock).mockResolvedValueOnce(mockSourceViewerFile());
(getComponentData as jest.Mock).mockResolvedValueOnce({
component: { leakPeriodDate: '2018-06-20T17:12:19+0200' }
wrapper.instance().loadSourcesBefore();
expect(wrapper.state().loadingSourcesBefore).toBe(true);
- expect(defaultLoadIssues).toBeCalledTimes(2);
+ expect(defaultLoadIssues).toBeCalledTimes(1);
expect(getSources).toBeCalledTimes(2);
await waitAndUpdate(wrapper);
});
it('should load sources after', async () => {
- (defaultLoadIssues as jest.Mock)
- .mockResolvedValueOnce([mockIssue(false, { key: 'issue1' })])
- .mockResolvedValueOnce([mockIssue(false, { key: 'issue2' })]);
+ (defaultLoadIssues as jest.Mock).mockResolvedValueOnce([
+ mockIssue(false, { key: 'issue1' }),
+ mockIssue(false, { key: 'issue2' })
+ ]);
(getComponentForSourceViewer as jest.Mock).mockResolvedValueOnce(mockSourceViewerFile());
(getComponentData as jest.Mock).mockResolvedValueOnce({
component: { leakPeriodDate: '2018-06-20T17:12:19+0200' }
wrapper.instance().loadSourcesAfter();
expect(wrapper.state().loadingSourcesAfter).toBe(true);
- expect(defaultLoadIssues).toBeCalledTimes(2);
+ expect(defaultLoadIssues).toBeCalledTimes(1);
expect(getSources).toBeCalledTimes(2);
await waitAndUpdate(wrapper);
import { LinearIssueLocation, SourceLine } from '../../../types/types';
import LocationIndex from '../../common/LocationIndex';
import Tooltip from '../../controls/Tooltip';
-import { highlightIssueLocations, highlightSymbol, splitByTokens } from '../helpers/highlight';
+import {
+ highlightIssueLocations,
+ highlightSymbol,
+ splitByTokens,
+ Token
+} from '../helpers/highlight';
interface Props {
className?: string;
}
};
+ renderToken(tokens: Token[]) {
+ const { highlightedLocationMessage, secondaryIssueLocations } = this.props;
+ const renderedTokens: React.ReactNode[] = [];
+
+ // track if the first marker is displayed before the source code
+ // set `false` for the first token in a row
+ let leadingMarker = false;
+
+ tokens.forEach((token, index) => {
+ if (this.props.displayLocationMarkers && token.markers.length > 0) {
+ token.markers.forEach(marker => {
+ const selected =
+ highlightedLocationMessage !== undefined && highlightedLocationMessage.index === marker;
+ const loc = secondaryIssueLocations.find(loc => loc.index === marker);
+ const message = loc && loc.text;
+ renderedTokens.push(this.renderMarker(marker, message, selected, leadingMarker));
+ });
+ }
+ renderedTokens.push(
+ // eslint-disable-next-line react/no-array-index-key
+ <span className={token.className} key={index}>
+ {token.text}
+ </span>
+ );
+
+ // keep leadingMarker truthy if previous token has only whitespaces
+ leadingMarker = (index === 0 ? true : leadingMarker) && !token.text.trim().length;
+ });
+ return renderedTokens;
+ }
+
renderMarker(index: number, message: string | undefined, selected: boolean, leading: boolean) {
const { onLocationSelect } = this.props;
const onClick = onLocationSelect ? () => onLocationSelect(index) : undefined;
secondaryIssueLocations
} = this.props;
- let tokens = splitByTokens(this.props.line.code || '');
+ const container = document.createElement('div');
+ container.innerHTML = this.props.line.code || '';
+
+ let tokens = splitByTokens(container.childNodes);
if (highlightedSymbols) {
highlightedSymbols.forEach(symbol => {
}
}
- const renderedTokens: React.ReactNode[] = [];
-
- // track if the first marker is displayed before the source code
- // set `false` for the first token in a row
- let leadingMarker = false;
-
- tokens.forEach((token, index) => {
- if (this.props.displayLocationMarkers && token.markers.length > 0) {
- token.markers.forEach(marker => {
- const selected =
- highlightedLocationMessage !== undefined && highlightedLocationMessage.index === marker;
- const loc = secondaryIssueLocations.find(loc => loc.index === marker);
- const message = loc && loc.text;
- renderedTokens.push(this.renderMarker(marker, message, selected, leadingMarker));
- });
- }
- renderedTokens.push(
- <span className={token.className} key={index}>
- {token.text}
- </span>
- );
-
- // keep leadingMarker truthy if previous token has only whitespaces
- leadingMarker = (index === 0 ? true : leadingMarker) && !token.text.trim().length;
- });
+ const renderedTokens = this.renderToken(tokens);
const style = padding ? { paddingBottom: `${padding}px` } : undefined;
describe('loadIssues', () => {
it('should load issues', async () => {
- const result = await loadIssues('foo.java', 1, 500, mockMainBranch());
+ const result = await loadIssues('foo.java', mockMainBranch());
expect(result).toMatchSnapshot();
});
});
const ISSUE_LOCATION_CLASS = 'source-line-code-issue';
-export function splitByTokens(code: string, rootClassName = ''): Token[] {
- const container = document.createElement('div');
+export function splitByTokens(code: NodeListOf<ChildNode>, rootClassName = ''): Token[] {
let tokens: Token[] = [];
- container.innerHTML = code;
- [].forEach.call(container.childNodes, (node: Element) => {
+ Array.prototype.forEach.call(code, (node: Element) => {
if (node.nodeType === 1) {
// ELEMENT NODE
const fullClassName = rootClassName ? rootClassName + ' ' + node.className : node.className;
- const innerTokens = splitByTokens(node.innerHTML, fullClassName);
+ const innerTokens = splitByTokens(node.childNodes, fullClassName);
tokens = tokens.concat(innerTokens);
}
if (node.nodeType === 3 && node.nodeValue) {
export function symbolsByLine(sources: SourceLine[]) {
const index: { [line: number]: string[] } = {};
sources.forEach(line => {
- const tokens = splitByTokens(line.code || '');
+ const container = document.createElement('div');
+ container.innerHTML = line.code || '';
+ const tokens = splitByTokens(container.childNodes);
const symbols = flatten(
tokens.map(token => {
const keys = token.className.match(/sym-\d+/g);
import { intersection } from 'lodash';
import { LinearIssueLocation } from '../../../types/types';
-export const LINES_TO_LOAD = 500;
+export const LINES_TO_LOAD = 1000;
export function optimizeHighlightedSymbols(
symbolsForLine: string[] = [],
// maximum possible value
const PAGE_SIZE = 500;
+// Maximum issues return 20*500 for the API.
+const PAGE_MAX = 20;
function buildQuery(component: string, branchLike: BranchLike | undefined) {
return {
};
}
-export function loadPage(query: RawQuery, page: number, pageSize = PAGE_SIZE): Promise<Issue[]> {
+function loadPage(query: RawQuery, page: number, pageSize = PAGE_SIZE): Promise<Issue[]> {
return searchIssues({
...query,
p: page,
);
}
-export function loadPageAndNext(
- query: RawQuery,
- toLine: number,
- page: number,
- pageSize = PAGE_SIZE
-): Promise<Issue[]> {
- return loadPage(query, page).then(issues => {
- if (issues.length === 0) {
- return [];
- }
+async function loadPageAndNext(query: RawQuery, page = 1, pageSize = PAGE_SIZE): Promise<Issue[]> {
+ const issues = await loadPage(query, page);
- const lastIssue = issues[issues.length - 1];
+ if (issues.length === 0) {
+ return [];
+ }
- if (
- (lastIssue.textRange != null && lastIssue.textRange.endLine > toLine) ||
- issues.length < pageSize
- ) {
- return issues;
- }
+ if (issues.length < pageSize || page >= PAGE_MAX) {
+ return issues;
+ }
- return loadPageAndNext(query, toLine, page + 1, pageSize).then(nextIssues => {
- return [...issues, ...nextIssues];
- });
- });
+ const nextIssues = await loadPageAndNext(query, page + 1, pageSize);
+ return [...issues, ...nextIssues];
}
export default function loadIssues(
component: string,
- _fromLine: number,
- toLine: number,
branchLike: BranchLike | undefined
): Promise<Issue[]> {
const query = buildQuery(component, branchLike);
- return new Promise(resolve => {
- loadPageAndNext(query, toLine, 1).then(issues => {
- resolve(issues);
- });
- });
+ return loadPageAndNext(query);
}